Преглед изворни кода

Merge pull request #10107 from weseek/feat/167668-display-individual-assistant-thread-history

feat: Show individual assistant thread history
Shun Miyazawa пре 9 месеци
родитељ
комит
867d431ee8

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

@@ -497,6 +497,8 @@
   },
   },
   "sidebar_ai_assistant": {
   "sidebar_ai_assistant": {
     "reference_pages_label": "Reference pages",
     "reference_pages_label": "Reference pages",
+    "recent_chat": "Recent chat",
+    "no_recent_chat": "No recent chat",
     "placeholder": "Ask me anything.",
     "placeholder": "Ask me anything.",
     "knowledge_assistant_placeholder": "Ask me anything.",
     "knowledge_assistant_placeholder": "Ask me anything.",
     "editor_assistant_placeholder": "Can I help you with anything?",
     "editor_assistant_placeholder": "Can I help you with anything?",

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

@@ -492,6 +492,8 @@
   },
   },
   "sidebar_ai_assistant": {
   "sidebar_ai_assistant": {
     "reference_pages_label": "Pages de référence",
     "reference_pages_label": "Pages de référence",
+    "recent_chat": "Chat récent",
+    "no_recent_chat": "Pas de chat récent",
     "knowledge_assistant_placeholder": "Demandez-moi n'importe quoi.",
     "knowledge_assistant_placeholder": "Demandez-moi n'importe quoi.",
     "editor_assistant_placeholder": "Puis-je vous aider ?",
     "editor_assistant_placeholder": "Puis-je vous aider ?",
     "summary_mode_label": "Mode résumé",
     "summary_mode_label": "Mode résumé",

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

@@ -530,6 +530,8 @@
   },
   },
   "sidebar_ai_assistant": {
   "sidebar_ai_assistant": {
     "reference_pages_label": "参照するページ",
     "reference_pages_label": "参照するページ",
+    "recent_chat": "最近のチャット",
+    "no_recent_chat": "チャットがありません",
     "knowledge_assistant_placeholder": "ききたいことを入力してください",
     "knowledge_assistant_placeholder": "ききたいことを入力してください",
     "editor_assistant_placeholder": "お手伝いできることはありますか?",
     "editor_assistant_placeholder": "お手伝いできることはありますか?",
     "summary_mode_label": "要約モード",
     "summary_mode_label": "要約モード",

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

@@ -487,6 +487,8 @@
   },
   },
   "sidebar_ai_assistant": {
   "sidebar_ai_assistant": {
     "reference_pages_label": "参考页面",
     "reference_pages_label": "参考页面",
+    "recent_chat": "最近聊天",
+    "no_recent_chat": "最近没有聊天",
     "knowledge_assistant_placeholder": "问我任何问题。",
     "knowledge_assistant_placeholder": "问我任何问题。",
     "editor_assistant_placeholder": "有什么需要帮忙的吗?",
     "editor_assistant_placeholder": "有什么需要帮忙的吗?",
     "summary_mode_label": "摘要模式",
     "summary_mode_label": "摘要模式",

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

@@ -19,7 +19,12 @@ import loggerFactory from '~/utils/logger';
 import type { SelectedPage } from '../../../../interfaces/selected-page';
 import type { SelectedPage } from '../../../../interfaces/selected-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 { useAiAssistantManagementModal, AiAssistantManagementModalPageMode, useSWRxAiAssistants } from '../../../stores/ai-assistant';
+import {
+  useSWRxAiAssistants,
+  useAiAssistantSidebar,
+  useAiAssistantManagementModal,
+  AiAssistantManagementModalPageMode,
+} from '../../../stores/ai-assistant';
 
 
 import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
 import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
 import { AiAssistantManagementEditPages } from './AiAssistantManagementEditPages';
 import { AiAssistantManagementEditPages } from './AiAssistantManagementEditPages';
@@ -63,6 +68,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   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 { data: aiAssistantSidebarData, refreshAiAssistantData } = useAiAssistantSidebar();
   const { data: pagePathsWithDescendantCount } = useSWRxPagePathsWithDescendantCount(
   const { data: pagePathsWithDescendantCount } = useSWRxPagePathsWithDescendantCount(
     removeGlobPath(aiAssistantManagementModalData?.aiAssistantData?.pagePathPatterns) ?? null,
     removeGlobPath(aiAssistantManagementModalData?.aiAssistantData?.pagePathPatterns) ?? null,
     undefined,
     undefined,
@@ -144,7 +150,10 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
       };
       };
 
 
       if (shouldEdit) {
       if (shouldEdit) {
-        await updateAiAssistant(aiAssistant._id, reqBody);
+        const updatedAiAssistant = await updateAiAssistant(aiAssistant._id, reqBody);
+        if (aiAssistantSidebarData?.aiAssistantData?._id === updatedAiAssistant._id) {
+          refreshAiAssistantData(updatedAiAssistant);
+        }
       }
       }
       else {
       else {
         await createAiAssistant(reqBody);
         await createAiAssistant(reqBody);

+ 24 - 10
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantChatInitialView.tsx

@@ -1,5 +1,10 @@
+import Link from 'next/link';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
+import { removeGlobPath } from '../../../../utils/remove-glob-path';
+
+import { ThreadList } from './ThreadList';
+
 type Props = {
 type Props = {
   description: string,
   description: string,
   pagePathPatterns: string[],
   pagePathPatterns: string[],
@@ -10,26 +15,35 @@ export const AiAssistantChatInitialView: React.FC<Props> = ({ description, pageP
 
 
   return (
   return (
     <>
     <>
-      <p className="fs-6 text-body-secondary mb-0">
-        {description}
-      </p>
+      {description.length !== 0 && (
+        <p className="text-body-secondary mb-0">
+          {description}
+        </p>
+      )}
 
 
       <div>
       <div>
-        <div className="d-flex align-items-center">
-          <p className="text-body-secondary mb-0">{t('sidebar_ai_assistant.reference_pages_label')}</p>
-        </div>
+        <p className="text-body-secondary mb-1">
+          {t('sidebar_ai_assistant.reference_pages_label')}
+        </p>
         <div className="d-flex flex-column gap-1">
         <div className="d-flex flex-column gap-1">
           { pagePathPatterns.map(pagePathPattern => (
           { pagePathPatterns.map(pagePathPattern => (
-            <a
+            <Link
               key={pagePathPattern}
               key={pagePathPattern}
-              href="#"
-              className="fs-6 text-body-secondary text-decoration-none"
+              href={removeGlobPath([pagePathPattern])[0]}
+              className="text-body-secondary text-decoration-underline link-underline-secondary"
             >
             >
               {pagePathPattern}
               {pagePathPattern}
-            </a>
+            </Link>
           ))}
           ))}
         </div>
         </div>
       </div>
       </div>
+
+      <div>
+        <p className="text-body-secondary mb-1">
+          {t('sidebar_ai_assistant.recent_chat')}
+        </p>
+        <ThreadList />
+      </div>
     </>
     </>
   );
   );
 };
 };

+ 7 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/ThreadList.module.scss

@@ -0,0 +1,7 @@
+.thread-list :global {
+  li {
+    &:hover {
+      background-color: var(--bs-secondary-bg) !important;
+    }
+  }
+}

+ 57 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/ThreadList.tsx

@@ -0,0 +1,57 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import type { IThreadRelationHasId } from '~/features/openai/interfaces/thread-relation';
+
+import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
+import { useSWRxThreads } from '../../../stores/thread';
+
+import styles from './ThreadList.module.scss';
+
+const moduleClass = styles['thread-list'] ?? '';
+
+
+export const ThreadList: React.FC = () => {
+  const { t } = useTranslation();
+  const { openChat, data: aiAssistantSidebarData } = useAiAssistantSidebar();
+  const { data: threads } = useSWRxThreads(aiAssistantSidebarData?.aiAssistantData?._id);
+
+  const openChatHandler = useCallback((threadData: IThreadRelationHasId) => {
+    const aiAssistantData = aiAssistantSidebarData?.aiAssistantData;
+    if (aiAssistantData == null) {
+      return;
+    }
+
+    openChat(aiAssistantData, threadData);
+  }, [aiAssistantSidebarData?.aiAssistantData, openChat]);
+
+  if (threads == null || threads.length === 0) {
+    return (
+      <p className="text-body-secondary">
+        {t('sidebar_ai_assistant.no_recent_chat')}
+      </p>
+    );
+  }
+
+  return (
+    <>
+      <ul className={`list-group ${moduleClass}`}>
+        {threads.map(thread => (
+          <li
+            onClick={() => { openChatHandler(thread) }}
+            key={thread._id}
+            role="button"
+            tabIndex={0}
+            className="d-flex align-items-center list-group-item list-group-item-action border-0 rounded-1 bg-body-tertiary mb-2"
+          >
+            <div className="text-body-secondary">
+              <span className="material-symbols-outlined fs-5 me-2">chat</span>
+              <span className="flex-grow-1">{thread.title}</span>
+            </div>
+          </li>
+        ))}
+      </ul>
+    </>
+  );
+};

+ 0 - 131
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantList.tsx

@@ -1,4 +1,3 @@
-// TODO: https://redmine.weseek.co.jp/issues/167745
 import React, { useCallback, useState } from 'react';
 import React, { useCallback, useState } from 'react';
 
 
 import type { IUserHasId } from '@growi/core';
 import type { IUserHasId } from '@growi/core';
@@ -14,112 +13,11 @@ import loggerFactory from '~/utils/logger';
 import { AiAssistantShareScope, type AiAssistantHasId } from '../../../../interfaces/ai-assistant';
 import { AiAssistantShareScope, type AiAssistantHasId } from '../../../../interfaces/ai-assistant';
 import { determineShareScope } from '../../../../utils/determine-share-scope';
 import { determineShareScope } from '../../../../utils/determine-share-scope';
 import { deleteAiAssistant, setDefaultAiAssistant } from '../../../services/ai-assistant';
 import { deleteAiAssistant, setDefaultAiAssistant } from '../../../services/ai-assistant';
-import { deleteThread } from '../../../services/thread';
 import { useAiAssistantSidebar, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
 import { useAiAssistantSidebar, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
-import { useSWRMUTxThreads, useSWRxThreads } from '../../../stores/thread';
 import { getShareScopeIcon } from '../../../utils/get-share-scope-Icon';
 import { getShareScopeIcon } from '../../../utils/get-share-scope-Icon';
 
 
 const logger = loggerFactory('growi:openai:client:components:AiAssistantList');
 const logger = loggerFactory('growi:openai:client:components:AiAssistantList');
 
 
-
-/*
-*  ThreadItem
-*/
-type ThreadItemProps = {
-  threadData: IThreadRelationHasId
-  aiAssistantData: AiAssistantHasId;
-  onThreadClick: (aiAssistantData: AiAssistantHasId, threadData?: IThreadRelationHasId) => void;
-  onThreadDelete: () => void;
-};
-
-const ThreadItem: React.FC<ThreadItemProps> = ({
-  threadData, aiAssistantData, onThreadClick, onThreadDelete,
-}) => {
-  const { t } = useTranslation();
-
-  const deleteThreadHandler = useCallback(async() => {
-    try {
-      await deleteThread({ aiAssistantId: aiAssistantData._id, threadRelationId: threadData._id });
-      toastSuccess(t('ai_assistant_substance.toaster.thread_deleted_success'));
-      onThreadDelete();
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(t('ai_assistant_substance.toaster.thread_deleted_failed'));
-    }
-  }, [aiAssistantData._id, onThreadDelete, t, threadData._id]);
-
-  const openChatHandler = useCallback(() => {
-    onThreadClick(aiAssistantData, threadData);
-  }, [aiAssistantData, onThreadClick, threadData]);
-
-  return (
-    <li
-      role="button"
-      className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1 ps-5"
-      onClick={(e) => {
-        e.stopPropagation();
-        openChatHandler();
-      }}
-    >
-      <div>
-        <span className="material-symbols-outlined fs-5">chat</span>
-      </div>
-
-      <div className="grw-ai-assistant-title-anchor ps-1">
-        <p className="text-truncate m-auto">{threadData?.title ?? 'Untitled thread'}</p>
-      </div>
-
-      <div className="grw-ai-assistant-actions opacity-0 d-flex justify-content-center ">
-        <button
-          type="button"
-          className="btn btn-link text-secondary p-0"
-          onClick={(e) => {
-            e.stopPropagation();
-            deleteThreadHandler();
-          }}
-        >
-          <span className="material-symbols-outlined fs-5">delete</span>
-        </button>
-      </div>
-    </li>
-  );
-};
-
-
-/*
-*  ThreadItems
-*/
-type ThreadItemsProps = {
-  aiAssistantData: AiAssistantHasId;
-  onThreadClick: (aiAssistantData: AiAssistantHasId, threadData?: IThreadRelationHasId) => void;
-  onThreadDelete: () => void;
-};
-
-const ThreadItems: React.FC<ThreadItemsProps> = ({ aiAssistantData, onThreadClick, onThreadDelete }) => {
-  const { t } = useTranslation();
-  const { data: threads } = useSWRxThreads(aiAssistantData._id);
-
-  if (threads == null || threads.length === 0) {
-    return <p className="text-secondary ms-5">{t('ai_assistant_substance.thread_does_not_exist')}</p>;
-  }
-
-  return (
-    <div className="grw-ai-assistant-item-children">
-      {threads.map(thread => (
-        <ThreadItem
-          key={thread._id}
-          threadData={thread}
-          aiAssistantData={aiAssistantData}
-          onThreadClick={onThreadClick}
-          onThreadDelete={onThreadDelete}
-        />
-      ))}
-    </div>
-  );
-};
-
-
 /*
 /*
 *  AiAssistantItem
 *  AiAssistantItem
 */
 */
@@ -140,10 +38,8 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
   onUpdated,
   onUpdated,
   onDeleted,
   onDeleted,
 }) => {
 }) => {
-  const [isThreadsOpened, setIsThreadsOpened] = useState(false);
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistant._id);
 
 
   const openManagementModalHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
   const openManagementModalHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
     onEditClick(aiAssistantData);
     onEditClick(aiAssistantData);
@@ -153,10 +49,6 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
     onItemClick(aiAssistantData);
     onItemClick(aiAssistantData);
   }, [onItemClick]);
   }, [onItemClick]);
 
 
-  const openThreadsHandler = useCallback(async() => {
-    mutateThreadData();
-    setIsThreadsOpened(toggle => !toggle);
-  }, [mutateThreadData]);
 
 
   const setDefaultAiAssistantHandler = useCallback(async() => {
   const setDefaultAiAssistantHandler = useCallback(async() => {
     try {
     try {
@@ -196,21 +88,6 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
         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">
-          <button
-            type="button"
-            onClick={(e) => {
-              e.stopPropagation();
-              openThreadsHandler();
-            }}
-            className={`grw-ai-assistant-triangle-btn btn px-0 ${isThreadsOpened ? 'grw-ai-assistant-open' : ''}`}
-          >
-            <div className="d-flex justify-content-center">
-              <span className="material-symbols-outlined fs-5">arrow_right</span>
-            </div>
-          </button>
-        </div> */}
-
         <div className="d-flex justify-content-center">
         <div className="d-flex justify-content-center">
           <span className="material-symbols-outlined fs-5">{getShareScopeIcon(aiAssistant.shareScope, aiAssistant.accessScope)}</span>
           <span className="material-symbols-outlined fs-5">{getShareScopeIcon(aiAssistant.shareScope, aiAssistant.accessScope)}</span>
         </div>
         </div>
@@ -258,14 +135,6 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
           )}
           )}
         </div>
         </div>
       </li>
       </li>
-
-      {/* { isThreadsOpened && (
-        <ThreadItems
-          aiAssistantData={aiAssistant}
-          onThreadClick={onItemClick}
-          onThreadDelete={mutateThreadData}
-        />
-      ) } */}
     </>
     </>
   );
   );
 };
 };

+ 4 - 3
apps/app/src/features/openai/client/services/ai-assistant.ts

@@ -1,13 +1,14 @@
 import { apiv3Post, apiv3Put, 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, AiAssistantHasId } from '../../interfaces/ai-assistant';
 
 
 export const createAiAssistant = async(body: UpsertAiAssistantData): Promise<void> => {
 export const createAiAssistant = async(body: UpsertAiAssistantData): Promise<void> => {
   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 updateAiAssistant = async(id: string, body: UpsertAiAssistantData): Promise<AiAssistantHasId> => {
+  const res = await apiv3Put<{updatedAiAssistant: AiAssistantHasId}>(`/openai/ai-assistant/${id}`, body);
+  return res.data.updatedAiAssistant;
 };
 };
 
 
 export const setDefaultAiAssistant = async(id: string, isDefault: boolean): Promise<void> => {
 export const setDefaultAiAssistant = async(id: string, isDefault: boolean): Promise<void> => {

+ 8 - 0
apps/app/src/features/openai/client/stores/ai-assistant.tsx

@@ -72,6 +72,7 @@ type AiAssistantSidebarUtils = {
   ): void
   ): void
   openEditor(): void
   openEditor(): void
   close(): void
   close(): void
+  refreshAiAssistantData(aiAssistantData?: AiAssistantHasId): void
   refreshThreadData(threadData?: IThreadRelationHasId): void
   refreshThreadData(threadData?: IThreadRelationHasId): void
 }
 }
 
 
@@ -100,6 +101,13 @@ export const useAiAssistantSidebar = (
         isOpened: false, isEditorAssistant: false, aiAssistantData: undefined, threadData: undefined,
         isOpened: false, isEditorAssistant: false, aiAssistantData: undefined, threadData: undefined,
       }), [swrResponse],
       }), [swrResponse],
     ),
     ),
+    refreshAiAssistantData: useCallback(
+      (aiAssistantData) => {
+        swrResponse.mutate((currentState = { isOpened: false }) => {
+          return { ...currentState, aiAssistantData };
+        });
+      }, [swrResponse],
+    ),
     refreshThreadData: useCallback(
     refreshThreadData: useCallback(
       (threadData?: IThreadRelationHasId) => {
       (threadData?: IThreadRelationHasId) => {
         swrResponse.mutate((currentState = { isOpened: false }) => {
         swrResponse.mutate((currentState = { isOpened: false }) => {

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

@@ -217,7 +217,9 @@ class OpenaiService implements IOpenaiService {
   }
   }
 
 
   async getThreadsByAiAssistantId(aiAssistantId: string, type: ThreadType = ThreadType.KNOWLEDGE): Promise<ThreadRelationDocument[]> {
   async getThreadsByAiAssistantId(aiAssistantId: string, type: ThreadType = ThreadType.KNOWLEDGE): Promise<ThreadRelationDocument[]> {
-    const threadRelations = await ThreadRelationModel.find({ aiAssistant: aiAssistantId, type });
+    const threadRelations = await ThreadRelationModel
+      .find({ aiAssistant: aiAssistantId, type })
+      .sort({ updatedAt: -1 });
     return threadRelations;
     return threadRelations;
   }
   }