From ce70b67459ae9ee5338c342b7f93dc50ad12bf3a Mon Sep 17 00:00:00 2001 From: Henry Li Date: Fri, 16 Jan 2026 19:51:39 +0800 Subject: [PATCH] refactor: move biz logic to core --- .../app/workspace/chats/[thread_id]/page.tsx | 49 ++------- .../message-list/message-list-item.tsx | 13 +++ .../workspace/message-list/message-list.tsx | 2 +- .../components/workspace/recent-chat-list.tsx | 4 +- frontend/src/core/threads/hooks.ts | 102 +++++++++++------- 5 files changed, 89 insertions(+), 81 deletions(-) diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 2970ac4..0cc15bf 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -1,11 +1,8 @@ "use client"; -import { type HumanMessage } from "@langchain/core/messages"; -import { useQueryClient } from "@tanstack/react-query"; import { useParams, useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; -import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { BreadcrumbItem } from "@/components/ui/breadcrumb"; import { InputBox } from "@/components/workspace/input-box"; import { MessageList } from "@/components/workspace/message-list/message-list"; @@ -17,13 +14,12 @@ import { } from "@/components/workspace/workspace-container"; import { useLocalSettings } from "@/core/settings"; import { type AgentThread } from "@/core/threads"; -import { useThreadStream } from "@/core/threads/hooks"; -import { titleOfThread } from "@/core/threads/utils"; +import { useSubmitThread, useThreadStream } from "@/core/threads/hooks"; +import { pathOfThread, titleOfThread } from "@/core/threads/utils"; import { uuid } from "@/core/utils/uuid"; export default function ChatPage() { const router = useRouter(); - const queryClient = useQueryClient(); const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); const isNewThread = useMemo( () => threadIdFromPath === "new", @@ -43,40 +39,15 @@ export default function ChatPage() { isNewThread, threadId, }); - const handleSubmit = useCallback( - async (message: PromptInputMessage) => { - const text = message.text.trim(); - if (isNewThread) { - router.replace(`/workspace/chats/${threadId}`); - } - await thread.submit( - { - messages: [ - { - type: "human", - content: [ - { - type: "text", - text, - }, - ], - }, - ] as HumanMessage[], - }, - { - threadId: isNewThread ? threadId! : undefined, - streamSubgraphs: true, - streamResumable: true, - context: { - ...threadContext, - thread_id: threadId!, - }, - }, - ); - void queryClient.invalidateQueries({ queryKey: ["threads", "search"] }); + const handleSubmit = useSubmitThread({ + isNewThread, + threadId, + thread, + threadContext, + afterSubmit() { + router.push(pathOfThread(threadId!)); }, - [isNewThread, queryClient, router, thread, threadContext, threadId], - ); + }); const handleStop = useCallback(async () => { await thread.stop(); }, [thread]); diff --git a/frontend/src/components/workspace/message-list/message-list-item.tsx b/frontend/src/components/workspace/message-list/message-list-item.tsx index 8fa238f..1b8569f 100644 --- a/frontend/src/components/workspace/message-list/message-list-item.tsx +++ b/frontend/src/components/workspace/message-list/message-list-item.tsx @@ -5,6 +5,7 @@ import { Message as AIElementMessage, MessageContent as AIElementMessageContent, MessageResponse as AIElementMessageResponse, + MessageToolbar, } from "@/components/ai-elements/message"; import { extractContentFromMessage, @@ -15,6 +16,7 @@ import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { cn } from "@/lib/utils"; import { MessageGroup } from "./message-group"; +import { CopyButton } from "../copy-button"; export function MessageListItem({ className, @@ -38,6 +40,17 @@ export function MessageListItem({ messagesInGroup={messagesInGroup} isLoading={isLoading} /> + +
+ +
+
); } diff --git a/frontend/src/components/workspace/message-list/message-list.tsx b/frontend/src/components/workspace/message-list/message-list.tsx index 9d5e72f..520ba31 100644 --- a/frontend/src/components/workspace/message-list/message-list.tsx +++ b/frontend/src/components/workspace/message-list/message-list.tsx @@ -31,7 +31,7 @@ export function MessageList({ {groupMessages( thread.messages, - (groupedMessages, groupIndex, isLastGroup) => { + (groupedMessages) => { if (groupedMessages[0] && hasContent(groupedMessages[0])) { const message = groupedMessages[0]; return ( diff --git a/frontend/src/components/workspace/recent-chat-list.tsx b/frontend/src/components/workspace/recent-chat-list.tsx index d7ce2dd..0b5c98f 100644 --- a/frontend/src/components/workspace/recent-chat-list.tsx +++ b/frontend/src/components/workspace/recent-chat-list.tsx @@ -57,7 +57,7 @@ export function RecentChatList() {
{threads.map((thread) => { - const isActive = pathOfThread(thread, false) === pathname; + const isActive = pathOfThread(thread.thread_id) === pathname; return ( {titleOfThread(thread)} diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index f0c642a..d472cac 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -22,16 +22,37 @@ export function useThreadStream({ threadId: string | null | undefined; }) { const queryClient = useQueryClient(); - return useStream({ + const thread = useStream({ client: getAPIClient(), assistantId: "lead_agent", threadId: isNewThread ? undefined : threadId, reconnectOnMount: true, fetchStateHistory: true, - onFinish() { - void queryClient.invalidateQueries({ queryKey: ["threads", "search"] }); + onFinish(state) { + // void queryClient.invalidateQueries({ queryKey: ["threads", "search"] }); + queryClient.setQueriesData( + { + queryKey: ["threads", "search"], + exact: false, + }, + (oldData: Array) => { + return oldData.map((t) => { + if (t.thread_id === threadId) { + return { + ...t, + values: { + ...t.values, + title: state.values.title, + }, + }; + } + return t; + }); + }, + ); }, }); + return thread; } export function useSubmitThread({ @@ -39,43 +60,47 @@ export function useSubmitThread({ thread, threadContext, isNewThread, - message, + afterSubmit, }: { isNewThread: boolean; - threadId: string; + threadId: string | null | undefined; thread: UseStream; - threadContext: AgentThreadContext; - message: PromptInputMessage; + threadContext: Omit; + afterSubmit?: () => void; }) { const queryClient = useQueryClient(); - const text = message.text.trim(); - const callback = useCallback(async () => { - await thread.submit( - { - messages: [ - { - type: "human", - content: [ - { - type: "text", - text, - }, - ], - }, - ] as HumanMessage[], - }, - { - threadId: isNewThread ? threadId : undefined, - streamSubgraphs: true, - streamResumable: true, - context: { - ...threadContext, - thread_id: threadId, + const callback = useCallback( + async (message: PromptInputMessage) => { + const text = message.text.trim(); + await thread.submit( + { + messages: [ + { + type: "human", + content: [ + { + type: "text", + text, + }, + ], + }, + ] as HumanMessage[], }, - }, - ); - void queryClient.invalidateQueries({ queryKey: ["threads", "search"] }); - }, [queryClient, thread, threadContext, threadId, isNewThread, text]); + { + threadId: isNewThread ? threadId! : undefined, + streamSubgraphs: true, + streamResumable: true, + context: { + ...threadContext, + thread_id: threadId, + }, + }, + ); + void queryClient.invalidateQueries({ queryKey: ["threads", "search"] }); + afterSubmit?.(); + }, + [thread, isNewThread, threadId, threadContext, queryClient, afterSubmit], + ); return callback; } @@ -86,12 +111,11 @@ export function useThreads( sortOrder: "desc", }, ) { - const langGraphClient = getAPIClient(); + const apiClient = getAPIClient(); return useQuery({ queryKey: ["threads", "search", params], queryFn: async () => { - const response = - await langGraphClient.threads.search(params); + const response = await apiClient.threads.search(params); return response as AgentThread[]; }, }); @@ -99,10 +123,10 @@ export function useThreads( export function useDeleteThread() { const queryClient = useQueryClient(); - const langGraphClient = getAPIClient(); + const apiClient = getAPIClient(); return useMutation({ mutationFn: async ({ threadId }: { threadId: string }) => { - await langGraphClient.threads.delete(threadId); + await apiClient.threads.delete(threadId); }, onSuccess(_, { threadId }) { queryClient.setQueriesData(