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

Merge pull request #10099 from weseek/imprv/167669-make-my-assistants-and-team-assistants-into-accordions-collapsible

imprv: Make my assistants and team assistants into accordions collapsible
Shun Miyazawa 9 месяцев назад
Родитель
Сommit
9f739d37e5

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

@@ -601,7 +601,7 @@
   "default_ai_assistant": {
     "not_set": "Default assistant is not set"
   },
-  "ai_assistant_tree": {
+  "ai_assistant_list": {
     "add_assistant": "Add Assistant",
     "my_assistants": "My Assistants",
     "team_assistants": "Team Assistants",

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

@@ -595,7 +595,7 @@
   "default_ai_assistant": {
     "not_set": "L'assistant par défaut n'est pas configuré"
   },
- "ai_assistant_tree": {
+ "ai_assistant_list": {
     "add_assistant": "Ajouter un assistant",
     "my_assistants": "Mes assistants",
     "team_assistants": "Assistants d'équipe",

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

@@ -633,7 +633,7 @@
   "default_ai_assistant": {
     "not_set": "デフォルトアシスタントが設定されていません"
   },
-  "ai_assistant_tree": {
+  "ai_assistant_list": {
     "add_assistant": "アシスタントを追加する",
     "my_assistants": "マイアシスタント",
     "team_assistants": "チームアシスタント",

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

@@ -590,7 +590,7 @@
   "default_ai_assistant": {
     "not_set": "未设置默认助手"
   },
-  "ai_assistant_tree": {
+  "ai_assistant_list": {
     "add_assistant": "添加助手",
     "my_assistants": "我的助手",
     "team_assistants": "团队助手",

+ 2 - 3
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.module.scss → apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantList.module.scss

@@ -1,5 +1,5 @@
 // == Colors
-.ai-assistant-tree-item :global {
+.ai-assistant-list :global {
   .grw-ai-assistant-actions {
     .btn-link {
       &:hover {
@@ -9,8 +9,7 @@
   }
 }
 
-
-.ai-assistant-tree-item :global {
+.ai-assistant-list :global {
   .list-group-item {
     height: 40px;
     padding-left: 4px;

+ 69 - 32
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.tsx → apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantList.tsx

@@ -1,8 +1,10 @@
+// TODO: https://redmine.weseek.co.jp/issues/167745
 import React, { useCallback, useState } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { getIdStringForRef } from '@growi/core';
 import { useTranslation } from 'react-i18next';
+import { Collapse } from 'reactstrap';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { IThreadRelationHasId } from '~/features/openai/interfaces/thread-relation';
@@ -17,11 +19,11 @@ import { useAiAssistantSidebar, useAiAssistantManagementModal } from '../../../s
 import { useSWRMUTxThreads, useSWRxThreads } from '../../../stores/thread';
 import { getShareScopeIcon } from '../../../utils/get-share-scope-Icon';
 
-import styles from './AiAssistantTree.module.scss';
+import styles from './AiAssistantList.module.scss';
 
-const logger = loggerFactory('growi:openai:client:components:AiAssistantTree');
+const logger = loggerFactory('growi:openai:client:components:AiAssistantList');
 
-const moduleClass = styles['ai-assistant-tree-item'] ?? '';
+const moduleClass = styles['ai-assistant-list'] ?? '';
 
 
 /*
@@ -42,12 +44,12 @@ const ThreadItem: React.FC<ThreadItemProps> = ({
   const deleteThreadHandler = useCallback(async() => {
     try {
       await deleteThread({ aiAssistantId: aiAssistantData._id, threadRelationId: threadData._id });
-      toastSuccess(t('ai_assistant_tree.toaster.thread_deleted_success'));
+      toastSuccess(t('ai_assistant_list.toaster.thread_deleted_success'));
       onThreadDelete();
     }
     catch (err) {
       logger.error(err);
-      toastError(t('ai_assistant_tree.toaster.thread_deleted_failed'));
+      toastError(t('ai_assistant_list.toaster.thread_deleted_failed'));
     }
   }, [aiAssistantData._id, onThreadDelete, t, threadData._id]);
 
@@ -103,7 +105,7 @@ const ThreadItems: React.FC<ThreadItemsProps> = ({ aiAssistantData, onThreadClic
   const { data: threads } = useSWRxThreads(aiAssistantData._id);
 
   if (threads == null || threads.length === 0) {
-    return <p className="text-secondary ms-5">{t('ai_assistant_tree.thread_does_not_exist')}</p>;
+    return <p className="text-secondary ms-5">{t('ai_assistant_list.thread_does_not_exist')}</p>;
   }
 
   return (
@@ -164,11 +166,11 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
     try {
       await setDefaultAiAssistant(aiAssistant._id, !aiAssistant.isDefault);
       onUpdated?.();
-      toastSuccess(t('ai_assistant_tree.toaster.ai_assistant_set_default_success'));
+      toastSuccess(t('ai_assistant_list.toaster.ai_assistant_set_default_success'));
     }
     catch (err) {
       logger.error(err);
-      toastError(t('ai_assistant_tree.toaster.ai_assistant_set_default_failed'));
+      toastError(t('ai_assistant_list.toaster.ai_assistant_set_default_failed'));
     }
   }, [aiAssistant._id, aiAssistant.isDefault, onUpdated, t]);
 
@@ -176,13 +178,13 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
     try {
       await deleteAiAssistant(aiAssistant._id);
       onDeleted?.();
-      toastSuccess('ai_assistant_tree.toaster.assistant_deleted_success');
+      toastSuccess(t('ai_assistant_list.toaster.ai_assistant_deleted_success'));
     }
     catch (err) {
       logger.error(err);
-      toastError('ai_assistant_tree.toaster.assistant_deleted');
+      toastError(t('ai_assistant_list.toaster.ai_assistant_deleted_failed'));
     }
-  }, [aiAssistant._id, onDeleted]);
+  }, [aiAssistant._id, onDeleted, t]);
 
   const isOperable = currentUser?._id != null && getIdStringForRef(aiAssistant.owner) === currentUser._id;
   const isPublicAiAssistantOperable = currentUser?.admin
@@ -198,7 +200,7 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
         role="button"
         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
             type="button"
             onClick={(e) => {
@@ -211,7 +213,7 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
               <span className="material-symbols-outlined fs-5">arrow_right</span>
             </div>
           </button>
-        </div>
+        </div> */}
 
         <div className="d-flex justify-content-center">
           <span className="material-symbols-outlined fs-5">{getShareScopeIcon(aiAssistant.shareScope, aiAssistant.accessScope)}</span>
@@ -261,45 +263,80 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
         </div>
       </li>
 
-      { isThreadsOpened && (
+      {/* { isThreadsOpened && (
         <ThreadItems
           aiAssistantData={aiAssistant}
           onThreadClick={onItemClick}
           onThreadDelete={mutateThreadData}
         />
-      ) }
+      ) } */}
     </>
   );
 };
 
 
 /*
-*  AiAssistantTree
+*  AiAssistantList
 */
-type AiAssistantTreeProps = {
+type AiAssistantListProps = {
+  isTeamAssistant?: boolean;
   aiAssistants: AiAssistantHasId[];
   onUpdated?: () => void;
   onDeleted?: () => void;
+  onCollapsed?: () => void;
 };
 
-export const AiAssistantTree: React.FC<AiAssistantTreeProps> = ({ aiAssistants, onUpdated, onDeleted }) => {
-  const { data: currentUser } = useCurrentUser();
+export const AiAssistantList: React.FC<AiAssistantListProps> = ({
+  isTeamAssistant, aiAssistants, onUpdated, onDeleted, onCollapsed,
+}) => {
+  const { t } = useTranslation();
   const { openChat } = useAiAssistantSidebar();
+  const { data: currentUser } = useCurrentUser();
   const { open: openAiAssistantManagementModal } = useAiAssistantManagementModal();
 
+  const [isCollapsed, setIsCollapsed] = useState(false);
+
+  const toggleCollapse = useCallback(() => {
+    setIsCollapsed((prev) => {
+      if (!prev) {
+        onCollapsed?.();
+      }
+      return !prev;
+    });
+  }, [onCollapsed]);
+
   return (
-    <ul className={`list-group ${moduleClass}`}>
-      {aiAssistants.map(assistant => (
-        <AiAssistantItem
-          key={assistant._id}
-          currentUser={currentUser}
-          aiAssistant={assistant}
-          onEditClick={openAiAssistantManagementModal}
-          onItemClick={openChat}
-          onUpdated={onUpdated}
-          onDeleted={onDeleted}
-        />
-      ))}
-    </ul>
+    <div className={`${moduleClass}`}>
+      <button
+        type="button"
+        className="btn btn-link p-0 text-secondary d-flex align-items-center"
+        aria-expanded={!isCollapsed}
+        onClick={toggleCollapse}
+      >
+        <h3 className="grw-ai-assistant-substance-header fw-bold mb-0 me-1">
+          {t(`ai_assistant_list.${isTeamAssistant ? 'team' : 'my'}_assistants`)}
+        </h3>
+        <span
+          className="material-symbols-outlined"
+        >{`keyboard_arrow_${isCollapsed ? 'up' : 'down'}`}
+        </span>
+      </button>
+
+      <Collapse isOpen={isCollapsed}>
+        <ul className="list-group">
+          {aiAssistants.map(assistant => (
+            <AiAssistantItem
+              key={assistant._id}
+              currentUser={currentUser}
+              aiAssistant={assistant}
+              onEditClick={openAiAssistantManagementModal}
+              onItemClick={openChat}
+              onUpdated={onUpdated}
+              onDeleted={onDeleted}
+            />
+          ))}
+        </ul>
+      </Collapse>
+    </div>
   );
 };

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

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
 
 import { useAiAssistantManagementModal, useSWRxAiAssistants } from '../../../stores/ai-assistant';
 
-import { AiAssistantTree } from './AiAssistantTree';
+import { AiAssistantList } from './AiAssistantList';
 
 import styles from './AiAssistantSubstance.module.scss';
 
@@ -23,33 +23,26 @@ export const AiAssistantContent = (): JSX.Element => {
         onClick={() => open()}
       >
         <span className="material-symbols-outlined fs-5 me-2">add</span>
-        <span className="fw-normal">{t('ai_assistant_tree.add_assistant')}</span>
+        <span className="fw-normal">{t('ai_assistant_list.add_assistant')}</span>
       </button>
 
       <div className="d-flex flex-column gap-4">
         <div>
-          <h3 className="fw-bold grw-ai-assistant-substance-header">
-            {t('ai_assistant_tree.my_assistants')}
-          </h3>
-          {aiAssistants?.myAiAssistants != null && aiAssistants.myAiAssistants.length !== 0 && (
-            <AiAssistantTree
-              onUpdated={mutateAiAssistants}
-              onDeleted={mutateAiAssistants}
-              aiAssistants={aiAssistants.myAiAssistants}
-            />
-          )}
+          <AiAssistantList
+            onUpdated={mutateAiAssistants}
+            onDeleted={mutateAiAssistants}
+            onCollapsed={mutateAiAssistants}
+            aiAssistants={aiAssistants?.myAiAssistants ?? []}
+          />
         </div>
 
         <div>
-          <h3 className="fw-bold grw-ai-assistant-substance-header">
-            {t('ai_assistant_tree.team_assistants')}
-          </h3>
-          {aiAssistants?.teamAiAssistants != null && aiAssistants.teamAiAssistants.length !== 0 && (
-            <AiAssistantTree
-              onUpdated={mutateAiAssistants}
-              aiAssistants={aiAssistants.teamAiAssistants}
-            />
-          )}
+          <AiAssistantList
+            isTeamAssistant
+            onUpdated={mutateAiAssistants}
+            onCollapsed={mutateAiAssistants}
+            aiAssistants={aiAssistants?.teamAiAssistants ?? []}
+          />
         </div>
       </div>
     </div>

+ 3 - 1
apps/app/src/features/openai/server/routes/get-recent-threads.ts

@@ -10,6 +10,7 @@ import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
+import { ThreadType } from '../../interfaces/thread-relation';
 import type { ThreadRelationDocument } from '../models/thread-relation';
 import ThreadRelationModel from '../models/thread-relation';
 import { getOpenaiService } from '../services/openai';
@@ -50,8 +51,9 @@ export const getRecentThreadsFactory: GetRecentThreadsFactory = (crowi) => {
       try {
         const paginateResult: PaginateResult<ThreadRelationDocument> = await ThreadRelationModel.paginate(
           {
-            isActive: true,
             userId: req.user._id,
+            type: ThreadType.KNOWLEDGE,
+            isActive: true,
           },
           {
             page: req.query.page ?? 1,