Răsfoiți Sursa

Merge pull request #10215 from weseek/feat/169928-redesign-of-referenced-pages

feat: Redesign of referenced pages
Yuki Takei 8 luni în urmă
părinte
comite
98a2148c3f
17 a modificat fișierele cu 343 adăugiri și 296 ștergeri
  1. 26 29
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditPages.tsx
  2. 3 5
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx
  3. 1 1
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.module.scss
  4. 24 17
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.tsx
  5. 16 39
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx
  6. 0 20
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageSelectionMethod.module.scss
  7. 8 44
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageSelectionMethod.tsx
  8. 18 23
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageTreeSelection.tsx
  9. 28 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/PageSelectionMethodButtons.module.scss
  10. 55 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/PageSelectionMethodButtons.tsx
  11. 95 28
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectablePagePageList.tsx
  12. 0 43
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectedPageList.tsx
  13. 4 4
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeWarningModal.tsx
  14. 0 35
      apps/app/src/features/openai/client/services/use-selected-pages.ts
  15. 55 0
      apps/app/src/features/openai/client/services/use-selected-pages.tsx
  16. 10 0
      apps/app/src/features/openai/interfaces/selectable-page.ts
  17. 0 8
      apps/app/src/features/openai/interfaces/selected-page.ts

+ 26 - 29
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditPages.tsx

@@ -3,19 +3,16 @@ import React, { useCallback, type JSX } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { ModalBody } from 'reactstrap';
 import { ModalBody } from 'reactstrap';
 
 
-import type { IPageForItem } from '~/interfaces/page';
 import { useLimitLearnablePageCountPerAssistant } from '~/stores-universal/context';
 import { useLimitLearnablePageCountPerAssistant } from '~/stores-universal/context';
-import { usePageSelectModal } from '~/stores/modal';
 
 
-import type { SelectedPage } from '../../../../interfaces/selected-page';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 
 
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
-import { SelectedPageList } from './SelectedPageList';
-
+import { PageSelectionMethodButtons } from './PageSelectionMethodButtons';
+import { SelectablePagePageList } from './SelectablePagePageList';
 
 
 type Props = {
 type Props = {
-  selectedPages: SelectedPage[];
-  onSelect: (page: IPageForItem, isIncludeSubPage: boolean) => void;
+  selectedPages: SelectablePage[];
   onRemove: (pageId: string) => void;
   onRemove: (pageId: string) => void;
 }
 }
 
 
@@ -23,35 +20,35 @@ export const AiAssistantManagementEditPages = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: limitLearnablePageCountPerAssistant } = useLimitLearnablePageCountPerAssistant();
   const { data: limitLearnablePageCountPerAssistant } = useLimitLearnablePageCountPerAssistant();
 
 
-  const { selectedPages, onSelect, onRemove } = props;
-
-  const { open: openPageSelectModal } = usePageSelectModal();
+  const { selectedPages, onRemove } = props;
 
 
-  const clickOpenPageSelectModalHandler = useCallback(() => {
-    openPageSelectModal({ onSelected: onSelect, isHierarchicalSelectionMode: true });
-  }, [onSelect, openPageSelectModal]);
+  const removePageHandler = useCallback((page: SelectablePage) => {
+    onRemove(page.path);
+  }, [onRemove]);
 
 
   return (
   return (
     <>
     <>
       <AiAssistantManagementHeader labelTranslationKey="modal_ai_assistant.page_mode_title.pages" />
       <AiAssistantManagementHeader labelTranslationKey="modal_ai_assistant.page_mode_title.pages" />
 
 
       <ModalBody className="px-4">
       <ModalBody className="px-4">
-        <p
-          className="text-secondary py-1"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('modal_ai_assistant.edit_page_description', { limitLearnablePageCountPerAssistant }) }}
-        />
-
-        <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>
-          {t('modal_ai_assistant.add_page_button')}
-        </button>
-
-        <SelectedPageList selectedPages={selectedPages} onRemove={onRemove} />
+        <div className="px-4">
+          <p
+            className="text-secondary"
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: t('modal_ai_assistant.edit_page_description', { limitLearnablePageCountPerAssistant }) }}
+          />
+
+          <div className="mb-3">
+            <PageSelectionMethodButtons />
+          </div>
+
+          <SelectablePagePageList
+            method="delete"
+            methodButtonPosition="right"
+            pages={selectedPages}
+            onClickMethodButton={removePageHandler}
+          />
+        </div>
       </ModalBody>
       </ModalBody>
     </>
     </>
   );
   );

+ 3 - 5
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx

@@ -11,7 +11,7 @@ import { AiAssistantShareScope, AiAssistantAccessScope } from '~/features/openai
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import { useCurrentUser, useLimitLearnablePageCountPerAssistant } from '~/stores-universal/context';
 import { useCurrentUser, useLimitLearnablePageCountPerAssistant } from '~/stores-universal/context';
 
 
-import type { SelectedPage } from '../../../../interfaces/selected-page';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 import { determineShareScope } from '../../../../utils/determine-share-scope';
 import { determineShareScope } from '../../../../utils/determine-share-scope';
 import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
 import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
 
 
@@ -25,7 +25,7 @@ type Props = {
   instruction: string;
   instruction: string;
   shareScope: AiAssistantShareScope,
   shareScope: AiAssistantShareScope,
   accessScope: AiAssistantAccessScope,
   accessScope: AiAssistantAccessScope,
-  selectedPages: SelectedPage[];
+  selectedPages: SelectablePage[];
   selectedUserGroupsForAccessScope: PopulatedGrantedGroup[],
   selectedUserGroupsForAccessScope: PopulatedGrantedGroup[],
   selectedUserGroupsForShareScope: PopulatedGrantedGroup[],
   selectedUserGroupsForShareScope: PopulatedGrantedGroup[],
   onNameChange: (value: string) => void;
   onNameChange: (value: string) => void;
@@ -58,9 +58,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
 
 
   const totalSelectedPageCount = useMemo(() => {
   const totalSelectedPageCount = useMemo(() => {
     return selectedPages.reduce((total, selectedPage) => {
     return selectedPages.reduce((total, selectedPage) => {
-      const descendantCount = selectedPage.isIncludeSubPage
-        ? selectedPage.page.descendantCount ?? 0
-        : 0;
+      const descendantCount = selectedPage.descendantCount ?? 0;
       const pageCountWithDescendants = descendantCount + 1;
       const pageCountWithDescendants = descendantCount + 1;
       return total + pageCountWithDescendants;
       return total + pageCountWithDescendants;
     }, 0);
     }, 0);

+ 1 - 1
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.module.scss

@@ -5,7 +5,7 @@
 .grw-ai-assistant-keyword-search :global {
 .grw-ai-assistant-keyword-search :global {
   .rbt {
   .rbt {
     .rbt-input-multi {
     .rbt-input-multi {
-      font-size: 1.4rem;
+      font-size: 1.2rem;
       border: none;
       border: none;
       border-bottom: 3px solid bs.$gray-200;
       border-bottom: 3px solid bs.$gray-200;
       border-radius: 0;
       border-radius: 0;

+ 24 - 17
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementKeywordSearch.tsx

@@ -12,6 +12,7 @@ import {
 
 
 import { useSWRxSearch } from '~/stores/search';
 import { useSWRxSearch } from '~/stores/search';
 
 
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 import { useSelectedPages } from '../../../services/use-selected-pages';
 import { useSelectedPages } from '../../../services/use-selected-pages';
 import {
 import {
   useAiAssistantManagementModal, AiAssistantManagementModalPageMode,
   useAiAssistantManagementModal, AiAssistantManagementModalPageMode,
@@ -34,11 +35,18 @@ const isSelectedSearchKeyword = (value: unknown): value is SelectedSearchKeyword
 };
 };
 
 
 
 
-export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (pages: IPageHasId[]) => void}): JSX.Element => {
-  const { updateBaseSelectedPages } = props;
+type Props = {
+  baseSelectedPages: SelectablePage[],
+  updateBaseSelectedPages: (pages: SelectablePage[]) => void;
+}
+
+export const AiAssistantKeywordSearch = (props: Props): JSX.Element => {
+  const { baseSelectedPages, updateBaseSelectedPages } = props;
 
 
   const [selectedSearchKeywords, setSelectedSearchKeywords] = useState<Array<SelectedSearchKeyword>>([]);
   const [selectedSearchKeywords, setSelectedSearchKeywords] = useState<Array<SelectedSearchKeyword>>([]);
-  const { selectedPages, addPageHandler, removePageHandler } = useSelectedPages();
+  const {
+    selectedPages, addPage, removePage,
+  } = useSelectedPages(baseSelectedPages);
 
 
   const joinedSelectedSearchKeywords = useMemo(() => {
   const joinedSelectedSearchKeywords = useMemo(() => {
     return selectedSearchKeywords.map(item => item.label).join(' ');
     return selectedSearchKeywords.map(item => item.label).join(' ');
@@ -60,17 +68,16 @@ export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (page
 
 
     const pages = searchResult.data.map(item => item.data);
     const pages = searchResult.data.map(item => item.data);
     return pages.map((page) => {
     return pages.map((page) => {
-      if (page.path === '/') {
-        page.path = '/*';
+      const newPage = { ...page };
+      if (newPage.path === '/') {
+        newPage.path = '/*';
+        return newPage;
       }
       }
-
-      if (!isGlobPatternPath(page.path)) {
-        page.path = `${page.path}/*`;
+      if (!isGlobPatternPath(newPage.path)) {
+        newPage.path = `${newPage.path}/*`;
       }
       }
-
-      return page;
+      return newPage;
     });
     });
-
   }, [searchResult]);
   }, [searchResult]);
 
 
   const shownSearchResult = useMemo(() => {
   const shownSearchResult = useMemo(() => {
@@ -125,14 +132,14 @@ export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (page
 
 
   const nextButtonClickHandler = useCallback(() => {
   const nextButtonClickHandler = useCallback(() => {
     updateBaseSelectedPages(Array.from(selectedPages.values()));
     updateBaseSelectedPages(Array.from(selectedPages.values()));
-    changePageMode(AiAssistantManagementModalPageMode.HOME);
-  }, [changePageMode, selectedPages, updateBaseSelectedPages]);
+    changePageMode(isNewAiAssistant ? AiAssistantManagementModalPageMode.HOME : AiAssistantManagementModalPageMode.PAGES);
+  }, [changePageMode, isNewAiAssistant, selectedPages, updateBaseSelectedPages]);
 
 
   return (
   return (
     <div className={moduleClass}>
     <div className={moduleClass}>
       <AiAssistantManagementHeader
       <AiAssistantManagementHeader
         backButtonColor="secondary"
         backButtonColor="secondary"
-        backToPageMode={AiAssistantManagementModalPageMode.PAGE_SELECTION_METHOD}
+        backToPageMode={baseSelectedPages.length === 0 ? AiAssistantManagementModalPageMode.PAGE_SELECTION_METHOD : AiAssistantManagementModalPageMode.PAGES}
         labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
         labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
       />
       />
 
 
@@ -168,8 +175,8 @@ export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (page
               <SelectablePagePageList
               <SelectablePagePageList
                 pages={pagesWithGlobPath ?? []}
                 pages={pagesWithGlobPath ?? []}
                 method="add"
                 method="add"
-                onClickMethodButton={addPageHandler}
-                disablePageIds={Array.from(selectedPages.keys())}
+                onClickMethodButton={addPage}
+                disablePagePaths={Array.from(selectedPages.values()).map(page => page.path)}
               />
               />
             </div>
             </div>
           </>
           </>
@@ -183,7 +190,7 @@ export const AiAssistantKeywordSearch = (props: { updateBaseSelectedPages: (page
           <SelectablePagePageList
           <SelectablePagePageList
             pages={Array.from(selectedPages.values())}
             pages={Array.from(selectedPages.values())}
             method="remove"
             method="remove"
-            onClickMethodButton={removePageHandler}
+            onClickMethodButton={removePage}
           />
           />
           <label className="form-text text-muted mt-2">
           <label className="form-text text-muted mt-2">
             {t('modal_ai_assistant.can_add_later')}
             {t('modal_ai_assistant.can_add_later')}

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

@@ -13,12 +13,12 @@ import { Modal, TabContent, TabPane } from 'reactstrap';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { UpsertAiAssistantData } from '~/features/openai/interfaces/ai-assistant';
 import type { UpsertAiAssistantData } from '~/features/openai/interfaces/ai-assistant';
 import { AiAssistantAccessScope, AiAssistantShareScope } from '~/features/openai/interfaces/ai-assistant';
 import { AiAssistantAccessScope, AiAssistantShareScope } from '~/features/openai/interfaces/ai-assistant';
-import type { IPagePathWithDescendantCount, IPageForItem } from '~/interfaces/page';
+import type { IPagePathWithDescendantCount } from '~/interfaces/page';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import { useSWRxPagePathsWithDescendantCount } from '~/stores/page';
 import { useSWRxPagePathsWithDescendantCount } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import type { SelectedPage } from '../../../../interfaces/selected-page';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 import { removeGlobPath } from '../../../../utils/remove-glob-path';
 import { removeGlobPath } from '../../../../utils/remove-glob-path';
 import { createAiAssistant, updateAiAssistant } from '../../../services/ai-assistant';
 import { createAiAssistant, updateAiAssistant } from '../../../services/ai-assistant';
 import {
 import {
@@ -56,14 +56,13 @@ const convertToPopulatedGrantedGroups = (selectedGroups: IGrantedGroup[]): Popul
   return populatedGrantedGroups;
   return populatedGrantedGroups;
 };
 };
 
 
-const convertToSelectedPages = (pagePathPatterns: string[], pagePathsWithDescendantCount: IPagePathWithDescendantCount[]): SelectedPage[] => {
+const convertToSelectedPages = (pagePathPatterns: string[], pagePathsWithDescendantCount: IPagePathWithDescendantCount[]): SelectablePage[] => {
   return pagePathPatterns.map((pagePathPattern) => {
   return pagePathPatterns.map((pagePathPattern) => {
-    const isIncludeSubPage = pagePathPattern.endsWith('/*');
-    const path = isIncludeSubPage ? pagePathPattern.slice(0, -2) : pagePathPattern;
-    const page = pagePathsWithDescendantCount.find(page => page.path === path);
+    const pathWithoutGlob = isGlobPatternPath(pagePathPattern) ? pagePathPattern.slice(0, -2) : pagePathPattern;
+    const page = pagePathsWithDescendantCount.find(p => p.path === pathWithoutGlob);
     return {
     return {
-      page: page ?? { path },
-      isIncludeSubPage,
+      ...page,
+      path: pagePathPattern,
     };
     };
   });
   });
 };
 };
@@ -93,7 +92,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   const [selectedAccessScope, setSelectedAccessScope] = useState<AiAssistantAccessScope>(AiAssistantAccessScope.OWNER);
   const [selectedAccessScope, setSelectedAccessScope] = useState<AiAssistantAccessScope>(AiAssistantAccessScope.OWNER);
   const [selectedUserGroupsForAccessScope, setSelectedUserGroupsForAccessScope] = useState<PopulatedGrantedGroup[]>([]);
   const [selectedUserGroupsForAccessScope, setSelectedUserGroupsForAccessScope] = useState<PopulatedGrantedGroup[]>([]);
   const [selectedUserGroupsForShareScope, setSelectedUserGroupsForShareScope] = useState<PopulatedGrantedGroup[]>([]);
   const [selectedUserGroupsForShareScope, setSelectedUserGroupsForShareScope] = useState<PopulatedGrantedGroup[]>([]);
-  const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
+  const [selectedPages, setSelectedPages] = useState<SelectablePage[]>([]);
   const [instruction, setInstruction] = useState<string>(t('modal_ai_assistant.default_instruction'));
   const [instruction, setInstruction] = useState<string>(t('modal_ai_assistant.default_instruction'));
 
 
 
 
@@ -121,23 +120,8 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   /*
   /*
   *  For AiAssistantManagementKeywordSearch & AiAssistantManagementPageTreeSelection methods
   *  For AiAssistantManagementKeywordSearch & AiAssistantManagementPageTreeSelection methods
   */
   */
-  const selectPageHandlerForKeywordSearchOrPageTreeSelection = useCallback((pages: IPageHasId[]) => {
-    if (pages.length === 0) {
-      return;
-    }
-
-    const convertedSelectedPages = pages.map((page) => {
-      const pagePath = page.path;
-      page.path = removeGlobPath([pagePath])[0];
-
-      return {
-        page,
-        // Determine whether to include subordinate pages from path
-        isIncludeSubPage: isGlobPatternPath(pagePath),
-      };
-    });
-
-    setSelectedPages(convertedSelectedPages);
+  const selectPageHandler = useCallback((pages: IPageHasId[]) => {
+    setSelectedPages(pages);
   }, []);
   }, []);
 
 
 
 
@@ -155,8 +139,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   const upsertAiAssistantHandler = useCallback(async() => {
   const upsertAiAssistantHandler = useCallback(async() => {
     try {
     try {
       const pagePathPatterns = selectedPages
       const pagePathPatterns = selectedPages
-        .map(selectedPage => (selectedPage.isIncludeSubPage ? `${selectedPage.page.path}/*` : selectedPage.page.path))
-        .filter((path): path is string => path !== undefined && path !== null);
+        .map(selectedPage => selectedPage.path);
 
 
       const grantedGroupsForShareScope = selectedShareScope === AiAssistantShareScope.GROUPS
       const grantedGroupsForShareScope = selectedShareScope === AiAssistantShareScope.GROUPS
         ? convertToGrantedGroups(selectedUserGroupsForShareScope)
         ? convertToGrantedGroups(selectedUserGroupsForShareScope)
@@ -241,15 +224,8 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   /*
   /*
   *  For AiAssistantManagementEditPages methods
   *  For AiAssistantManagementEditPages methods
   */
   */
-  const selectPageHandler = useCallback((page: IPageForItem, isIncludeSubPage: boolean) => {
-    const selectedPageIds = selectedPages.map(selectedPage => selectedPage.page.path);
-    if (page.path != null && !selectedPageIds.includes(page.path)) {
-      setSelectedPages([...selectedPages, { page, isIncludeSubPage }]);
-    }
-  }, [selectedPages]);
-
   const removePageHandler = useCallback((pagePath: string) => {
   const removePageHandler = useCallback((pagePath: string) => {
-    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.page.path !== pagePath));
+    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.path !== pagePath));
   }, [selectedPages]);
   }, [selectedPages]);
 
 
 
 
@@ -273,13 +249,15 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
 
 
         <TabPane tabId={AiAssistantManagementModalPageMode.KEYWORD_SEARCH}>
         <TabPane tabId={AiAssistantManagementModalPageMode.KEYWORD_SEARCH}>
           <AiAssistantKeywordSearch
           <AiAssistantKeywordSearch
-            updateBaseSelectedPages={selectPageHandlerForKeywordSearchOrPageTreeSelection}
+            baseSelectedPages={selectedPages}
+            updateBaseSelectedPages={selectPageHandler}
           />
           />
         </TabPane>
         </TabPane>
 
 
         <TabPane tabId={AiAssistantManagementModalPageMode.PAGE_TREE_SELECTION}>
         <TabPane tabId={AiAssistantManagementModalPageMode.PAGE_TREE_SELECTION}>
           <AiAssistantManagementPageTreeSelection
           <AiAssistantManagementPageTreeSelection
-            updateBaseSelectedPages={selectPageHandlerForKeywordSearchOrPageTreeSelection}
+            baseSelectedPages={selectedPages}
+            updateBaseSelectedPages={selectPageHandler}
           />
           />
         </TabPane>
         </TabPane>
 
 
@@ -316,7 +294,6 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
         <TabPane tabId={AiAssistantManagementModalPageMode.PAGES}>
         <TabPane tabId={AiAssistantManagementModalPageMode.PAGES}>
           <AiAssistantManagementEditPages
           <AiAssistantManagementEditPages
             selectedPages={selectedPages}
             selectedPages={selectedPages}
-            onSelect={selectPageHandler}
             onRemove={removePageHandler}
             onRemove={removePageHandler}
           />
           />
         </TabPane>
         </TabPane>

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

@@ -1,20 +0,0 @@
-@use '@growi/core-styles/scss/bootstrap/init' as bs;
-
-.grw-ai-assistant-management-page-selection-method :global {
-  .page-selection-method-btn {
-      width: 280px;
-  }
-}
-
-
-// == Colors
-.grw-ai-assistant-management-page-selection-method :global {
-  .page-selection-method-btn {
-      border: 2px solid bs.$gray-300;
-      &:hover {
-        color: var(--bs-primary);
-        background-color: rgba(var(--bs-primary-rgb), 0.1);
-        border-color: var(--bs-primary)
-      }
-    }
-}

+ 8 - 44
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageSelectionMethod.tsx

@@ -5,41 +5,18 @@ import {
   ModalBody,
   ModalBody,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
+import { useAiAssistantManagementModal } from '../../../stores/ai-assistant';
 
 
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
-
-import styles from './AiAssistantManagementPageSelectionMethod.module.scss';
-
-const moduleClass = styles['grw-ai-assistant-management-page-selection-method'] ?? '';
-
-const SelectionButton = (props: { icon: string, label: string, onClick: () => void }): JSX.Element => {
-  const { icon, label, onClick } = props;
-
-  return (
-    <button
-      type="button"
-      className="btn p-4 text-center page-selection-method-btn"
-      onClick={onClick}
-    >
-      <span
-        className="material-symbols-outlined d-block mb-3 fs-1"
-      >
-        {icon}
-      </span>
-      <div>{label}</div>
-    </button>
-  );
-};
-
+import { PageSelectionMethodButtons } from './PageSelectionMethodButtons';
 
 
 export const AiAssistantManagementPageSelectionMethod = (): JSX.Element => {
 export const AiAssistantManagementPageSelectionMethod = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: aiAssistantManagementModalData, changePageMode } = useAiAssistantManagementModal();
+  const { data: aiAssistantManagementModalData } = useAiAssistantManagementModal();
   const isNewAiAssistant = aiAssistantManagementModalData?.aiAssistantData == null;
   const isNewAiAssistant = aiAssistantManagementModalData?.aiAssistantData == null;
 
 
   return (
   return (
-    <div className={moduleClass}>
+    <>
       <AiAssistantManagementHeader
       <AiAssistantManagementHeader
         hideBackButton={isNewAiAssistant}
         hideBackButton={isNewAiAssistant}
         labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
         labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
@@ -49,23 +26,10 @@ export const AiAssistantManagementPageSelectionMethod = (): JSX.Element => {
         <h4 className="text-center fw-bold mb-4 mt-2">
         <h4 className="text-center fw-bold mb-4 mt-2">
           {t('modal_ai_assistant.select_source_pages')}
           {t('modal_ai_assistant.select_source_pages')}
         </h4>
         </h4>
-        <div className="row justify-content-center">
-          <div className="col-auto">
-            <SelectionButton
-              icon="manage_search"
-              label={t('modal_ai_assistant.search_by_keyword')}
-              onClick={() => changePageMode(AiAssistantManagementModalPageMode.KEYWORD_SEARCH)}
-            />
-          </div>
-          <div className="col-auto">
-            <SelectionButton
-              icon="account_tree"
-              label={t('modal_ai_assistant.select_from_page_tree')}
-              onClick={() => changePageMode(AiAssistantManagementModalPageMode.PAGE_TREE_SELECTION)}
-            />
-          </div>
-        </div>
+
+        <PageSelectionMethodButtons />
+
       </ModalBody>
       </ModalBody>
-    </div>
+    </>
   );
   );
 };
 };

+ 18 - 23
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementPageTreeSelection.tsx

@@ -2,7 +2,6 @@ import React, {
   Suspense, useCallback, memo, useMemo,
   Suspense, useCallback, memo, useMemo,
 } from 'react';
 } from 'react';
 
 
-import type { IPageHasId } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import {
 import {
   ModalBody,
   ModalBody,
@@ -15,6 +14,7 @@ import { TreeItemLayout } from '~/client/components/TreeItem';
 import type { IPageForItem } from '~/interfaces/page';
 import type { IPageForItem } from '~/interfaces/page';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores-universal/context';
 
 
+import { type SelectablePage, isSelectablePage } from '../../../../interfaces/selectable-page';
 import { useSelectedPages } from '../../../services/use-selected-pages';
 import { useSelectedPages } from '../../../services/use-selected-pages';
 import { AiAssistantManagementModalPageMode, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
 import { AiAssistantManagementModalPageMode, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
 
 
@@ -25,22 +25,14 @@ import styles from './AiAssistantManagementPageTreeSelection.module.scss';
 
 
 const moduleClass = styles['grw-ai-assistant-management-page-tree-selection'] ?? '';
 const moduleClass = styles['grw-ai-assistant-management-page-tree-selection'] ?? '';
 
 
-export const isIPageHasId = (value?: IPageForItem): value is IPageHasId => {
-  if (value == null) {
-    return false;
-  }
-  return value._id != null && value.path != null;
-};
-
-
-const SelectablePageTree = memo((props: { onClickAddPageButton: (page: IPageHasId) => void }) => {
+const SelectablePageTree = memo((props: { onClickAddPageButton: (page: SelectablePage) => void }) => {
   const { onClickAddPageButton } = props;
   const { onClickAddPageButton } = props;
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
 
   const pageTreeItemClickHandler = useCallback((page: IPageForItem) => {
   const pageTreeItemClickHandler = useCallback((page: IPageForItem) => {
-    if (!isIPageHasId(page)) {
+    if (!isSelectablePage(page)) {
       return;
       return;
     }
     }
 
 
@@ -88,21 +80,24 @@ const SelectablePageTree = memo((props: { onClickAddPageButton: (page: IPageHasI
   );
   );
 });
 });
 
 
+type Props = {
+  baseSelectedPages: SelectablePage[],
+  updateBaseSelectedPages: (pages: SelectablePage[]) => void;
+}
 
 
-export const AiAssistantManagementPageTreeSelection = (props: { updateBaseSelectedPages: (pages: IPageHasId[]) => void}): JSX.Element => {
-  const { updateBaseSelectedPages } = props;
+export const AiAssistantManagementPageTreeSelection = (props: Props): JSX.Element => {
+  const { baseSelectedPages, updateBaseSelectedPages } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: aiAssistantManagementModalData, changePageMode } = useAiAssistantManagementModal();
   const { data: aiAssistantManagementModalData, changePageMode } = useAiAssistantManagementModal();
   const isNewAiAssistant = aiAssistantManagementModalData?.aiAssistantData == null;
   const isNewAiAssistant = aiAssistantManagementModalData?.aiAssistantData == null;
 
 
-  const { selectedPages, addPageHandler, removePageHandler } = useSelectedPages();
+  const {
+    selectedPages, addPage, removePage,
+  } = useSelectedPages(baseSelectedPages);
 
 
   // SelectedPages will include subordinate pages by default
   // SelectedPages will include subordinate pages by default
-  const pagesWithGlobPath = useMemo((): IPageHasId[] | undefined => {
-    if (selectedPages.size === 0) {
-      return;
-    }
+  const pagesWithGlobPath = useMemo(() => {
     return Array.from(selectedPages.values()).map((page) => {
     return Array.from(selectedPages.values()).map((page) => {
       if (page.path === '/') {
       if (page.path === '/') {
         page.path = '/*';
         page.path = '/*';
@@ -118,14 +113,14 @@ export const AiAssistantManagementPageTreeSelection = (props: { updateBaseSelect
 
 
   const nextButtonClickHandler = useCallback(() => {
   const nextButtonClickHandler = useCallback(() => {
     updateBaseSelectedPages(Array.from(selectedPages.values()));
     updateBaseSelectedPages(Array.from(selectedPages.values()));
-    changePageMode(AiAssistantManagementModalPageMode.HOME);
-  }, [changePageMode, selectedPages, updateBaseSelectedPages]);
+    changePageMode(isNewAiAssistant ? AiAssistantManagementModalPageMode.HOME : AiAssistantManagementModalPageMode.PAGES);
+  }, [changePageMode, isNewAiAssistant, selectedPages, updateBaseSelectedPages]);
 
 
   return (
   return (
     <div className={moduleClass}>
     <div className={moduleClass}>
       <AiAssistantManagementHeader
       <AiAssistantManagementHeader
         backButtonColor="secondary"
         backButtonColor="secondary"
-        backToPageMode={AiAssistantManagementModalPageMode.PAGE_SELECTION_METHOD}
+        backToPageMode={baseSelectedPages.length === 0 ? AiAssistantManagementModalPageMode.PAGE_SELECTION_METHOD : AiAssistantManagementModalPageMode.PAGES}
         labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
         labelTranslationKey={isNewAiAssistant ? 'modal_ai_assistant.header.add_new_assistant' : 'modal_ai_assistant.header.update_assistant'}
       />
       />
 
 
@@ -136,7 +131,7 @@ export const AiAssistantManagementPageTreeSelection = (props: { updateBaseSelect
 
 
         <Suspense fallback={<ItemsTreeContentSkeleton />}>
         <Suspense fallback={<ItemsTreeContentSkeleton />}>
           <div className="px-4">
           <div className="px-4">
-            <SelectablePageTree onClickAddPageButton={addPageHandler} />
+            <SelectablePageTree onClickAddPageButton={addPage} />
           </div>
           </div>
         </Suspense>
         </Suspense>
 
 
@@ -149,7 +144,7 @@ export const AiAssistantManagementPageTreeSelection = (props: { updateBaseSelect
             method="remove"
             method="remove"
             methodButtonPosition="right"
             methodButtonPosition="right"
             pages={pagesWithGlobPath ?? []}
             pages={pagesWithGlobPath ?? []}
-            onClickMethodButton={removePageHandler}
+            onClickMethodButton={removePage}
           />
           />
           <label className="form-text text-muted mt-2">
           <label className="form-text text-muted mt-2">
             {t('modal_ai_assistant.can_add_later')}
             {t('modal_ai_assistant.can_add_later')}

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

@@ -0,0 +1,28 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+// == Colors
+.page-selection-method-buttons :global {
+  .page-selection-method-btn {
+      &:hover {
+        color: var(--bs-primary);
+        background-color: rgba(var(--bs-primary-rgb), 0.1);
+        border-color: var(--bs-primary) !important;
+      }
+    }
+}
+
+@include bs.color-mode(light) {
+  .page-selection-method-buttons :global {
+    .page-selection-method-btn {
+      border: 2px solid bs.$gray-300;
+    }
+  }
+}
+
+@include bs.color-mode(dark) {
+  .page-selection-method-buttons :global {
+    .page-selection-method-btn {
+      border: 2px solid bs.$gray-700;
+    }
+  }
+}

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

@@ -0,0 +1,55 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
+
+import styles from './PageSelectionMethodButtons.module.scss';
+
+const moduleClass = styles['page-selection-method-buttons'] ?? '';
+
+const SelectionButton = (props: { icon: string, label: string, onClick: () => void }): JSX.Element => {
+  const { icon, label, onClick } = props;
+
+  return (
+    <button
+      type="button"
+      className="btn text-center py-4 w-100 page-selection-method-btn"
+      onClick={onClick}
+    >
+      <span
+        className="material-symbols-outlined d-block mb-3 fs-1"
+      >
+        {icon}
+      </span>
+      <div>{label}</div>
+    </button>
+  );
+};
+
+
+export const PageSelectionMethodButtons = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { changePageMode } = useAiAssistantManagementModal();
+
+  return (
+    <div className={moduleClass}>
+      <div className="row g-3">
+        <div className="col">
+          <SelectionButton
+            icon="manage_search"
+            label={t('modal_ai_assistant.search_by_keyword')}
+            onClick={() => changePageMode(AiAssistantManagementModalPageMode.KEYWORD_SEARCH)}
+          />
+        </div>
+        <div className="col">
+          <SelectionButton
+            icon="account_tree"
+            label={t('modal_ai_assistant.select_from_page_tree')}
+            onClick={() => changePageMode(AiAssistantManagementModalPageMode.PAGE_TREE_SELECTION)}
+          />
+        </div>
+      </div>
+    </div>
+  );
+};

+ 95 - 28
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectablePagePageList.tsx

@@ -1,46 +1,93 @@
-import type { IPageHasId } from '@growi/core';
+import React, { useMemo, memo } from 'react';
+
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
+import { type SelectablePage } from '../../../../interfaces/selectable-page';
+
 import styles from './SelectablePagePageList.module.scss';
 import styles from './SelectablePagePageList.module.scss';
 
 
 const moduleClass = styles['selectable-page-page-list'] ?? '';
 const moduleClass = styles['selectable-page-page-list'] ?? '';
 
 
-type Props = {
-  pages: IPageHasId[],
-  method: 'add' | 'remove',
+type MethodButtonProps = {
+  page: SelectablePage;
+  disablePagePaths: string[];
+  methodButtonColor: string;
+  methodButtonIconName: string;
+  onClickMethodButton: (page: SelectablePage) => void;
+}
+
+const MethodButton = memo((props: MethodButtonProps) => {
+  const {
+    page,
+    disablePagePaths,
+    methodButtonColor,
+    methodButtonIconName,
+    onClickMethodButton,
+  } = props;
+
+  return (
+    <button
+      type="button"
+      className={`btn border-0 ${methodButtonColor}`}
+      disabled={disablePagePaths.includes(page.path)}
+      onClick={(e) => {
+        e.stopPropagation();
+        onClickMethodButton(page);
+      }}
+    >
+      <span className="material-symbols-outlined">
+        {methodButtonIconName}
+      </span>
+    </button>
+  );
+});
+
+
+type SelectablePagePageListProps = {
+  pages: SelectablePage[],
+  method: 'add' | 'remove' | 'delete'
   methodButtonPosition?: 'left' | 'right',
   methodButtonPosition?: 'left' | 'right',
-  disablePageIds?: string[],
-  onClickMethodButton: (page: IPageHasId) => void,
+  disablePagePaths?: string[],
+  onClickMethodButton: (page: SelectablePage) => void,
 }
 }
 
 
-export const SelectablePagePageList = (props: Props): JSX.Element => {
+export const SelectablePagePageList = (props: SelectablePagePageListProps): JSX.Element => {
   const {
   const {
     pages,
     pages,
     method,
     method,
     methodButtonPosition = 'left',
     methodButtonPosition = 'left',
-    disablePageIds,
+    disablePagePaths = [],
     onClickMethodButton,
     onClickMethodButton,
   } = props;
   } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const methodButton = (page: IPageHasId) => {
-    return (
-      <button
-        type="button"
-        className={`btn border-0 ${method === 'add' ? 'text-primary' : 'text-secondary'}`}
-        disabled={disablePageIds?.includes(page._id)}
-        onClick={(e) => {
-          e.stopPropagation();
-          onClickMethodButton(page);
-        }}
-      >
-        <span className="material-symbols-outlined">
-          { method === 'add' ? 'add_circle' : 'do_not_disturb_on' }
-        </span>
-      </button>
-    );
-  };
+  const methodButtonIconName = useMemo(() => {
+    switch (method) {
+      case 'add':
+        return 'add_circle';
+      case 'remove':
+        return 'do_not_disturb_on';
+      case 'delete':
+        return 'delete';
+      default:
+        return '';
+    }
+  }, [method]);
+
+  const methodButtonColor = useMemo(() => {
+    switch (method) {
+      case 'add':
+        return 'text-primary';
+      case 'remove':
+        return 'text-secondary';
+      case 'delete':
+        return 'text-secondary';
+      default:
+        return '';
+    }
+  }, [method]);
+
 
 
   if (pages.length === 0) {
   if (pages.length === 0) {
     return (
     return (
@@ -57,7 +104,7 @@ export const SelectablePagePageList = (props: Props): JSX.Element => {
       {pages.map((page) => {
       {pages.map((page) => {
         return (
         return (
           <button
           <button
-            key={page._id}
+            key={page.path}
             type="button"
             type="button"
             className="list-group-item border-0 list-group-item-action page-list-item d-flex align-items-center p-1 mb-2 rounded"
             className="list-group-item border-0 list-group-item-action page-list-item d-flex align-items-center p-1 mb-2 rounded"
             onClick={(e) => {
             onClick={(e) => {
@@ -65,7 +112,17 @@ export const SelectablePagePageList = (props: Props): JSX.Element => {
             }}
             }}
           >
           >
 
 
-            {methodButtonPosition === 'left' && methodButton(page)}
+            {methodButtonPosition === 'left'
+              && (
+                <MethodButton
+                  page={page}
+                  disablePagePaths={disablePagePaths}
+                  methodButtonColor={methodButtonColor}
+                  methodButtonIconName={methodButtonIconName}
+                  onClickMethodButton={onClickMethodButton}
+                />
+              )
+            }
 
 
             <div className={`flex-grow-1 ${methodButtonPosition === 'left' ? 'me-4' : 'ms-2'}`}>
             <div className={`flex-grow-1 ${methodButtonPosition === 'left' ? 'me-4' : 'ms-2'}`}>
               <span>
               <span>
@@ -79,7 +136,17 @@ export const SelectablePagePageList = (props: Props): JSX.Element => {
               </span>
               </span>
             </span>
             </span>
 
 
-            {methodButtonPosition === 'right' && methodButton(page)}
+            {methodButtonPosition === 'right'
+              && (
+                <MethodButton
+                  page={page}
+                  disablePagePaths={disablePagePaths}
+                  methodButtonColor={methodButtonColor}
+                  methodButtonIconName={methodButtonIconName}
+                  onClickMethodButton={onClickMethodButton}
+                />
+              )
+            }
           </button>
           </button>
         );
         );
       })}
       })}

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

@@ -1,43 +0,0 @@
-import { memo } from 'react';
-
-import type { SelectedPage } from '../../../../interfaces/selected-page';
-
-type SelectedPageListProps = {
-  selectedPages: SelectedPage[];
-  onRemove?: (pagePath?: string) => void;
-};
-
-const SelectedPageListBase: React.FC<SelectedPageListProps> = ({ selectedPages, onRemove }: SelectedPageListProps) => {
-  if (selectedPages.length === 0) {
-    return <></>;
-  }
-
-  return (
-    <div className="mb-3">
-      {selectedPages.map(({ page, isIncludeSubPage }) => (
-        <div
-          key={page.path}
-          className="mb-2 d-flex justify-content-between align-items-center bg-body-tertiary rounded py-2 px-3"
-        >
-          <div className="d-flex align-items-center overflow-hidden text-body">
-            { isIncludeSubPage
-              ? <>{`${page.path}/*`}</>
-              : <>{page.path}</>
-            }
-          </div>
-          {onRemove != null && page.path != null && (
-            <button
-              type="button"
-              className="btn p-0 ms-3 text-body-secondary"
-              onClick={() => onRemove(page.path)}
-            >
-              <span className="material-symbols-outlined fs-4">delete</span>
-            </button>
-          )}
-        </div>
-      ))}
-    </div>
-  );
-};
-
-export const SelectedPageList = memo(SelectedPageListBase);

+ 4 - 4
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeWarningModal.tsx

@@ -5,11 +5,11 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import type { SelectedPage } from '../../../../interfaces/selected-page';
+import type { SelectablePage } from '../../../../interfaces/selectable-page';
 
 
 type Props = {
 type Props = {
   isOpen: boolean,
   isOpen: boolean,
-  selectedPages: SelectedPage[],
+  selectedPages: SelectablePage[],
   closeModal: () => void,
   closeModal: () => void,
   onSubmit: () => Promise<void>,
   onSubmit: () => Promise<void>,
 }
 }
@@ -48,8 +48,8 @@ export const ShareScopeWarningModal = (props: Props): JSX.Element => {
         <div className="mb-4">
         <div className="mb-4">
           <p className="mb-2 text-secondary">{t('share_scope_warning_modal.selected_pages_label')}</p>
           <p className="mb-2 text-secondary">{t('share_scope_warning_modal.selected_pages_label')}</p>
           {selectedPages.map(selectedPage => (
           {selectedPages.map(selectedPage => (
-            <code key={selectedPage.page.path}>
-              {selectedPage.page.path}
+            <code key={selectedPage.path}>
+              {selectedPage.path}
             </code>
             </code>
           ))}
           ))}
         </div>
         </div>

+ 0 - 35
apps/app/src/features/openai/client/services/use-selected-pages.ts

@@ -1,35 +0,0 @@
-import { useState, useCallback } from 'react';
-
-import type { IPageHasId } from '@growi/core';
-
-type UseSelectedPages = {
-  selectedPages: Map<string, IPageHasId>,
-  addPageHandler: (page: IPageHasId) => void,
-  removePageHandler: (page: IPageHasId) => void,
-}
-
-export const useSelectedPages = (): UseSelectedPages => {
-  const [selectedPages, setSelectedPages] = useState<Map<string, IPageHasId>>(new Map());
-
-  const addPageHandler = useCallback((page: IPageHasId) => {
-    setSelectedPages((prev) => {
-      const newMap = new Map(prev);
-      newMap.set(page._id, page);
-      return newMap;
-    });
-  }, []);
-
-  const removePageHandler = useCallback((page: IPageHasId) => {
-    setSelectedPages((prev) => {
-      const newMap = new Map(prev);
-      newMap.delete(page._id);
-      return newMap;
-    });
-  }, []);
-
-  return {
-    selectedPages,
-    addPageHandler,
-    removePageHandler,
-  };
-};

+ 55 - 0
apps/app/src/features/openai/client/services/use-selected-pages.tsx

@@ -0,0 +1,55 @@
+import { useState, useCallback, useEffect } from 'react';
+
+import type { SelectablePage } from '../../interfaces/selectable-page';
+import { useAiAssistantManagementModal } from '../stores/ai-assistant';
+
+
+type UseSelectedPages = {
+  selectedPages: Map<string, SelectablePage>,
+  addPage: (page: SelectablePage) => void,
+  removePage: (page: SelectablePage) => void,
+}
+
+export const useSelectedPages = (initialPages?: SelectablePage[]): UseSelectedPages => {
+  const [selectedPages, setSelectedPages] = useState<Map<string, SelectablePage>>(new Map());
+  const { data: aiAssistantManagementModalData } = useAiAssistantManagementModal();
+
+  useEffect(() => {
+    // Initialize each time PageMode is changed
+    if (initialPages != null && aiAssistantManagementModalData?.pageMode != null) {
+      const initialMap = new Map<string, SelectablePage>();
+      initialPages.forEach((page) => {
+        if (page.path != null) {
+          initialMap.set(page.path, page);
+        }
+      });
+      setSelectedPages(initialMap);
+    }
+  }, [aiAssistantManagementModalData?.pageMode, initialPages]);
+
+  const addPage = useCallback((page: SelectablePage) => {
+    setSelectedPages((prev) => {
+      const newMap = new Map(prev);
+      if (page.path != null) {
+        newMap.set(page.path, page);
+      }
+      return newMap;
+    });
+  }, []);
+
+  const removePage = useCallback((page: SelectablePage) => {
+    setSelectedPages((prev) => {
+      const newMap = new Map(prev);
+      if (page.path != null) {
+        newMap.delete(page.path);
+      }
+      return newMap;
+    });
+  }, []);
+
+  return {
+    selectedPages,
+    addPage,
+    removePage,
+  };
+};

+ 10 - 0
apps/app/src/features/openai/interfaces/selectable-page.ts

@@ -0,0 +1,10 @@
+import type { IPageHasId } from '@growi/core';
+
+import type { IPageForItem } from '~/interfaces/page';
+
+export type SelectablePage = Partial<IPageHasId> & { path: string }
+
+// type guard
+export const isSelectablePage = (page: IPageForItem): page is SelectablePage => {
+  return page.path != null;
+};

+ 0 - 8
apps/app/src/features/openai/interfaces/selected-page.ts

@@ -1,8 +0,0 @@
-import type { IPageHasId } from '@growi/core';
-
-import type { IPageForItem } from '~/interfaces/page';
-
-export type SelectedPage = {
-  page: IPageForItem | IPageHasId,
-  isIncludeSubPage: boolean,
-}