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

Merge remote-tracking branch 'origin/feat/growi-ai-next' into feat/unified-merge-view

Yuki Takei 1 год назад
Родитель
Сommit
ab418d855a

+ 8 - 0
apps/app/src/components/Layout/BasicLayout.tsx

@@ -8,6 +8,12 @@ import { RawLayout } from './RawLayout';
 
 
 import styles from './BasicLayout.module.scss';
 import styles from './BasicLayout.module.scss';
 
 
+const AiAssistantChatSidebar = dynamic(
+  () => import('~/features/openai/client/components/AiAssistant/AiAssistantChatSidebar/AiAssistantChatSidebar')
+    .then(mod => mod.AiAssistantChatSidebar), { ssr: false },
+);
+
+
 const moduleClass = styles['grw-basic-layout'] ?? '';
 const moduleClass = styles['grw-basic-layout'] ?? '';
 
 
 
 
@@ -59,6 +65,8 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
           <AlertSiteUrlUndefined />
           <AlertSiteUrlUndefined />
           {children}
           {children}
         </div>
         </div>
+
+        <AiAssistantChatSidebar />
       </div>
       </div>
 
 
       <GrowiNavbarBottom />
       <GrowiNavbarBottom />

+ 14 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantChatSidebar/AiAssistantChatSidebar.module.scss

@@ -0,0 +1,14 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+@use '@growi/core-styles/scss/variables/growi-official-colors';
+@use '@growi/ui/scss/atoms/btn-muted';
+
+// == Colors
+.grw-ai-assistant-chat-sidebar :global {
+  .growi-ai-chat-icon {
+    color: growi-official-colors.$growi-ai-purple;
+  }
+
+  .btn-submit {
+    @include btn-muted.colorize(bs.$purple, bs.$purple);
+  }
+}

+ 90 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantChatSidebar/AiAssistantChatSidebar.tsx

@@ -0,0 +1,90 @@
+import {
+  type FC, memo, useRef, useEffect,
+} from 'react';
+
+import SimpleBar from 'simplebar-react';
+
+import type { AiAssistantHasId } from '../../../../interfaces/ai-assistant';
+import { useAiAssistantChatSidebar } from '../../../stores/ai-assistant';
+
+import styles from './AiAssistantChatSidebar.module.scss';
+
+const moduleClass = styles['grw-ai-assistant-chat-sidebar'] ?? '';
+
+const RIGHT_SIDEBAR_WIDTH = 500;
+
+type AiAssistantChatSidebarSubstanceProps = {
+  aiAssistantData?: AiAssistantHasId;
+  closeAiAssistantChatSidebar: () => void
+}
+
+const AiAssistantChatSidebarSubstance: React.FC<AiAssistantChatSidebarSubstanceProps> = (props: AiAssistantChatSidebarSubstanceProps) => {
+  const { aiAssistantData, closeAiAssistantChatSidebar } = props;
+
+  return (
+    <>
+      <div className="d-flex align-items-center p-3 border-bottom">
+        <span className="growi-custom-icons growi-ai-chat-icon me-3 fs-4">ai_assistant</span>
+        <h5 className="mb-0 fw-bold flex-grow-1 text-truncate">{aiAssistantData?.name}</h5>
+        <button
+          type="button"
+          className="btn btn-link p-0 border-0"
+          onClick={closeAiAssistantChatSidebar}
+        >
+          <span className="material-symbols-outlined">close</span>
+        </button>
+      </div>
+
+      <div className="p-3 w-100">
+        {/* AI Chat Screen Implementation */}
+        {/* TODO: https://redmine.weseek.co.jp/issues/161511 */}
+      </div>
+    </>
+  );
+};
+
+
+export const AiAssistantChatSidebar: FC = memo((): JSX.Element => {
+  const sidebarRef = useRef<HTMLDivElement>(null);
+  const sidebarScrollerRef = useRef<HTMLDivElement>(null);
+
+  const { data: aiAssistantChatSidebarData, close: closeAiAssistantChatSidebar } = useAiAssistantChatSidebar();
+  const isOpened = aiAssistantChatSidebarData?.isOpened ?? false;
+
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      if (isOpened && sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
+        closeAiAssistantChatSidebar();
+      }
+    };
+
+    document.addEventListener('mousedown', handleClickOutside);
+    return () => {
+      document.removeEventListener('mousedown', handleClickOutside);
+    };
+  }, [closeAiAssistantChatSidebar, isOpened]);
+
+  return (
+    <>
+      {isOpened && (
+        <div
+          ref={sidebarRef}
+          className={`position-fixed top-0 end-0 h-100 border-start bg-white shadow-sm ${moduleClass}`}
+          style={{ zIndex: 1500, width: `${RIGHT_SIDEBAR_WIDTH}px` }}
+          data-testid="grw-right-sidebar"
+        >
+          <SimpleBar
+            scrollableNodeProps={{ ref: sidebarScrollerRef }}
+            className="h-100 position-relative"
+            autoHide
+          >
+            <AiAssistantChatSidebarSubstance
+              aiAssistantData={aiAssistantChatSidebarData?.aiAssistantData}
+              closeAiAssistantChatSidebar={closeAiAssistantChatSidebar}
+            />
+          </SimpleBar>
+        </div>
+      )}
+    </>
+  );
+});

+ 13 - 1
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditShare.tsx

@@ -1,4 +1,6 @@
-import React, { useCallback, useState } from 'react';
+import React, {
+  useCallback, useState, useEffect,
+} from 'react';
 
 
 import {
 import {
   ModalBody, Input, Label,
   ModalBody, Input, Label,
@@ -50,6 +52,15 @@ export const AiAssistantManagementEditShare = (props: Props): JSX.Element => {
   const [isSelectUserGroupModalOpen, setIsSelectUserGroupModalOpen] = useState(false);
   const [isSelectUserGroupModalOpen, setIsSelectUserGroupModalOpen] = useState(false);
   const [selectedUserGroupType, setSelectedUserGroupType] = useState<ScopeType>(ScopeType.ACCESS);
   const [selectedUserGroupType, setSelectedUserGroupType] = useState<ScopeType>(ScopeType.ACCESS);
 
 
+  useEffect(() => {
+    setIsShared(() => {
+      if (selectedShareScope !== AiAssistantShareScope.SAME_AS_ACCESS_SCOPE) {
+        return true;
+      }
+      return selectedShareScope === AiAssistantShareScope.SAME_AS_ACCESS_SCOPE && selectedAccessScope !== AiAssistantAccessScope.OWNER;
+    });
+  }, [isShared, selectedAccessScope, selectedShareScope]);
+
   const changeShareToggleHandler = useCallback(() => {
   const changeShareToggleHandler = useCallback(() => {
     setIsShared((prev) => {
     setIsShared((prev) => {
       if (prev) { // if isShared === true
       if (prev) { // if isShared === true
@@ -95,6 +106,7 @@ export const AiAssistantManagementEditShare = (props: Props): JSX.Element => {
             id="shareAssistantSwitch"
             id="shareAssistantSwitch"
             className="form-check-input"
             className="form-check-input"
             checked={isShared}
             checked={isShared}
+            defaultChecked={isShared}
             onChange={changeShareToggleHandler}
             onChange={changeShareToggleHandler}
           />
           />
           <Label className="form-check-label" for="shareAssistantSwitch">
           <Label className="form-check-label" for="shareAssistantSwitch">

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

@@ -13,6 +13,7 @@ import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } fro
 import { ShareScopeWarningModal } from './ShareScopeWarningModal';
 import { ShareScopeWarningModal } from './ShareScopeWarningModal';
 
 
 type Props = {
 type Props = {
+  shouldEdit: boolean;
   name: string;
   name: string;
   description: string;
   description: string;
   instruction: string;
   instruction: string;
@@ -24,6 +25,7 @@ type Props = {
 
 
 export const AiAssistantManagementHome = (props: Props): JSX.Element => {
 export const AiAssistantManagementHome = (props: Props): JSX.Element => {
   const {
   const {
+    shouldEdit,
     name,
     name,
     description,
     description,
     instruction,
     instruction,
@@ -61,7 +63,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
     <>
     <>
       <ModalHeader tag="h4" toggle={closeAiAssistantManagementModal} className="pe-4">
       <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="growi-custom-icons growi-ai-assistant-icon me-3 fs-4">ai_assistant</span>
-        <span className="fw-bold">新規アシスタントの追加</span> {/* TODO i18n */}
+        <span className="fw-bold">{t(shouldEdit ? 'アシスタントの更新' : '新規アシスタントの追加')}</span> {/* TODO i18n */}
       </ModalHeader>
       </ModalHeader>
 
 
       <div className="px-4">
       <div className="px-4">
@@ -137,7 +139,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
 
 
         <ModalFooter>
         <ModalFooter>
           <button type="button" className="btn btn-outline-secondary" onClick={closeAiAssistantManagementModal}>キャンセル</button>
           <button type="button" className="btn btn-outline-secondary" onClick={closeAiAssistantManagementModal}>キャンセル</button>
-          <button type="button" className="btn btn-primary" onClick={createAiAssistantHandler}>アシスタントを作成する</button>
+          <button type="button" className="btn btn-primary" onClick={createAiAssistantHandler}>{t(shouldEdit ? 'アシスタントを更新する' : 'アシスタントを作成する')}</button>
         </ModalFooter>
         </ModalFooter>
       </div>
       </div>
 
 

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

@@ -1,6 +1,6 @@
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useState, useEffect } from 'react';
 
 
-import type { IGrantedGroup } from '@growi/core';
+import { type IGrantedGroup, isPopulated } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { Modal, TabContent, TabPane } from 'reactstrap';
 import { Modal, TabContent, TabPane } from 'reactstrap';
 
 
@@ -11,7 +11,7 @@ import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import type { SelectedPage } from '../../../../interfaces/selected-page';
 import type { SelectedPage } from '../../../../interfaces/selected-page';
-import { createAiAssistant } from '../../../services/ai-assistant';
+import { createAiAssistant, updateAiAssistant } from '../../../services/ai-assistant';
 import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode, useSWRxAiAssistants } from '../../../stores/ai-assistant';
 import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode, useSWRxAiAssistants } from '../../../stores/ai-assistant';
 
 
 import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
 import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
@@ -33,12 +33,32 @@ const convertToGrantedGroups = (selectedGroups: PopulatedGrantedGroup[]): IGrant
   }));
   }));
 };
 };
 
 
+// IGrantedGroup[] -> PopulatedGrantedGroup[]
+const convertToPopulatedGrantedGroups = (selectedGroups: IGrantedGroup[]): PopulatedGrantedGroup[] => {
+  const populatedGrantedGroups = selectedGroups.filter(group => isPopulated(group.item)) as PopulatedGrantedGroup[];
+  return populatedGrantedGroups;
+};
+
+// string[] -> SelectedPage[]
+const convertToSelectedPages = (pagePathPatterns: string[]): SelectedPage[] => {
+  return pagePathPatterns.map((pagePathPattern) => {
+    const isIncludeSubPage = pagePathPattern.endsWith('/*');
+    const path = isIncludeSubPage ? pagePathPattern.slice(0, -2) : pagePathPattern;
+    return {
+      page: { path },
+      isIncludeSubPage,
+    };
+  });
+};
+
 const AiAssistantManagementModalSubstance = (): JSX.Element => {
 const AiAssistantManagementModalSubstance = (): JSX.Element => {
   // Hooks
   // Hooks
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { mutate: mutateAiAssistants } = useSWRxAiAssistants();
   const { mutate: mutateAiAssistants } = useSWRxAiAssistants();
   const { data: aiAssistantManagementModalData, close: closeAiAssistantManagementModal } = useAiAssistantManagementModal();
   const { data: aiAssistantManagementModalData, close: closeAiAssistantManagementModal } = useAiAssistantManagementModal();
 
 
+  const aiAssistant = aiAssistantManagementModalData?.aiAssistantData;
+  const shouldEdit = aiAssistant != null;
   const pageMode = aiAssistantManagementModalData?.pageMode ?? AiAssistantManagementModalPageMode.HOME;
   const pageMode = aiAssistantManagementModalData?.pageMode ?? AiAssistantManagementModalPageMode.HOME;
 
 
   // States
   // States
@@ -51,6 +71,20 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
   const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
   const [instruction, setInstruction] = useState<string>(t('modal_ai_assistant.default_instruction'));
   const [instruction, setInstruction] = useState<string>(t('modal_ai_assistant.default_instruction'));
 
 
+  // Effects
+  useEffect(() => {
+    if (shouldEdit) {
+      setName(aiAssistant.name);
+      setDescription(aiAssistant.description);
+      setInstruction(aiAssistant.additionalInstruction);
+      setSelectedPages(convertToSelectedPages(aiAssistant.pagePathPatterns));
+      setSelectedShareScope(aiAssistant.shareScope);
+      setSelectedAccessScope(aiAssistant.accessScope);
+      setSelectedUserGroupsForShareScope(convertToPopulatedGrantedGroups(aiAssistant.grantedGroupsForShareScope ?? []));
+      setSelectedUserGroupsForAccessScope(convertToPopulatedGrantedGroups(aiAssistant.grantedGroupsForAccessScope ?? []));
+    }
+  // eslint-disable-next-line max-len
+  }, [aiAssistant?.accessScope, aiAssistant?.additionalInstruction, aiAssistant?.description, aiAssistant?.grantedGroupsForAccessScope, aiAssistant?.grantedGroupsForShareScope, aiAssistant?.name, aiAssistant?.pagePathPatterns, aiAssistant?.shareScope, shouldEdit]);
 
 
   /*
   /*
   *  For AiAssistantManagementHome methods
   *  For AiAssistantManagementHome methods
@@ -69,40 +103,42 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
         .map(selectedPage => (selectedPage.isIncludeSubPage ? `${selectedPage.page.path}/*` : selectedPage.page.path))
         .map(selectedPage => (selectedPage.isIncludeSubPage ? `${selectedPage.page.path}/*` : selectedPage.page.path))
         .filter((path): path is string => path !== undefined && path !== null);
         .filter((path): path is string => path !== undefined && path !== null);
 
 
-      const grantedGroupsForShareScope = convertToGrantedGroups(selectedUserGroupsForShareScope);
-      const grantedGroupsForAccessScope = convertToGrantedGroups(selectedUserGroupsForAccessScope);
+      const grantedGroupsForShareScope = selectedShareScope === AiAssistantShareScope.GROUPS
+        ? convertToGrantedGroups(selectedUserGroupsForShareScope)
+        : undefined;
+
+      const grantedGroupsForAccessScope = selectedAccessScope === AiAssistantAccessScope.GROUPS
+        ? convertToGrantedGroups(selectedUserGroupsForAccessScope)
+        : undefined;
 
 
-      await createAiAssistant({
+      const reqBody = {
         name,
         name,
         description,
         description,
         additionalInstruction: instruction,
         additionalInstruction: instruction,
         pagePathPatterns,
         pagePathPatterns,
         shareScope: selectedShareScope,
         shareScope: selectedShareScope,
         accessScope: selectedAccessScope,
         accessScope: selectedAccessScope,
-        grantedGroupsForShareScope: selectedShareScope === AiAssistantShareScope.GROUPS ? grantedGroupsForShareScope : undefined,
-        grantedGroupsForAccessScope: selectedAccessScope === AiAssistantAccessScope.GROUPS ? grantedGroupsForAccessScope : undefined,
-      });
-
-      toastSuccess('アシスタントを作成しました');
+        grantedGroupsForShareScope,
+        grantedGroupsForAccessScope,
+      };
+
+      if (shouldEdit) {
+        await updateAiAssistant(aiAssistant._id, reqBody);
+      }
+      else {
+        await createAiAssistant(reqBody);
+      }
+
+      toastSuccess(shouldEdit ? 'アシスタントが更新されました' : 'アシスタントが作成されました');
       mutateAiAssistants();
       mutateAiAssistants();
       closeAiAssistantManagementModal();
       closeAiAssistantManagementModal();
     }
     }
     catch (err) {
     catch (err) {
-      toastError('アシスタントの作成に失敗しました');
+      toastError(shouldEdit ? 'アシスタントの更新に失敗しました' : 'アシスタントの作成に失敗しました');
       logger.error(err);
       logger.error(err);
     }
     }
-  }, [
-    mutateAiAssistants,
-    closeAiAssistantManagementModal,
-    description,
-    instruction,
-    name,
-    selectedAccessScope,
-    selectedPages,
-    selectedShareScope,
-    selectedUserGroupsForAccessScope,
-    selectedUserGroupsForShareScope,
-  ]);
+  // eslint-disable-next-line max-len
+  }, [selectedPages, selectedShareScope, selectedUserGroupsForShareScope, selectedAccessScope, selectedUserGroupsForAccessScope, name, description, instruction, shouldEdit, mutateAiAssistants, closeAiAssistantManagementModal, aiAssistant?._id]);
 
 
 
 
   /*
   /*
@@ -172,6 +208,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
       <TabContent activeTab={pageMode}>
       <TabContent activeTab={pageMode}>
         <TabPane tabId={AiAssistantManagementModalPageMode.HOME}>
         <TabPane tabId={AiAssistantManagementModalPageMode.HOME}>
           <AiAssistantManagementHome
           <AiAssistantManagementHome
+            shouldEdit={shouldEdit}
             name={name}
             name={name}
             description={description}
             description={description}
             shareScope={selectedShareScope}
             shareScope={selectedShareScope}

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

@@ -17,7 +17,7 @@ export const AiAssistantContent = (): JSX.Element => {
       <button
       <button
         type="button"
         type="button"
         className="btn btn-outline-secondary px-3 d-flex align-items-center mb-4"
         className="btn btn-outline-secondary px-3 d-flex align-items-center mb-4"
-        onClick={open}
+        onClick={() => open()}
       >
       >
         <span className="material-symbols-outlined fs-5 me-2">add</span>
         <span className="material-symbols-outlined fs-5 me-2">add</span>
         <span className="fw-normal">アシスタントを追加する</span>
         <span className="fw-normal">アシスタントを追加する</span>

+ 34 - 6
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.tsx

@@ -8,6 +8,7 @@ import { useCurrentUser } from '~/stores-universal/context';
 import type { AiAssistantAccessScope } from '../../../../interfaces/ai-assistant';
 import type { AiAssistantAccessScope } from '../../../../interfaces/ai-assistant';
 import { AiAssistantShareScope, type AiAssistantHasId } from '../../../../interfaces/ai-assistant';
 import { AiAssistantShareScope, type AiAssistantHasId } from '../../../../interfaces/ai-assistant';
 import { deleteAiAssistant } from '../../../services/ai-assistant';
 import { deleteAiAssistant } from '../../../services/ai-assistant';
+import { useAiAssistantChatSidebar, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
 
 
 import styles from './AiAssistantTree.module.scss';
 import styles from './AiAssistantTree.module.scss';
 
 
@@ -84,6 +85,8 @@ type AiAssistantItemProps = {
   currentUserId?: string;
   currentUserId?: string;
   aiAssistant: AiAssistantHasId;
   aiAssistant: AiAssistantHasId;
   threads: Thread[];
   threads: Thread[];
+  onEditClicked?: (aiAssistantData: AiAssistantHasId) => void;
+  onItemClicked?: (aiAssistantData: AiAssistantHasId) => void;
   onDeleted?: () => void;
   onDeleted?: () => void;
 };
 };
 
 
@@ -91,13 +94,20 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
   currentUserId,
   currentUserId,
   aiAssistant,
   aiAssistant,
   threads,
   threads,
+  onEditClicked,
+  onItemClicked,
   onDeleted,
   onDeleted,
 }) => {
 }) => {
   const [isThreadsOpened, setIsThreadsOpened] = useState(false);
   const [isThreadsOpened, setIsThreadsOpened] = useState(false);
 
 
-  const openChatHandler = useCallback(() => {
-    // TODO: https://redmine.weseek.co.jp/issues/159530
-  }, []);
+  const openManagementModalHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
+    onEditClicked?.(aiAssistantData);
+  }, [onEditClicked]);
+
+  const openChatHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
+    onItemClicked?.(aiAssistantData);
+  }, [onItemClicked]);
+
 
 
   const openThreadsHandler = useCallback(() => {
   const openThreadsHandler = useCallback(() => {
     setIsThreadsOpened(toggle => !toggle);
     setIsThreadsOpened(toggle => !toggle);
@@ -119,14 +129,20 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
   return (
   return (
     <>
     <>
       <li
       <li
-        onClick={openChatHandler}
+        onClick={(e) => {
+          e.stopPropagation();
+          openChatHandler(aiAssistant);
+        }}
         role="button"
         role="button"
         className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1"
         className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1"
       >
       >
         <div className="d-flex justify-content-center">
         <div className="d-flex justify-content-center">
           <button
           <button
             type="button"
             type="button"
-            onClick={openThreadsHandler}
+            onClick={(e) => {
+              e.stopPropagation();
+              openThreadsHandler();
+            }}
             className={`grw-ai-assistant-triangle-btn btn px-0 ${isThreadsOpened ? 'grw-ai-assistant-open' : ''}`}
             className={`grw-ai-assistant-triangle-btn btn px-0 ${isThreadsOpened ? 'grw-ai-assistant-open' : ''}`}
           >
           >
             <div className="d-flex justify-content-center">
             <div className="d-flex justify-content-center">
@@ -148,13 +164,20 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
             <button
             <button
               type="button"
               type="button"
               className="btn btn-link text-secondary p-0 ms-2"
               className="btn btn-link text-secondary p-0 ms-2"
+              onClick={(e) => {
+                e.stopPropagation();
+                openManagementModalHandler(aiAssistant);
+              }}
             >
             >
               <span className="material-symbols-outlined fs-5">edit</span>
               <span className="material-symbols-outlined fs-5">edit</span>
             </button>
             </button>
             <button
             <button
               type="button"
               type="button"
               className="btn btn-link text-secondary p-0"
               className="btn btn-link text-secondary p-0"
-              onClick={deleteAiAssistantHandler}
+              onClick={(e) => {
+                e.stopPropagation();
+                deleteAiAssistantHandler();
+              }}
             >
             >
               <span className="material-symbols-outlined fs-5">delete</span>
               <span className="material-symbols-outlined fs-5">delete</span>
             </button>
             </button>
@@ -183,6 +206,9 @@ type AiAssistantTreeProps = {
 
 
 export const AiAssistantTree: React.FC<AiAssistantTreeProps> = ({ aiAssistants, onDeleted }) => {
 export const AiAssistantTree: React.FC<AiAssistantTreeProps> = ({ aiAssistants, onDeleted }) => {
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
+  const { open: openAiAssistantChatSidebar } = useAiAssistantChatSidebar();
+  const { open: openAiAssistantManagementModal } = useAiAssistantManagementModal();
+
   return (
   return (
     <ul className={`list-group ${moduleClass}`}>
     <ul className={`list-group ${moduleClass}`}>
       {aiAssistants.map(assistant => (
       {aiAssistants.map(assistant => (
@@ -191,6 +217,8 @@ export const AiAssistantTree: React.FC<AiAssistantTreeProps> = ({ aiAssistants,
           currentUserId={currentUser?._id}
           currentUserId={currentUser?._id}
           aiAssistant={assistant}
           aiAssistant={assistant}
           threads={dummyThreads}
           threads={dummyThreads}
+          onEditClicked={openAiAssistantManagementModal}
+          onItemClicked={openAiAssistantChatSidebar}
           onDeleted={onDeleted}
           onDeleted={onDeleted}
         />
         />
       ))}
       ))}

+ 5 - 1
apps/app/src/features/openai/client/services/ai-assistant.ts

@@ -1,4 +1,4 @@
-import { apiv3Post, apiv3Delete } from '~/client/util/apiv3-client';
+import { apiv3Post, apiv3Put, apiv3Delete } from '~/client/util/apiv3-client';
 
 
 import type { UpsertAiAssistantData } from '../../interfaces/ai-assistant';
 import type { UpsertAiAssistantData } from '../../interfaces/ai-assistant';
 
 
@@ -6,6 +6,10 @@ export const createAiAssistant = async(body: UpsertAiAssistantData): Promise<voi
   await apiv3Post('/openai/ai-assistant', body);
   await apiv3Post('/openai/ai-assistant', body);
 };
 };
 
 
+export const updateAiAssistant = async(id: string, body: UpsertAiAssistantData): Promise<void> => {
+  await apiv3Put(`/openai/ai-assistant/${id}`, body);
+};
+
 export const deleteAiAssistant = async(id: string): Promise<void> => {
 export const deleteAiAssistant = async(id: string): Promise<void> => {
   await apiv3Delete(`/openai/ai-assistant/${id}`);
   await apiv3Delete(`/openai/ai-assistant/${id}`);
 };
 };

+ 30 - 5
apps/app/src/features/openai/client/stores/ai-assistant.tsx

@@ -6,7 +6,7 @@ import useSWRImmutable from 'swr/immutable';
 
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 
 
-import { type AccessibleAiAssistantsHasId } from '../../interfaces/ai-assistant';
+import { type AccessibleAiAssistantsHasId, type AiAssistantHasId } from '../../interfaces/ai-assistant';
 
 
 export const AiAssistantManagementModalPageMode = {
 export const AiAssistantManagementModalPageMode = {
   HOME: 'home',
   HOME: 'home',
@@ -20,10 +20,11 @@ type AiAssistantManagementModalPageMode = typeof AiAssistantManagementModalPageM
 type AiAssistantManagementModalStatus = {
 type AiAssistantManagementModalStatus = {
   isOpened: boolean,
   isOpened: boolean,
   pageMode?: AiAssistantManagementModalPageMode,
   pageMode?: AiAssistantManagementModalPageMode,
+  aiAssistantData?: AiAssistantHasId;
 }
 }
 
 
 type AiAssistantManagementModalUtils = {
 type AiAssistantManagementModalUtils = {
-  open(): void
+  open(aiAssistantData?: AiAssistantHasId): void
   close(): void
   close(): void
   changePageMode(pageType: AiAssistantManagementModalPageMode): void
   changePageMode(pageType: AiAssistantManagementModalPageMode): void
 }
 }
@@ -36,10 +37,10 @@ export const useAiAssistantManagementModal = (
 
 
   return {
   return {
     ...swrResponse,
     ...swrResponse,
-    open: useCallback(() => { swrResponse.mutate({ isOpened: true }) }, [swrResponse]),
-    close: useCallback(() => swrResponse.mutate({ isOpened: false }), [swrResponse]),
+    open: useCallback((aiAssistantData) => { swrResponse.mutate({ isOpened: true, aiAssistantData }) }, [swrResponse]),
+    close: useCallback(() => swrResponse.mutate({ isOpened: false, aiAssistantData: undefined }), [swrResponse]),
     changePageMode: useCallback((pageMode: AiAssistantManagementModalPageMode) => {
     changePageMode: useCallback((pageMode: AiAssistantManagementModalPageMode) => {
-      swrResponse.mutate({ isOpened: swrResponse.data?.isOpened ?? false, pageMode });
+      swrResponse.mutate({ isOpened: swrResponse.data?.isOpened ?? false, pageMode, aiAssistantData: swrResponse.data?.aiAssistantData });
     }, [swrResponse]),
     }, [swrResponse]),
   };
   };
 };
 };
@@ -51,3 +52,27 @@ export const useSWRxAiAssistants = (): SWRResponse<AccessibleAiAssistantsHasId,
     ([endpoint]) => apiv3Get(endpoint).then(response => response.data.accessibleAiAssistants),
     ([endpoint]) => apiv3Get(endpoint).then(response => response.data.accessibleAiAssistants),
   );
   );
 };
 };
+
+
+type AiAssistantChatSidebarStatus = {
+  isOpened: boolean,
+  aiAssistantData?: AiAssistantHasId;
+}
+
+type AiAssistantChatSidebarUtils = {
+  open(aiAssistantData: AiAssistantHasId): void
+  close(): void
+}
+
+export const useAiAssistantChatSidebar = (
+    status?: AiAssistantChatSidebarStatus,
+): SWRResponse<AiAssistantChatSidebarStatus, Error> & AiAssistantChatSidebarUtils => {
+  const initialStatus = { isOpened: false };
+  const swrResponse = useSWRStatic<AiAssistantChatSidebarStatus, Error>('AiAssistantChatSidebar', status, { fallbackData: initialStatus });
+
+  return {
+    ...swrResponse,
+    open: useCallback((aiAssistantData: AiAssistantHasId) => { swrResponse.mutate({ isOpened: true, aiAssistantData }) }, [swrResponse]),
+    close: useCallback(() => swrResponse.mutate({ isOpened: false }), [swrResponse]),
+  };
+};

+ 2 - 2
apps/app/src/features/openai/server/models/ai-assistant.ts

@@ -53,7 +53,7 @@ const schema = new Schema<AiAssistantDocument>(
         },
         },
         item: {
         item: {
           type: Schema.Types.ObjectId,
           type: Schema.Types.ObjectId,
-          refPath: 'grantedGroups.type',
+          refPath: 'grantedGroupsForShareScope.type',
           required: true,
           required: true,
           index: true,
           index: true,
         },
         },
@@ -75,7 +75,7 @@ const schema = new Schema<AiAssistantDocument>(
         },
         },
         item: {
         item: {
           type: Schema.Types.ObjectId,
           type: Schema.Types.ObjectId,
-          refPath: 'grantedGroups.type',
+          refPath: 'grantedGroupsForAccessScope.type',
           required: true,
           required: true,
           index: true,
           index: true,
         },
         },

+ 3 - 1
apps/app/src/features/openai/server/services/openai.ts

@@ -653,7 +653,9 @@ class OpenaiService implements IOpenaiService {
           ],
           ],
         },
         },
       ],
       ],
-    });
+    })
+      .populate('grantedGroupsForShareScope.item')
+      .populate('grantedGroupsForAccessScope.item');
 
 
     return {
     return {
       myAiAssistants: assistants.filter(assistant => assistant.owner.toString() === user._id.toString()) ?? [],
       myAiAssistants: assistants.filter(assistant => assistant.owner.toString() === user._id.toString()) ?? [],