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

Merge pull request #9925 from weseek/imprv/updating-thread-title

imprv: Updating thread title
Yuki Takei 11 месяцев назад
Родитель
Сommit
3b389e9c72

+ 56 - 26
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar.tsx

@@ -18,17 +18,16 @@ import { MessageErrorCode, StreamErrorCode } from '../../../../interfaces/messag
 import type { IThreadRelationHasId } from '../../../../interfaces/thread-relation';
 import {
   useEditorAssistant,
-  useAiAssistantSidebarCloseEffect as useAiAssistantSidebarCloseEffectForEditorAssistant,
   isEditorAssistantFormData,
   type FormData as FormDataForEditorAssistant,
 } from '../../../services/editor-assistant';
 import {
   useKnowledgeAssistant,
   useFetchAndSetMessageDataEffect,
-  useAiAssistantSidebarCloseEffect as useAiAssistantSidebarCloseEffectForKnowledgeAssistant,
   type FormData as FormDataForKnowledgeAssistant,
 } from '../../../services/knowledge-assistant';
 import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
+import { useSWRxThreads } from '../../../stores/thread';
 
 import { MessageCard, type MessageCardRole } from './MessageCard';
 import { ResizableTextarea } from './ResizableTextArea';
@@ -45,7 +44,9 @@ type AiAssistantSidebarSubstanceProps = {
   isEditorAssistant: boolean;
   aiAssistantData?: AiAssistantHasId;
   threadData?: IThreadRelationHasId;
-  closeAiAssistantSidebar: () => void
+  onCloseButtonClicked?: () => void;
+  onNewThreadCreated?: (thread: IThreadRelationHasId) => void;
+  onMessageReceived?: () => void;
 }
 
 const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> = (props: AiAssistantSidebarSubstanceProps) => {
@@ -53,11 +54,12 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     isEditorAssistant,
     aiAssistantData,
     threadData,
-    closeAiAssistantSidebar,
+    onCloseButtonClicked,
+    onNewThreadCreated,
+    onMessageReceived,
   } = props;
 
   // States
-  const [currentThreadId, setCurrentThreadId] = useState<string | undefined>(threadData?.threadId);
   const [messageLogs, setMessageLogs] = useState<MessageLog[]>([]);
   const [generatingAnswerMessage, setGeneratingAnswerMessage] = useState<MessageLog>();
   const [errorMessage, setErrorMessage] = useState<string | undefined>();
@@ -126,16 +128,20 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     return thread;
   }, [aiAssistantData, createThreadForEditorAssistant, createThreadForKnowledgeAssistant, isEditorAssistant]);
 
-  const postMessage = useCallback(async(currentThreadId: string, formData: FormData) => {
+  const postMessage = useCallback(async(threadId: string, formData: FormData) => {
+    if (threadId == null) {
+      throw new Error('threadId is not set');
+    }
+
     if (isEditorAssistant) {
       if (isEditorAssistantFormData(formData)) {
-        const response = await postMessageForEditorAssistant(currentThreadId, formData);
+        const response = await postMessageForEditorAssistant(threadId, formData);
         return response;
       }
       return;
     }
     if (aiAssistantData?._id != null) {
-      const response = await postMessageForKnowledgeAssistant(aiAssistantData._id, currentThreadId, formData);
+      const response = await postMessageForKnowledgeAssistant(aiAssistantData._id, threadId, formData);
       return response;
     }
   }, [aiAssistantData?._id, isEditorAssistant, postMessageForEditorAssistant, postMessageForKnowledgeAssistant]);
@@ -167,16 +173,17 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     setGeneratingAnswerMessage(newAnswerMessage);
 
     // create thread
-    let currentThreadId_ = currentThreadId;
-    if (currentThreadId_ == null) {
+    let threadId = threadData?.threadId;
+    if (threadId == null) {
       try {
-        const thread = await createThread(newUserMessage.content);
-        if (thread == null) {
+        const newThread = await createThread(newUserMessage.content);
+        if (newThread == null) {
           return;
         }
 
-        setCurrentThreadId(thread.threadId);
-        currentThreadId_ = thread.threadId;
+        threadId = newThread.threadId;
+
+        onNewThreadCreated?.(newThread);
       }
       catch (err) {
         logger.error(err.toString());
@@ -186,11 +193,11 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
 
     // post message
     try {
-      if (currentThreadId_ == null) {
+      if (threadId == null) {
         return;
       }
 
-      const response = await postMessage(currentThreadId_, data);
+      const response = await postMessage(threadId, data);
       if (response == null) {
         return;
       }
@@ -226,6 +233,9 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
             setMessageLogs(msgs => [...msgs, generatingAnswerMessage]);
             return undefined;
           });
+
+          // refresh thread data
+          onMessageReceived?.();
           return;
         }
 
@@ -249,10 +259,10 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
                 textValues.push(data.appendedMessage);
               },
               onDetectedDiff: (data) => {
-                console.log('sse diff', { data });
+                logger.debug('sse diff', { data });
               },
               onFinalized: (data) => {
-                console.log('sse finalized', { data });
+                logger.debug('sse finalized', { data });
               },
             });
           }
@@ -287,7 +297,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     }
 
   // eslint-disable-next-line max-len
-  }, [isGenerating, messageLogs, resetForm, currentThreadId, createThread, t, postMessage, form, processMessageForKnowledgeAssistant, processMessageForEditorAssistant, growiCloudUri]);
+  }, [isGenerating, messageLogs, resetForm, threadData?.threadId, createThread, onNewThreadCreated, t, postMessage, form, onMessageReceived, processMessageForKnowledgeAssistant, processMessageForEditorAssistant, growiCloudUri]);
 
   const submit = useCallback((data: FormData) => {
     if (isEditorAssistant) {
@@ -319,10 +329,13 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
   }, [headerIconForEditorAssistant, headerIconForKnowledgeAssistant, isEditorAssistant]);
 
   const headerText = useMemo(() => {
+    if (threadData?.title) {
+      return threadData.title;
+    }
     return isEditorAssistant
       ? headerTextForEditorAssistant
       : headerTextForKnowledgeAssistant;
-  }, [isEditorAssistant, headerTextForEditorAssistant, headerTextForKnowledgeAssistant]);
+  }, [threadData?.title, isEditorAssistant, headerTextForEditorAssistant, headerTextForKnowledgeAssistant]);
 
   const placeHolder = useMemo(() => {
     if (form.formState.isSubmitting) {
@@ -365,14 +378,14 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
           <button
             type="button"
             className="btn btn-link p-0 border-0"
-            onClick={closeAiAssistantSidebar}
+            onClick={onCloseButtonClicked}
           >
             <span className="material-symbols-outlined">close</span>
           </button>
         </div>
         <div className="p-4 d-flex flex-column gap-4 vh-100">
 
-          { currentThreadId != null
+          { threadData != null
             ? (
               <div className="vstack gap-4 pb-2">
                 { messageLogs.map(message => (
@@ -471,7 +484,7 @@ export const AiAssistantSidebar: FC = memo((): JSX.Element => {
   const sidebarRef = useRef<HTMLDivElement>(null);
   const sidebarScrollerRef = useRef<HTMLDivElement>(null);
 
-  const { data: aiAssistantSidebarData, close: closeAiAssistantSidebar } = useAiAssistantSidebar();
+  const { data: aiAssistantSidebarData, close: closeAiAssistantSidebar, refreshThreadData } = useAiAssistantSidebar();
   const { mutate: mutateIsEnableUnifiedMergeView } = useIsEnableUnifiedMergeView();
 
   const aiAssistantData = aiAssistantSidebarData?.aiAssistantData;
@@ -479,8 +492,11 @@ export const AiAssistantSidebar: FC = memo((): JSX.Element => {
   const isOpened = aiAssistantSidebarData?.isOpened;
   const isEditorAssistant = aiAssistantSidebarData?.isEditorAssistant ?? false;
 
-  useAiAssistantSidebarCloseEffectForEditorAssistant();
-  useAiAssistantSidebarCloseEffectForKnowledgeAssistant(sidebarRef);
+  const { data: threads, mutate: mutateThreads } = useSWRxThreads(aiAssistantData?._id);
+
+  const newThreadCreatedHandler = useCallback((thread: IThreadRelationHasId): void => {
+    refreshThreadData(thread);
+  }, [refreshThreadData]);
 
   useEffect(() => {
     if (!aiAssistantSidebarData?.isOpened) {
@@ -488,6 +504,18 @@ export const AiAssistantSidebar: FC = memo((): JSX.Element => {
     }
   }, [aiAssistantSidebarData?.isOpened, mutateIsEnableUnifiedMergeView]);
 
+  // refresh thread data when the data is changed
+  useEffect(() => {
+    if (threads == null) {
+      return;
+    }
+
+    const currentThread = threads.find(t => t.threadId === threadData?.threadId);
+    if (currentThread != null) {
+      refreshThreadData(currentThread);
+    }
+  }, [threads, refreshThreadData, threadData?.threadId]);
+
   if (!isOpened) {
     return <></>;
   }
@@ -507,7 +535,9 @@ export const AiAssistantSidebar: FC = memo((): JSX.Element => {
           isEditorAssistant={isEditorAssistant}
           threadData={threadData}
           aiAssistantData={aiAssistantData}
-          closeAiAssistantSidebar={closeAiAssistantSidebar}
+          onMessageReceived={mutateThreads}
+          onNewThreadCreated={newThreadCreatedHandler}
+          onCloseButtonClicked={closeAiAssistantSidebar}
         />
       </SimpleBar>
     </div>

+ 0 - 11
apps/app/src/features/openai/client/services/editor-assistant.tsx

@@ -413,17 +413,6 @@ export const useEditorAssistant: UseEditorAssistant = () => {
   };
 };
 
-export const useAiAssistantSidebarCloseEffect = (): void => {
-  const { data, close } = useAiAssistantSidebar();
-  const { data: editorMode } = useEditorMode();
-
-  useEffect(() => {
-    if (data?.isEditorAssistant && editorMode !== EditorMode.Editor) {
-      close();
-    }
-  }, [close, data?.isEditorAssistant, editorMode]);
-};
-
 // type guard
 export const isEditorAssistantFormData = (formData): formData is FormData => {
   return 'markdownType' in formData;

+ 65 - 39
apps/app/src/features/openai/client/services/knowledge-assistant.tsx

@@ -1,4 +1,4 @@
-import type { Dispatch, SetStateAction, RefObject } from 'react';
+import type { Dispatch, SetStateAction } from 'react';
 import {
   useCallback, useMemo, useState, useEffect,
 } from 'react';
@@ -13,7 +13,7 @@ import { apiv3Post } from '~/client/util/apiv3-client';
 import { SseMessageSchema, type SseMessage } from '~/features/openai/interfaces/knowledge-assistant/sse-schemas';
 import { handleIfSuccessfullyParsed } from '~/features/openai/utils/handle-if-successfully-parsed';
 
-import type { MessageLog } from '../../interfaces/message';
+import type { MessageLog, MessageWithCustomMetaData } from '../../interfaces/message';
 import type { IThreadRelationHasId } from '../../interfaces/thread-relation';
 import { ThreadType } from '../../interfaces/thread-relation';
 import { AiAssistantChatInitialView } from '../components/AiAssistant/AiAssistantSidebar/AiAssistantChatInitialView';
@@ -253,50 +253,76 @@ export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
 };
 
 
-export const useFetchAndSetMessageDataEffect = (setMessageLogs: Dispatch<SetStateAction<MessageLog[]>>, threadId?: string): void => {
-  const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
-  const { trigger: mutateMessageData } = useSWRMUTxMessages(aiAssistantSidebarData?.aiAssistantData?._id, threadId);
-
-  useEffect(() => {
-    const fetchAndSetMessageData = async() => {
-      const messageData = await mutateMessageData();
-      if (messageData != null) {
-        const normalizedMessageData = messageData.data
-          .reverse()
-          .filter(message => message.metadata?.shouldHideMessage !== 'true');
-
-        setMessageLogs(() => {
-          return normalizedMessageData.map((message, index) => (
-            {
-              id: index.toString(),
-              content: message.content[0].type === 'text' ? message.content[0].text.value : '',
-              isUserMessage: message.role === 'user',
-            }
-          ));
-        });
+// Helper function to transform API message data to MessageLog[]
+const transformApiMessagesToLogs = (
+    apiMessageData: MessageWithCustomMetaData | null | undefined,
+): MessageLog[] => {
+  if (apiMessageData?.data == null || !Array.isArray(apiMessageData.data)) {
+    return [];
+  }
+
+  // Define a type for the items in apiMessageData.data for clarity
+  type ApiMessageItem = (typeof apiMessageData.data)[number];
+
+  return apiMessageData.data
+    .slice() // Create a shallow copy before reversing
+    .reverse()
+    .filter((message: ApiMessageItem) => message.metadata?.shouldHideMessage !== 'true')
+    .map((message: ApiMessageItem): MessageLog => {
+      // Extract the first text content block, if any
+      let messageTextContent = '';
+      const textContentBlock = message.content?.find(contentBlock => contentBlock.type === 'text');
+      if (textContentBlock != null && textContentBlock.type === 'text') {
+        messageTextContent = textContentBlock.text.value;
       }
-    };
-
-    if (threadId != null) {
-      fetchAndSetMessageData();
-    }
 
-  }, [mutateMessageData, setMessageLogs, threadId]);
+      return {
+        id: message.id, // Use the actual message ID from OpenAI
+        content: messageTextContent,
+        isUserMessage: message.role === 'user',
+      };
+    });
 };
 
-export const useAiAssistantSidebarCloseEffect = (sidebarRef: RefObject<HTMLDivElement>): void => {
-  const { data, close } = useAiAssistantSidebar();
+export const useFetchAndSetMessageDataEffect = (
+    setMessageLogs: Dispatch<SetStateAction<MessageLog[]>>,
+    threadId?: string,
+): void => {
+  const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
+  const { trigger: mutateMessageData } = useSWRMUTxMessages(
+    aiAssistantSidebarData?.aiAssistantData?._id,
+    threadId,
+  );
 
   useEffect(() => {
-    const handleClickOutside = (event: MouseEvent) => {
-      if (data?.isOpened && sidebarRef.current && !sidebarRef.current.contains(event.target as Node) && !data.isEditorAssistant) {
-        close();
+    if (threadId == null) {
+      setMessageLogs([]);
+      return; // Early return if no threadId
+    }
+
+    const fetchAndSetLogs = async() => {
+      try {
+        // Assuming mutateMessageData() returns a Promise<MessageWithCustomMetaData | null | undefined>
+        const rawApiMessageData: MessageWithCustomMetaData | null | undefined = await mutateMessageData();
+        const fetchedLogs = transformApiMessagesToLogs(rawApiMessageData);
+
+        setMessageLogs((currentLogs) => {
+          // Preserve current logs if they represent a single, user-submitted message
+          // AND the newly fetched logs are empty (common for new threads).
+          const shouldPreserveCurrentMessage = currentLogs.length === 1
+            && currentLogs[0].isUserMessage
+            && fetchedLogs.length === 0;
+
+          // Update with fetched logs, or preserve current if applicable
+          return shouldPreserveCurrentMessage ? currentLogs : fetchedLogs;
+        });
+      }
+      catch (error) {
+        // console.error('Failed to fetch or process message data:', error); // Optional: for debugging
+        setMessageLogs([]); // Clear logs on error to avoid inconsistent state
       }
     };
 
-    document.addEventListener('mousedown', handleClickOutside);
-    return () => {
-      document.removeEventListener('mousedown', handleClickOutside);
-    };
-  }, [close, data?.isEditorAssistant, data?.isOpened, sidebarRef]);
+    fetchAndSetLogs();
+  }, [threadId, mutateMessageData, setMessageLogs]); // Dependencies
 };

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

@@ -7,7 +7,7 @@ import useSWRImmutable from 'swr/immutable';
 import { apiv3Get } from '~/client/util/apiv3-client';
 
 import { type AccessibleAiAssistantsHasId, type AiAssistantHasId } from '../../interfaces/ai-assistant';
-import type { IThreadRelationHasId } from '../../interfaces/thread-relation';
+import type { IThreadRelationHasId } from '../../interfaces/thread-relation'; // IThreadHasId を削除
 
 export const AiAssistantManagementModalPageMode = {
   HOME: 'home',
@@ -72,6 +72,7 @@ type AiAssistantSidebarUtils = {
   ): void
   openEditor(): void
   close(): void
+  refreshThreadData(threadData?: IThreadRelationHasId): void
 }
 
 export const useAiAssistantSidebar = (
@@ -83,7 +84,7 @@ export const useAiAssistantSidebar = (
   return {
     ...swrResponse,
     openChat: useCallback(
-      (aiAssistantData: AiAssistantHasId, threadData: IThreadRelationHasId) => {
+      (aiAssistantData: AiAssistantHasId, threadData?: IThreadRelationHasId) => {
         swrResponse.mutate({ isOpened: true, aiAssistantData, threadData });
       }, [swrResponse],
     ),
@@ -99,5 +100,12 @@ export const useAiAssistantSidebar = (
         isOpened: false, isEditorAssistant: false, aiAssistantData: undefined, threadData: undefined,
       }), [swrResponse],
     ),
+    refreshThreadData: useCallback(
+      (threadData?: IThreadRelationHasId) => {
+        swrResponse.mutate((currentState = { isOpened: false }) => {
+          return { ...currentState, threadData };
+        });
+      }, [swrResponse],
+    ),
   };
 };

+ 1 - 0
apps/app/src/features/openai/client/stores/thread.tsx

@@ -22,5 +22,6 @@ export const useSWRMUTxThreads = (aiAssistantId?: string): SWRMutationResponse<I
   return useSWRMutation(
     key,
     ([endpoint]) => apiv3Get(endpoint).then(response => response.data.threads),
+    { revalidate: true },
   );
 };

+ 16 - 11
apps/app/src/features/openai/server/services/openai.ts

@@ -120,16 +120,6 @@ class OpenaiService implements IOpenaiService {
   }
 
   async createThread(userId: string, type: ThreadType, aiAssistantId?: string, initialUserMessage?: string): Promise<ThreadRelationDocument> {
-    let threadTitle: string | null = null;
-    if (initialUserMessage != null) {
-      try {
-        threadTitle = await this.generateThreadTitle(initialUserMessage);
-      }
-      catch (err) {
-        logger.error(err);
-      }
-    }
-
     try {
       const aiAssistant = aiAssistantId != null
         ? await AiAssistantModel.findOne({ _id: { $eq: aiAssistantId } }).populate<{ vectorStore: IVectorStore }>('vectorStore')
@@ -141,8 +131,23 @@ class OpenaiService implements IOpenaiService {
         type,
         aiAssistant: aiAssistantId,
         threadId: thread.id,
-        title: threadTitle,
+        title: null, // Initialize title as null
       });
+
+      if (initialUserMessage != null) {
+        // Do not await, run in background
+        this.generateThreadTitle(initialUserMessage)
+          .then(async(generatedTitle) => {
+            if (generatedTitle != null) {
+              threadRelation.title = generatedTitle;
+              await threadRelation.save();
+            }
+          })
+          .catch((err) => {
+            logger.error(`Failed to generate thread title for threadId ${thread.id}:`, err);
+          });
+      }
+
       return threadRelation;
     }
     catch (err) {