Przeglądaj źródła

Redesign AiAssistantManagementModal

Shun Miyazawa 1 rok temu
rodzic
commit
dd980de99a

+ 3 - 1
apps/app/public/static/locales/en_US/translation.json

@@ -501,7 +501,9 @@
     "budget_exceeded_for_growi_cloud": "You have reached your OpenAI API usage limit. To use the Knowledge Assistant again, please add credits from the GROWI.cloud admin page for Hosted users or from the OpenAI billing page for Owned users.",
     "error_message": "An error has occurred",
     "show_error_detail": "Show error details"
-
+  },
+  "modal_ai_assistant": {
+    "default_instruction": "You are the knowledge assistant for this Wiki. Please provide support according to the following guidelines:\n\n- Analyze document relevance and connect information\n- Suggest new perspectives\n- Provide accurate information based on understanding the intent of questions\nI will provide information in a structured format when necessary."
   },
   "link_edit": {
     "edit_link": "Edit Link",

+ 3 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -497,6 +497,9 @@
     "error_message": "Erreur",
     "show_error_detail": "Détails de l'exposition"
   },
+  "modal_ai_assistant": {
+    "default_instruction": "Vous êtes l'assistant de connaissances pour ce Wiki. Veuillez fournir un support selon les directives suivantes :\n\n- Analyser la pertinence des documents et relier les informations\n- Proposer de nouvelles perspectives\n- Fournir des informations précises en comprenant l'intention des questions\nJe fournirai les informations sous forme structurée si nécessaire."
+  },
   "link_edit": {
     "edit_link": "Modifier lien",
     "set_link_and_label": "Ajouter lien et étiquette",

+ 3 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -535,6 +535,9 @@
     "error_message": "エラーが発生しました",
     "show_error_detail": "詳細を表示"
   },
+  "modal_ai_assistant": {
+    "default_instruction": "あなたはこのWikiの知識アシスタントです。以下の方針で支援を行ってください:\n\n- 文書の関連性分析と情報の関連付け\n- 新しい視点の提案\n- 質問の意図を理解した的確な情報提供 必要に応じて構造化された形式で情報を提供します。"
+  },
   "link_edit": {
     "edit_link": "リンク編集",
     "set_link_and_label": "リンク情報",

+ 3 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -491,6 +491,9 @@
     "error_message": "错误",
     "show_error_detail": "显示详情"
   },
+  "modal_ai_assistant": {
+    "default_instruction": "您是这个Wiki的知识助手。请按照以下方针提供支持:\n\n- 分析文档相关性并连接信息\n- 提出新的观点\n- 理解问题意图并提供准确信息\n必要时我会以结构化的形式提供信息。"
+  },
   "link_edit": {
     "edit_link": "Edit Link",
     "set_link_and_label": "Set link and label",

+ 39 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditInstruction.tsx

@@ -0,0 +1,39 @@
+import { ModalBody, Input } from 'reactstrap';
+
+import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
+
+type Props = {
+  instruction: string;
+  onChange: (value: string) => void;
+  onReset: () => void;
+}
+
+export const AiAssistantManagementEditInstruction = (props: Props): JSX.Element => {
+  const { instruction, onChange, onReset } = props;
+
+  return (
+    <>
+      <AiAssistantManagementHeader />
+
+      <ModalBody className="px-4">
+        <p className="text-secondary py-1">
+          アシスタントの振る舞いを決める指示文を設定できます。<br />
+          この指示に従ってにアシスタントの回答や分析を行います。
+        </p>
+
+        <Input
+          autoFocus
+          type="textarea"
+          className="mb-3"
+          rows="8"
+          value={instruction}
+          onChange={e => onChange(e.target.value)}
+        />
+
+        <button type="button" onClick={onReset} className="btn btn-outline-secondary btn-sm">
+          デフォルトに戻す
+        </button>
+      </ModalBody>
+    </>
+  );
+};

+ 24 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHeader.tsx

@@ -0,0 +1,24 @@
+import { ModalHeader } from 'reactstrap';
+
+import { useAiAssistantManegementModal, AiAssistantManegementModalPageMode } from '../../../stores/ai-assistant';
+
+export const AiAssistantManagementHeader = (): JSX.Element => {
+  const { close, changePageMode } = useAiAssistantManegementModal();
+
+  return (
+    <ModalHeader
+      close={(
+        <button type="button" className="btn p-0" onClick={close}>
+          <span className="material-symbols-outlined">close</span>
+        </button>
+      )}
+    >
+      <div className="d-flex align-items-center">
+        <button type="button" className="btn p-0 me-3" onClick={() => changePageMode(AiAssistantManegementModalPageMode.HOME)}>
+          <span className="material-symbols-outlined text-primary">chevron_left</span>
+        </button>
+        <span>アシスタントへの指示</span>
+      </div>
+    </ModalHeader>
+  );
+};

+ 97 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx

@@ -0,0 +1,97 @@
+import React from 'react';
+
+import {
+  ModalHeader, ModalBody, ModalFooter, Input,
+} from 'reactstrap';
+
+import { useAiAssistantManegementModal, AiAssistantManegementModalPageMode } from '../../../stores/ai-assistant';
+
+type Props = {
+  instruction: string;
+}
+
+export const AiAssistantManagementHome = (props: Props): JSX.Element => {
+  const { instruction } = props;
+
+  const { close: closeAiAssistantManegementModal, changePageMode } = useAiAssistantManegementModal();
+
+  return (
+    <>
+      <ModalHeader tag="h4" toggle={closeAiAssistantManegementModal} className="pe-4">
+        <span className="growi-custom-icons growi-ai-assistant-icon me-3 fs-4">ai_assistant</span>
+        <span className="fw-bold">新規アシスタントの追加</span> {/* TODO i18n */}
+      </ModalHeader>
+
+      <div className="px-4">
+        <ModalBody>
+          <div className="mb-4">
+            <Input
+              type="text"
+              placeholder="アシスタント名を入力"
+              bsSize="lg"
+              className="border-0 border-bottom border-primary px-0 rounded-0 shadow-none"
+            />
+          </div>
+
+          <div className="mb-4">
+            <div className="d-flex align-items-center mb-2">
+              <span className="text-secondary">アシスタントのメモ</span>
+              <span className="badge text-bg-secondary ms-2">任意</span>
+            </div>
+            <Input
+              type="textarea"
+              placeholder="内容や用途のメモを表示させることができます"
+              rows="4"
+            />
+            <small className="text-secondary d-block mt-2">
+              メモの内容はアシスタントの処理に影響しません。
+            </small>
+          </div>
+
+          <div>
+            <button
+              type="button"
+              className="btn w-100 d-flex justify-content-between align-items-center py-3 mb-2 border-0 rounded text-start"
+            >
+              <span className="fw-normal">アシスタントの共有</span>
+              <div className="d-flex align-items-center text-secondary">
+                <span>UserNameのみ</span>
+                <span className="material-symbols-outlined ms-2 align-middle">chevron_right</span>
+              </div>
+            </button>
+
+            <button
+              type="button"
+              className="btn w-100 d-flex justify-content-between align-items-center py-3 mb-2 border-0 rounded text-start"
+            >
+              <span className="fw-normal">参照ページ</span>
+              <div className="d-flex align-items-center text-secondary">
+                <span>3ページ</span>
+                <span className="material-symbols-outlined ms-2 align-middle">chevron_right</span>
+              </div>
+            </button>
+
+            <button
+              type="button"
+              onClick={() => { changePageMode(AiAssistantManegementModalPageMode.INSTRUCTION) }}
+              className="btn w-100 d-flex justify-content-between align-items-center py-3 mb-2 border-0 rounded text-start"
+            >
+              <span className="fw-normal">アシスタントへの指示</span>
+              <div className="d-flex align-items-center text-secondary">
+                <span className="text-truncate" style={{ maxWidth: '280px' }}>
+                  {instruction}
+                </span>
+                <span className="material-symbols-outlined ms-2 align-middle">chevron_right</span>
+              </div>
+            </button>
+          </div>
+        </ModalBody>
+
+        <ModalFooter>
+          <button type="button" className="btn btn-outline-secondary" onClick={() => {}}>キャンセル</button>
+          <button type="button" className="btn btn-primary" onClick={() => {}}>アシスタントを作成する</button>
+        </ModalFooter>
+      </div>
+    </>
+  );
+};

+ 143 - 117
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManegementModal.tsx

@@ -1,9 +1,7 @@
 import React, { useCallback, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input,
-} from 'reactstrap';
+import { Modal } from 'reactstrap';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { IPageForItem } from '~/interfaces/page';
@@ -12,9 +10,11 @@ import loggerFactory from '~/utils/logger';
 
 import type { SelectedPage } from '../../../interfaces/selected-page';
 import { createAiAssistant } from '../../services/ai-assistant';
-import { useAiAssistantManegementModal } from '../../stores/ai-assistant';
+import { useAiAssistantManegementModal, AiAssistantManegementModalPageMode } from '../../stores/ai-assistant';
 import { SelectedPageList } from '../Common/SelectedPageList';
 
+import { AiAssistantManagementEditInstruction } from './AiAssistantManagementModal/AiAssistantManagementEditInstruction';
+import { AiAssistantManagementHome } from './AiAssistantManagementModal/AiAssistantManagementHome';
 
 import styles from './AiAssistantManegementModal.module.scss';
 
@@ -23,9 +23,18 @@ const moduleClass = styles['grw-ai-assistant-manegement'] ?? '';
 const logger = loggerFactory('growi:openai:client:components:AiAssistantManegementModal');
 
 const AiAssistantManegementModalSubstance = (): JSX.Element => {
+  // Hooks
+  const { t } = useTranslation();
   const { open: openPageSelectModal } = usePageSelectModal();
+  const { data: aiAssistantManegementModalData } = useAiAssistantManegementModal();
+
+  const pageMode = aiAssistantManegementModalData?.pageMode ?? AiAssistantManegementModalPageMode.HOME;
+
+  // States
   const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
+  const [instruction, setInstruction] = useState<string>(t('modal_ai_assistant.default_instruction'));
 
+  // Functions
   const clickOpenPageSelectModalHandler = useCallback(() => {
     const onSelected = (page: IPageForItem, isIncludeSubPage: boolean) => {
       const selectedPageIds = selectedPages.map(selectedPage => selectedPage.page._id);
@@ -37,7 +46,6 @@ const AiAssistantManegementModalSubstance = (): JSX.Element => {
     openPageSelectModal({ onSelected, isHierarchicalSelectionMode: true });
   }, [openPageSelectModal, selectedPages]);
 
-
   const clickRmoveSelectedPageHandler = useCallback((pageId: string) => {
     setSelectedPages(selectedPages.filter(selectedPage => selectedPage.page._id !== pageId));
   }, [selectedPages]);
@@ -51,7 +59,7 @@ const AiAssistantManegementModalSubstance = (): JSX.Element => {
       await createAiAssistant({
         name: 'test',
         description: 'test',
-        additionalInstruction: 'test',
+        additionalInstruction: instruction,
         pagePathPatterns,
         shareScope: 'publicOnly',
         accessScope: 'publicOnly',
@@ -62,133 +70,151 @@ const AiAssistantManegementModalSubstance = (): JSX.Element => {
       toastError('アシスタントの作成に失敗しました');
       logger.error(err);
     }
-  }, [selectedPages]);
+  }, [instruction, selectedPages]);
+
+
+  /*
+  *  For AiAssistantManagementEditInstruction methods
+  */
+  const changeInstructionHandler = useCallback((value: string) => {
+    setInstruction(value);
+  }, []);
+
+  const resetInstructionHandler = useCallback(() => {
+    setInstruction(t('modal_ai_assistant.default_instruction'));
+  }, [t]);
 
   return (
-    <div className="px-4">
-      <ModalBody>
-        <Form>
-          <FormGroup className="mb-4">
-            <Label className="mb-2 ">アシスタント名</Label>
-            <Input
-              type="text"
-              placeholder="アシスタント名を入力"
-              className="border rounded"
-            />
-          </FormGroup>
-
-          <FormGroup className="mb-4">
-            <div className="d-flex align-items-center mb-2">
-              <Label className="mb-0">アシスタントの種類</Label>
-              <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
-            </div>
-            <div className="d-flex gap-4">
-              <FormGroup check>
-                <Input type="checkbox" defaultChecked />
-                <Label check>ナレッジアシスタント</Label>
-              </FormGroup>
-              <FormGroup check>
-                <Input type="checkbox" />
-                <Label check>エディタアシスタント</Label>
-              </FormGroup>
-              <FormGroup check>
-                <Input type="checkbox" />
-                <Label check>ラーニングアシスタント</Label>
-              </FormGroup>
-            </div>
-          </FormGroup>
-
-          <FormGroup className="mb-4">
-            <div className="d-flex align-items-center mb-2">
-              <Label className="mb-0">共有範囲</Label>
-              <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
-            </div>
-            <Input type="select" className="border rounded w-50">
-              <option>自分のみ</option>
-            </Input>
-          </FormGroup>
-
-          <FormGroup className="mb-4">
-            <div className="d-flex align-items-center mb-2">
-              <Label className="mb-0">参照するページ</Label>
-              <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
-            </div>
-            <SelectedPageList selectedPages={selectedPages} onRemove={clickRmoveSelectedPageHandler} />
-            <button
-              type="button"
-              className="btn btn-outline-primary d-flex align-items-center gap-1"
-              onClick={clickOpenPageSelectModalHandler}
-            >
-              <span>+</span>
-              追加する
-            </button>
-          </FormGroup>
-
-          <FormGroup>
-            <div className="d-flex align-items-center mb-2">
-              <Label className="mb-0 me-2">アシスタントへの指示</Label>
-              <label className="form-label form-check-label">
-                <span className="badge text-bg-danger mt-2">
-                  必須
-                </span>
-              </label>
-            </div>
-            <Input
-              type="textarea"
-              placeholder="アシスタントに実行して欲しい内容を具体的に記入してください"
-              className="border rounded"
-              rows={4}
-            />
-          </FormGroup>
-
-          <FormGroup>
-            <div className="d-flex align-items-center mb-2">
-              <Label className="mb-0 me-2">アシスタントのメモ</Label>
-              <label className="form-label form-check-label">
-                <span className="badge text-bg-secondary mt-2">
-                  必須
-                </span>
-              </label>
-            </div>
-            <Input
-              type="textarea"
-              placeholder="内容や用途のメモを表示させることができます"
-              className="border rounded"
-              rows={4}
-            />
-            <p className="mt-1 text-muted">メモ内容はアシスタントには影響しません。</p>
-          </FormGroup>
-        </Form>
-      </ModalBody>
-
-      <ModalFooter className="border-0 pt-0 mb-3">
-        <button type="button" className="btn btn-outline-secondary" onClick={() => {}}>キャンセル</button>
-        <button type="button" className="btn btn-primary" onClick={clickCreateAiAssistantHandler}>作成</button>
-      </ModalFooter>
-    </div>
+    <>
+      {pageMode === AiAssistantManegementModalPageMode.HOME && (
+        <AiAssistantManagementHome
+          instruction={instruction}
+        />
+      )}
+
+      {pageMode === AiAssistantManegementModalPageMode.INSTRUCTION && (
+        <AiAssistantManagementEditInstruction
+          instruction={instruction}
+          onChange={changeInstructionHandler}
+          onReset={resetInstructionHandler}
+        />
+      )}
+    </>
+    // <div className="px-4">
+    //   <ModalBody>
+    //     <Form>
+    //       <FormGroup className="mb-4">
+    //         <Label className="mb-2 ">アシスタント名</Label>
+    //         <Input
+    //           type="text"
+    //           placeholder="アシスタント名を入力"
+    //           className="border rounded"
+    //         />
+    //       </FormGroup>
+
+  //       <FormGroup className="mb-4">
+  //         <div className="d-flex align-items-center mb-2">
+  //           <Label className="mb-0">アシスタントの種類</Label>
+  //           <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
+  //         </div>
+  //         <div className="d-flex gap-4">
+  //           <FormGroup check>
+  //             <Input type="checkbox" defaultChecked />
+  //             <Label check>ナレッジアシスタント</Label>
+  //           </FormGroup>
+  //           <FormGroup check>
+  //             <Input type="checkbox" />
+  //             <Label check>エディタアシスタント</Label>
+  //           </FormGroup>
+  //           <FormGroup check>
+  //             <Input type="checkbox" />
+  //             <Label check>ラーニングアシスタント</Label>
+  //           </FormGroup>
+  //         </div>
+  //       </FormGroup>
+
+  //       <FormGroup className="mb-4">
+  //         <div className="d-flex align-items-center mb-2">
+  //           <Label className="mb-0">共有範囲</Label>
+  //           <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
+  //         </div>
+  //         <Input type="select" className="border rounded w-50">
+  //           <option>自分のみ</option>
+  //         </Input>
+  //       </FormGroup>
+
+  //       <FormGroup className="mb-4">
+  //         <div className="d-flex align-items-center mb-2">
+  //           <Label className="mb-0">参照するページ</Label>
+  //           <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
+  //         </div>
+  //         <SelectedPageList selectedPages={selectedPages} onRemove={clickRmoveSelectedPageHandler} />
+  //         <button
+  //           type="button"
+  //           className="btn btn-outline-primary d-flex align-items-center gap-1"
+  //           onClick={clickOpenPageSelectModalHandler}
+  //         >
+  //           <span>+</span>
+  //           追加する
+  //         </button>
+  //       </FormGroup>
+
+  //       <FormGroup>
+  //         <div className="d-flex align-items-center mb-2">
+  //           <Label className="mb-0 me-2">アシスタントへの指示</Label>
+  //           <label className="form-label form-check-label">
+  //             <span className="badge text-bg-danger mt-2">
+  //               必須
+  //             </span>
+  //           </label>
+  //         </div>
+  //         <Input
+  //           type="textarea"
+  //           placeholder="アシスタントに実行して欲しい内容を具体的に記入してください"
+  //           className="border rounded"
+  //           rows={4}
+  //         />
+  //       </FormGroup>
+
+  //       <FormGroup>
+  //         <div className="d-flex align-items-center mb-2">
+  //           <Label className="mb-0 me-2">アシスタントのメモ</Label>
+  //           <label className="form-label form-check-label">
+  //             <span className="badge text-bg-secondary mt-2">
+  //               必須
+  //             </span>
+  //           </label>
+  //         </div>
+  //         <Input
+  //           type="textarea"
+  //           placeholder="内容や用途のメモを表示させることができます"
+  //           className="border rounded"
+  //           rows={4}
+  //         />
+  //         <p className="mt-1 text-muted">メモ内容はアシスタントには影響しません。</p>
+  //       </FormGroup>
+  //     </Form>
+  //   </ModalBody>
+
+  //   <ModalFooter className="border-0 pt-0 mb-3">
+  //     <button type="button" className="btn btn-outline-secondary" onClick={() => {}}>キャンセル</button>
+  //     <button type="button" className="btn btn-primary" onClick={clickCreateAiAssistantHandler}>作成</button>
+  //   </ModalFooter>
+  // </div>
   );
 };
 
 
 export const AiAssistantManegementModal = (): JSX.Element => {
-  const { t } = useTranslation();
-
   const { data: aiAssistantManegementModalData, close: closeAiAssistantManegementModal } = useAiAssistantManegementModal();
 
   const isOpened = aiAssistantManegementModalData?.isOpened ?? false;
 
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeAiAssistantManegementModal} className={moduleClass} scrollable>
-
-      <ModalHeader tag="h4" toggle={closeAiAssistantManegementModal} className="pe-4">
-        <span className="growi-custom-icons growi-ai-assistant-icon me-3 fs-4">ai_assistant</span>
-        <span className="fw-bold">新規アシスタントの追加</span> {/* TODO i18n */}
-      </ModalHeader>
-
       { isOpened && (
         <AiAssistantManegementModalSubstance />
       ) }
-
     </Modal>
   );
 };

+ 13 - 4
apps/app/src/features/openai/client/stores/ai-assistant.tsx

@@ -3,27 +3,36 @@ import { useCallback } from 'react';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import type { SWRResponse } from 'swr';
 
+export const AiAssistantManegementModalPageMode = {
+  HOME: 'home',
+  INSTRUCTION: 'instruction',
+} as const;
+
+type AiAssistantManegementModalPageMode = typeof AiAssistantManegementModalPageMode[keyof typeof AiAssistantManegementModalPageMode];
 
 type AiAssistantManegementModalStatus = {
   isOpened: boolean,
+  pageMode?: AiAssistantManegementModalPageMode,
 }
 
 type AiAssistantManegementModalUtils = {
   open(): void
   close(): void
+  changePageMode(pageType: AiAssistantManegementModalPageMode): void
 }
 
 export const useAiAssistantManegementModal = (
     status?: AiAssistantManegementModalStatus,
 ): SWRResponse<AiAssistantManegementModalStatus, Error> & AiAssistantManegementModalUtils => {
-  const initialStatus = { isOpened: false };
+  const initialStatus = { isOpened: false, pageType: AiAssistantManegementModalPageMode.HOME };
   const swrResponse = useSWRStatic<AiAssistantManegementModalStatus, Error>('AiAssistantManegementModal', status, { fallbackData: initialStatus });
 
   return {
     ...swrResponse,
-    open: useCallback(() => {
-      swrResponse.mutate({ isOpened: true });
-    }, [swrResponse]),
+    open: useCallback(() => { swrResponse.mutate({ isOpened: true }) }, [swrResponse]),
     close: useCallback(() => swrResponse.mutate({ isOpened: false }), [swrResponse]),
+    changePageMode: useCallback((pageMode: AiAssistantManegementModalPageMode) => {
+      swrResponse.mutate({ isOpened: swrResponse.data?.isOpened ?? false, pageMode });
+    }, [swrResponse]),
   };
 };