Просмотр исходного кода

Merge pull request #9598 from weseek/feat/160864-redesign-ai-assistant-management-modal

feat: Redesign AiAssistantManagementModal
Shun Miyazawa 1 год назад
Родитель
Сommit
3ce6b8fee5
16 измененных файлов с 537 добавлено и 230 удалено
  1. 8 1
      apps/app/public/static/locales/en_US/translation.json
  2. 8 0
      apps/app/public/static/locales/fr_FR/translation.json
  3. 8 0
      apps/app/public/static/locales/ja_JP/translation.json
  4. 8 0
      apps/app/public/static/locales/zh_CN/translation.json
  5. 4 4
      apps/app/src/components/Layout/BasicLayout.tsx
  6. 39 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditInstruction.tsx
  7. 52 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditPages.tsx
  8. 26 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHeader.tsx
  9. 101 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx
  10. 15 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.module.scss
  11. 228 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx
  12. 0 8
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManegementModal.module.scss
  13. 0 194
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManegementModal.tsx
  14. 2 2
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantSubstance.tsx
  15. 17 11
      apps/app/src/features/openai/client/components/Common/SelectedPageList.tsx
  16. 21 10
      apps/app/src/features/openai/client/stores/ai-assistant.tsx

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

@@ -501,7 +501,14 @@
     "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.",
+    "page_mode_title": {
+      "share": "Assistant Sharing",
+      "pages": "Reference Pages",
+      "instruction": "Assistant Instructions"
+    }
   },
   "link_edit": {
     "edit_link": "Edit Link",

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

@@ -497,6 +497,14 @@
     "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.",
+    "page_mode_title": {
+      "share": "Partage de l'assistant",
+      "pages": "Pages de référence",
+      "instruction": "Instructions de l'assistant"
+    }
+  },
   "link_edit": {
     "edit_link": "Modifier lien",
     "set_link_and_label": "Ajouter lien et étiquette",

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

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

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

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

+ 4 - 4
apps/app/src/components/Layout/BasicLayout.tsx

@@ -35,9 +35,9 @@ const DeleteBookmarkFolderModal = dynamic(
 );
 const SearchModal = dynamic(() => import('../../features/search/client/components/SearchModal'), { ssr: false });
 const AiChatModal = dynamic(() => import('~/features/openai/chat/components/AiChatModal').then(mod => mod.AiChatModal), { ssr: false });
-const AiAssistantManegementModal = dynamic(
-  () => import('~/features/openai/client/components/AiAssistant/AiAssistantManegementModal')
-    .then(mod => mod.AiAssistantManegementModal), { ssr: false },
+const AiAssistantManagementModal = dynamic(
+  () => import('~/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal')
+    .then(mod => mod.AiAssistantManagementModal), { ssr: false },
 );
 const PageSelectModal = dynamic(() => import('~/client/components/PageSelectModal/PageSelectModal').then(mod => mod.PageSelectModal), { ssr: false });
 
@@ -74,7 +74,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
       <PageSelectModal />
       <SearchModal />
       <AiChatModal />
-      <AiAssistantManegementModal />
+      <AiAssistantManagementModal />
 
       <PagePresentationModal />
       <HotkeysManager />

+ 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>
+    </>
+  );
+};

+ 52 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditPages.tsx

@@ -0,0 +1,52 @@
+import React, { useCallback } from 'react';
+
+import { ModalBody } from 'reactstrap';
+
+import type { IPageForItem } from '~/interfaces/page';
+import { usePageSelectModal } from '~/stores/modal';
+
+import type { SelectedPage } from '../../../../interfaces/selected-page';
+import { SelectedPageList } from '../../Common/SelectedPageList';
+
+import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
+
+
+type Props = {
+  selectedPages: SelectedPage[];
+  onSelect: (page: IPageForItem, isIncludeSubPage: boolean) => void;
+  onRemove: (pageId: string) => void;
+}
+
+export const AiAssistantManagementEditPages = (props: Props): JSX.Element => {
+  const { selectedPages, onSelect, onRemove } = props;
+
+  const { open: openPageSelectModal } = usePageSelectModal();
+
+  const clickOpenPageSelectModalHandler = useCallback(() => {
+    openPageSelectModal({ onSelected: onSelect, isHierarchicalSelectionMode: true });
+  }, [onSelect, openPageSelectModal]);
+
+  return (
+    <>
+      <AiAssistantManagementHeader />
+
+      <ModalBody className="px-4">
+        <p className="text-secondary py-1">
+          アシスタントが参照するページを編集します。<br />
+          参照できるページは配下ページも含めて200ページまでです。
+        </p>
+
+        <button
+          type="button"
+          onClick={clickOpenPageSelectModalHandler}
+          className="btn btn-outline-primary w-100 mb-3 d-flex align-items-center justify-content-center"
+        >
+          <span className="material-symbols-outlined me-2">add</span>
+          ページを追加する
+        </button>
+
+        <SelectedPageList selectedPages={selectedPages} onRemove={onRemove} />
+      </ModalBody>
+    </>
+  );
+};

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

@@ -0,0 +1,26 @@
+import { useTranslation } from 'react-i18next';
+import { ModalHeader } from 'reactstrap';
+
+import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
+
+export const AiAssistantManagementHeader = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { data, close, changePageMode } = useAiAssistantManagementModal();
+
+  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(AiAssistantManagementModalPageMode.HOME)}>
+          <span className="material-symbols-outlined text-primary">chevron_left</span>
+        </button>
+        <span>{t(`modal_ai_assistant.page_mode_title.${data?.pageMode}`)}</span>
+      </div>
+    </ModalHeader>
+  );
+};

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

@@ -0,0 +1,101 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+import {
+  ModalHeader, ModalBody, ModalFooter, Input,
+} from 'reactstrap';
+
+import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
+
+type Props = {
+  instruction: string;
+}
+
+export const AiAssistantManagementHome = (props: Props): JSX.Element => {
+  const { instruction } = props;
+
+  const { t } = useTranslation();
+
+  const { close: closeAiAssistantManagementModal, changePageMode } = useAiAssistantManagementModal();
+
+  return (
+    <>
+      <ModalHeader tag="h4" toggle={closeAiAssistantManagementModal} 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 growi-ai-assistant-name">
+            <Input
+              type="text"
+              placeholder="アシスタント名を入力"
+              bsSize="lg"
+              className="border-0 border-bottom border-2 px-0 rounded-0"
+            />
+          </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"
+            >
+              <span className="fw-normal">{t('modal_ai_assistant.page_mode_title.share')}</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"
+              onClick={() => { changePageMode(AiAssistantManagementModalPageMode.PAGES) }}
+              className="btn w-100 d-flex justify-content-between align-items-center py-3 mb-2 border-0"
+            >
+              <span className="fw-normal">{t('modal_ai_assistant.page_mode_title.pages')}</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(AiAssistantManagementModalPageMode.INSTRUCTION) }}
+              className="btn w-100 d-flex justify-content-between align-items-center py-3 mb-2 border-0"
+            >
+              <span className="fw-normal">{t('modal_ai_assistant.page_mode_title.instruction')}</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>
+    </>
+  );
+};

+ 15 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.module.scss

@@ -0,0 +1,15 @@
+@use '@growi/core-styles/scss/variables/growi-official-colors';
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+// == Colors
+.grw-ai-assistant-management :global {
+  .growi-ai-assistant-icon {
+    color: growi-official-colors.$growi-ai-purple;
+  }
+  .growi-ai-assistant-name {
+    .form-control:focus {
+      border-color: var(--bs-primary) !important;
+      box-shadow: none;
+    }
+  }
+}

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

@@ -0,0 +1,228 @@
+import React, { useCallback, useState } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { Modal, TabContent, TabPane } from 'reactstrap';
+
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import type { IPageForItem } from '~/interfaces/page';
+import loggerFactory from '~/utils/logger';
+
+import type { SelectedPage } from '../../../../interfaces/selected-page';
+import { createAiAssistant } from '../../../services/ai-assistant';
+import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
+
+import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
+import { AiAssistantManagementEditPages } from './AiAssistantManagementEditPages';
+import { AiAssistantManagementHome } from './AiAssistantManagementHome';
+
+import styles from './AiAssistantManagementModal.module.scss';
+
+const moduleClass = styles['grw-ai-assistant-management'] ?? '';
+
+const logger = loggerFactory('growi:openai:client:components:AiAssistantManagementModal');
+
+const AiAssistantManagementModalSubstance = (): JSX.Element => {
+  // Hooks
+  const { t } = useTranslation();
+  const { data: aiAssistantManagementModalData } = useAiAssistantManagementModal();
+
+  const pageMode = aiAssistantManagementModalData?.pageMode ?? AiAssistantManagementModalPageMode.HOME;
+
+  // States
+  const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
+  const [instruction, setInstruction] = useState<string>(t('modal_ai_assistant.default_instruction'));
+
+  // Functions
+  const clickCreateAiAssistantHandler = useCallback(async() => {
+    try {
+      const pagePathPatterns = selectedPages
+        .map(selectedPage => (selectedPage.isIncludeSubPage ? `${selectedPage.page.path}/*` : selectedPage.page.path))
+        .filter((path): path is string => path !== undefined && path !== null);
+
+      await createAiAssistant({
+        name: 'test',
+        description: 'test',
+        additionalInstruction: instruction,
+        pagePathPatterns,
+        shareScope: 'publicOnly',
+        accessScope: 'publicOnly',
+      });
+      toastSuccess('アシスタントを作成しました');
+    }
+    catch (err) {
+      toastError('アシスタントの作成に失敗しました');
+      logger.error(err);
+    }
+  }, [instruction, selectedPages]);
+
+
+  /*
+  *  For AiAssistantManagementEditPages methods
+  */
+  const selectPageHandler = useCallback((page: IPageForItem, isIncludeSubPage: boolean) => {
+    const selectedPageIds = selectedPages.map(selectedPage => selectedPage.page._id);
+    if (page._id != null && !selectedPageIds.includes(page._id)) {
+      setSelectedPages([...selectedPages, { page, isIncludeSubPage }]);
+    }
+  }, [selectedPages]);
+
+  const removePageHandler = useCallback((pageId: string) => {
+    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.page._id !== pageId));
+  }, [selectedPages]);
+
+
+  /*
+  *  For AiAssistantManagementEditInstruction methods
+  */
+  const changeInstructionHandler = useCallback((value: string) => {
+    setInstruction(value);
+  }, []);
+
+  const resetInstructionHandler = useCallback(() => {
+    setInstruction(t('modal_ai_assistant.default_instruction'));
+  }, [t]);
+
+  return (
+    <>
+      <TabContent activeTab={pageMode}>
+        <TabPane tabId={AiAssistantManagementModalPageMode.HOME}>
+          <AiAssistantManagementHome
+            instruction={instruction}
+          />
+        </TabPane>
+
+        <TabPane tabId={AiAssistantManagementModalPageMode.PAGES}>
+          <AiAssistantManagementEditPages
+            selectedPages={selectedPages}
+            onSelect={selectPageHandler}
+            onRemove={removePageHandler}
+          />
+        </TabPane>
+
+        <TabPane tabId={AiAssistantManagementModalPageMode.INSTRUCTION}>
+          <AiAssistantManagementEditInstruction
+            instruction={instruction}
+            onChange={changeInstructionHandler}
+            onReset={resetInstructionHandler}
+          />
+        </TabPane>
+      </TabContent>
+    </>
+    // <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 AiAssistantManagementModal = (): JSX.Element => {
+  const { data: aiAssistantManagementModalData, close: closeAiAssistantManagementModal } = useAiAssistantManagementModal();
+
+  const isOpened = aiAssistantManagementModalData?.isOpened ?? false;
+
+  return (
+    <Modal size="lg" isOpen={isOpened} toggle={closeAiAssistantManagementModal} className={moduleClass} scrollable>
+      { isOpened && (
+        <AiAssistantManagementModalSubstance />
+      ) }
+    </Modal>
+  );
+};

+ 0 - 8
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManegementModal.module.scss

@@ -1,8 +0,0 @@
-@use '@growi/core-styles/scss/variables/growi-official-colors';
-
-// == Colors
-.grw-ai-assistant-manegement :global {
-  .growi-ai-assistant-icon {
-    color: growi-official-colors.$growi-ai-purple;
-  }
-}

+ 0 - 194
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManegementModal.tsx

@@ -1,194 +0,0 @@
-import React, { useCallback, useState } from 'react';
-
-import { useTranslation } from 'react-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input,
-} from 'reactstrap';
-
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import type { IPageForItem } from '~/interfaces/page';
-import { usePageSelectModal } from '~/stores/modal';
-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 { SelectedPageList } from '../Common/SelectedPageList';
-
-
-import styles from './AiAssistantManegementModal.module.scss';
-
-const moduleClass = styles['grw-ai-assistant-manegement'] ?? '';
-
-const logger = loggerFactory('growi:openai:client:components:AiAssistantManegementModal');
-
-const AiAssistantManegementModalSubstance = (): JSX.Element => {
-  const { open: openPageSelectModal } = usePageSelectModal();
-  const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
-
-  const clickOpenPageSelectModalHandler = useCallback(() => {
-    const onSelected = (page: IPageForItem, isIncludeSubPage: boolean) => {
-      const selectedPageIds = selectedPages.map(selectedPage => selectedPage.page._id);
-      if (page._id != null && !selectedPageIds.includes(page._id)) {
-        setSelectedPages([...selectedPages, { page, isIncludeSubPage }]);
-      }
-    };
-
-    openPageSelectModal({ onSelected, isHierarchicalSelectionMode: true });
-  }, [openPageSelectModal, selectedPages]);
-
-
-  const clickRmoveSelectedPageHandler = useCallback((pageId: string) => {
-    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.page._id !== pageId));
-  }, [selectedPages]);
-
-  const clickCreateAiAssistantHandler = useCallback(async() => {
-    try {
-      const pagePathPatterns = selectedPages
-        .map(selectedPage => (selectedPage.isIncludeSubPage ? `${selectedPage.page.path}/*` : selectedPage.page.path))
-        .filter((path): path is string => path !== undefined && path !== null);
-
-      await createAiAssistant({
-        name: 'test',
-        description: 'test',
-        additionalInstruction: 'test',
-        pagePathPatterns,
-        shareScope: 'publicOnly',
-        accessScope: 'publicOnly',
-      });
-      toastSuccess('アシスタントを作成しました');
-    }
-    catch (err) {
-      toastError('アシスタントの作成に失敗しました');
-      logger.error(err);
-    }
-  }, [selectedPages]);
-
-  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>
-  );
-};
-
-
-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>
-  );
-};

+ 2 - 2
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantSubstance.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
 
-import { useAiAssistantManegementModal } from '../../../stores/ai-assistant';
+import { useAiAssistantManagementModal } from '../../../stores/ai-assistant';
 
 export const AiAssistantContent = (): JSX.Element => {
-  const { open } = useAiAssistantManegementModal();
+  const { open } = useAiAssistantManagementModal();
 
   return (
     <div>

+ 17 - 11
apps/app/src/features/openai/client/components/Common/SelectedPageList.tsx

@@ -1,8 +1,5 @@
-import type { FC } from 'react';
 import { memo } from 'react';
 
-import { useTranslation } from 'react-i18next';
-
 import type { SelectedPage } from '../../../interfaces/selected-page';
 
 type SelectedPageListProps = {
@@ -10,9 +7,7 @@ type SelectedPageListProps = {
   onRemove?: (pageId?: string) => void;
 };
 
-const SelectedPageListBase: FC<SelectedPageListProps> = ({ selectedPages, onRemove }) => {
-  const { t } = useTranslation();
-
+const SelectedPageListBase: React.FC<SelectedPageListProps> = ({ selectedPages, onRemove }: SelectedPageListProps) => {
   if (selectedPages.length === 0) {
     return <></>;
   }
@@ -20,12 +15,23 @@ const SelectedPageListBase: FC<SelectedPageListProps> = ({ selectedPages, onRemo
   return (
     <div className="mb-3">
       {selectedPages.map(({ page, isIncludeSubPage }) => (
-        <div key={page._id} className="mb-1 d-flex align-items-center">
-          <code>{ page.path }</code>
-          {isIncludeSubPage && <span className="badge rounded-pill text-bg-secondary ms-2">{t('Include Subordinated Page')}</span>}
+        <div
+          key={page._id}
+          className="mb-2 d-flex justify-content-between align-items-center bg-light rounded py-2 px-3"
+        >
+          <div className="d-flex align-items-center overflow-hidden">
+            { isIncludeSubPage
+              ? <>{`${page.path}/*`}</>
+              : <>{page.path}</>
+            }
+          </div>
           {onRemove != null && page._id != null && page._id && (
-            <button className="btn border-0 " type="button" onClick={() => onRemove(page._id)}>
-              <span className="fs-5 material-symbols-outlined text-secondary">delete</span>
+            <button
+              type="button"
+              className="btn p-0 ms-3 text-secondary"
+              onClick={() => onRemove(page._id)}
+            >
+              <span className="material-symbols-outlined fs-4">delete</span>
             </button>
           )}
         </div>

+ 21 - 10
apps/app/src/features/openai/client/stores/ai-assistant.tsx

@@ -3,27 +3,38 @@ import { useCallback } from 'react';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import type { SWRResponse } from 'swr';
 
+export const AiAssistantManagementModalPageMode = {
+  HOME: 'home',
+  SHARE: 'share',
+  PAGES: 'pages',
+  INSTRUCTION: 'instruction',
+} as const;
 
-type AiAssistantManegementModalStatus = {
+type AiAssistantManagementModalPageMode = typeof AiAssistantManagementModalPageMode[keyof typeof AiAssistantManagementModalPageMode];
+
+type AiAssistantManagementModalStatus = {
   isOpened: boolean,
+  pageMode?: AiAssistantManagementModalPageMode,
 }
 
-type AiAssistantManegementModalUtils = {
+type AiAssistantManagementModalUtils = {
   open(): void
   close(): void
+  changePageMode(pageType: AiAssistantManagementModalPageMode): void
 }
 
-export const useAiAssistantManegementModal = (
-    status?: AiAssistantManegementModalStatus,
-): SWRResponse<AiAssistantManegementModalStatus, Error> & AiAssistantManegementModalUtils => {
-  const initialStatus = { isOpened: false };
-  const swrResponse = useSWRStatic<AiAssistantManegementModalStatus, Error>('AiAssistantManegementModal', status, { fallbackData: initialStatus });
+export const useAiAssistantManagementModal = (
+    status?: AiAssistantManagementModalStatus,
+): SWRResponse<AiAssistantManagementModalStatus, Error> & AiAssistantManagementModalUtils => {
+  const initialStatus = { isOpened: false, pageType: AiAssistantManagementModalPageMode.HOME };
+  const swrResponse = useSWRStatic<AiAssistantManagementModalStatus, Error>('AiAssistantManagementModal', 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: AiAssistantManagementModalPageMode) => {
+      swrResponse.mutate({ isOpened: swrResponse.data?.isOpened ?? false, pageMode });
+    }, [swrResponse]),
   };
 };