UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

326 lines (276 loc) 11.1 kB
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */ // Disable the auto sort key eslint rule to make the code more logic and readable import isEqual from 'fast-deep-equal'; import { SWRResponse, mutate } from 'swr'; import { StateCreator } from 'zustand/vanilla'; import { chainSummaryTitle } from '@/chains/summaryTitle'; import { LOADING_FLAT, THREAD_DRAFT_ID } from '@/const/message'; import { isDeprecatedEdition } from '@/const/version'; import { useClientDataSWR } from '@/libs/swr'; import { chatService } from '@/services/chat'; import { threadService } from '@/services/thread'; import { threadSelectors } from '@/store/chat/selectors'; import { ChatStore } from '@/store/chat/store'; import { useSessionStore } from '@/store/session'; import { useUserStore } from '@/store/user'; import { systemAgentSelectors } from '@/store/user/selectors'; import { ChatMessage, CreateMessageParams, SendThreadMessageParams } from '@/types/message'; import { ThreadItem, ThreadType } from '@/types/topic'; import { merge } from '@/utils/merge'; import { setNamespace } from '@/utils/storeDebug'; import { ThreadDispatch, threadReducer } from './reducer'; const n = setNamespace('thd'); const SWR_USE_FETCH_THREADS = 'SWR_USE_FETCH_THREADS'; export interface ChatThreadAction { // update updateThreadInputMessage: (message: string) => void; refreshThreads: () => Promise<void>; /** * Sends a new thread message to the AI chat system */ sendThreadMessage: (params: SendThreadMessageParams) => Promise<void>; resendThreadMessage: (messageId: string) => Promise<void>; delAndResendThreadMessage: (messageId: string) => Promise<void>; createThread: (params: { message: CreateMessageParams; sourceMessageId: string; topicId: string; type: ThreadType; }) => Promise<{ threadId: string; messageId: string }>; openThreadCreator: (messageId: string) => void; openThreadInPortal: (threadId: string, sourceMessageId: string) => void; closeThreadPortal: () => void; useFetchThreads: (enable: boolean, topicId?: string) => SWRResponse<ThreadItem[]>; summaryThreadTitle: (threadId: string, messages: ChatMessage[]) => Promise<void>; updateThreadTitle: (id: string, title: string) => Promise<void>; removeThread: (id: string) => Promise<void>; switchThread: (id: string) => void; internal_updateThreadTitleInSummary: (id: string, title: string) => void; internal_updateThreadLoading: (id: string, loading: boolean) => void; internal_updateThread: (id: string, data: Partial<ThreadItem>) => Promise<void>; internal_dispatchThread: (payload: ThreadDispatch, action?: any) => void; } export const chatThreadMessage: StateCreator< ChatStore, [['zustand/devtools', never]], [], ChatThreadAction > = (set, get) => ({ updateThreadInputMessage: (message) => { if (isEqual(message, get().threadInputMessage)) return; set({ threadInputMessage: message }, false, n(`updateThreadInputMessage`, message)); }, openThreadCreator: (messageId) => { set( { threadStartMessageId: messageId, portalThreadId: undefined, startToForkThread: true }, false, 'openThreadCreator', ); get().togglePortal(true); }, openThreadInPortal: (threadId, sourceMessageId) => { set( { portalThreadId: threadId, threadStartMessageId: sourceMessageId, startToForkThread: false }, false, 'openThreadInPortal', ); get().togglePortal(true); }, closeThreadPortal: () => { set( { threadStartMessageId: undefined, portalThreadId: undefined, startToForkThread: undefined }, false, 'closeThreadPortal', ); get().togglePortal(false); }, sendThreadMessage: async ({ message }) => { const { internal_coreProcessMessage, activeTopicId, activeId, threadStartMessageId, newThreadMode, portalThreadId, } = get(); if (!activeId || !activeTopicId) return; // if message is empty or no files, then stop if (!message) return; set({ isCreatingThreadMessage: true }, false, n('creatingThreadMessage/start')); const newMessage: CreateMessageParams = { content: message, // if message has attached with files, then add files to message and the agent // files: fileIdList, role: 'user', sessionId: activeId, // if there is activeTopicId,then add topicId to message topicId: activeTopicId, threadId: portalThreadId, }; let parentMessageId: string | undefined = undefined; let tempMessageId: string | undefined = undefined; // if there is no portalThreadId, then create a thread and then append message if (!portalThreadId) { if (!threadStartMessageId) return; // we need to create a temp message for optimistic update tempMessageId = get().internal_createTmpMessage({ ...newMessage, threadId: THREAD_DRAFT_ID, }); get().internal_toggleMessageLoading(true, tempMessageId); const { threadId, messageId } = await get().createThread({ message: newMessage, sourceMessageId: threadStartMessageId, topicId: activeTopicId, type: newThreadMode, }); parentMessageId = messageId; // mark the portal in thread mode await get().refreshThreads(); await get().refreshMessages(); get().openThreadInPortal(threadId, threadStartMessageId); } else { // if there is a thread, just append message // we need to create a temp message for optimistic update tempMessageId = get().internal_createTmpMessage(newMessage); get().internal_toggleMessageLoading(true, tempMessageId); parentMessageId = await get().internal_createMessage(newMessage, { tempMessageId }); } get().internal_toggleMessageLoading(false, tempMessageId); if (!parentMessageId) return; // update assistant update to make it rerank useSessionStore.getState().triggerSessionUpdate(get().activeId); // Get the current messages to generate AI response const messages = threadSelectors.portalAIChats(get()); await internal_coreProcessMessage(messages, parentMessageId, { ragQuery: get().internal_shouldUseRAG() ? message : undefined, threadId: get().portalThreadId, inPortalThread: true, }); set({ isCreatingThreadMessage: false }, false, n('creatingThreadMessage/stop')); // 说明是在新建 thread,需要自动总结标题 if (!portalThreadId) { const portalThread = threadSelectors.currentPortalThread(get()); if (!portalThread) return; const chats = threadSelectors.portalAIChats(get()); await get().summaryThreadTitle(portalThread.id, chats); } }, resendThreadMessage: async (messageId) => { const chats = threadSelectors.portalAIChats(get()); await get().internal_resendMessage(messageId, { messages: chats, threadId: get().portalThreadId, inPortalThread: true, }); }, delAndResendThreadMessage: async (id) => { get().resendThreadMessage(id); get().deleteMessage(id); }, createThread: async ({ message, sourceMessageId, topicId, type }) => { set({ isCreatingThread: true }, false, n('creatingThread/start')); const data = await threadService.createThreadWithMessage({ topicId, sourceMessageId, type, message, }); set({ isCreatingThread: false }, false, n('creatingThread/end')); return data; }, useFetchThreads: (enable, topicId) => useClientDataSWR<ThreadItem[]>( enable && !!topicId && !isDeprecatedEdition ? [SWR_USE_FETCH_THREADS, topicId] : null, async ([, topicId]: [string, string]) => threadService.getThreads(topicId), { onSuccess: (threads) => { const nextMap = { ...get().threadMaps, [topicId!]: threads }; // no need to update map if the topics have been init and the map is the same if (get().topicsInit && isEqual(nextMap, get().topicMaps)) return; set( { threadMaps: nextMap, threadsInit: true }, false, n('useFetchThreads(success)', { topicId }), ); }, }, ), refreshThreads: async () => { const topicId = get().activeTopicId; if (!topicId) return; return mutate([SWR_USE_FETCH_THREADS, topicId]); }, removeThread: async (id) => { await threadService.removeThread(id); await get().refreshThreads(); if (get().activeThreadId === id) { set({ activeThreadId: undefined }); } }, switchThread: async (id) => { set({ activeThreadId: id }, false, n('toggleTopic')); }, updateThreadTitle: async (id, title) => { await get().internal_updateThread(id, { title }); }, summaryThreadTitle: async (threadId, messages) => { const { internal_updateThreadTitleInSummary, internal_updateThreadLoading } = get(); const portalThread = threadSelectors.currentPortalThread(get()); if (!portalThread) return; internal_updateThreadTitleInSummary(threadId, LOADING_FLAT); let output = ''; const threadConfig = systemAgentSelectors.thread(useUserStore.getState()); await chatService.fetchPresetTaskResult({ onError: () => { internal_updateThreadTitleInSummary(threadId, portalThread.title); }, onFinish: async (text) => { await get().internal_updateThread(threadId, { title: text }); }, onLoadingChange: (loading) => { internal_updateThreadLoading(threadId, loading); }, onMessageHandle: (chunk) => { switch (chunk.type) { case 'text': { output += chunk.text; } } internal_updateThreadTitleInSummary(threadId, output); }, params: merge(threadConfig, chainSummaryTitle(messages)), }); }, // Internal process method of the topics internal_updateThreadTitleInSummary: (id, title) => { get().internal_dispatchThread( { type: 'updateThread', id, value: { title } }, 'updateThreadTitleInSummary', ); }, internal_updateThreadLoading: (id, loading) => { set( (state) => { if (loading) return { threadLoadingIds: [...state.threadLoadingIds, id] }; return { threadLoadingIds: state.threadLoadingIds.filter((i) => i !== id) }; }, false, n('updateThreadLoading'), ); }, internal_updateThread: async (id, data) => { get().internal_dispatchThread({ type: 'updateThread', id, value: data }); get().internal_updateThreadLoading(id, true); await threadService.updateThread(id, data); await get().refreshThreads(); get().internal_updateThreadLoading(id, false); }, internal_dispatchThread: (payload, action) => { const nextThreads = threadReducer(threadSelectors.currentTopicThreads(get()), payload); const nextMap = { ...get().threadMaps, [get().activeTopicId!]: nextThreads }; // no need to update map if is the same if (isEqual(nextMap, get().threadMaps)) return; set({ threadMaps: nextMap }, false, action ?? n(`dispatchThread/${payload.type}`)); }, });