diff --git a/examples/chat-ui/src/components/ConversationThread.tsx b/examples/chat-ui/src/components/ConversationThread.tsx index 1912abf..f86a2e4 100644 --- a/examples/chat-ui/src/components/ConversationThread.tsx +++ b/examples/chat-ui/src/components/ConversationThread.tsx @@ -12,6 +12,7 @@ import ModelSelectionModal from './ModelSelectionModal' import McpServerModal from './McpServerModal' import { useAutoscroll } from '../hooks/useAutoscroll' import { useStreamResponse } from '../hooks/useStreamResponse' +import { useConversationUpdater } from '../hooks/useConversationUpdater' import { setApiKey } from '../utils/apiKeys' import { hasApiKey } from '../utils/apiKeys' import ApiKeyModal from './ApiKeyModal' @@ -88,6 +89,11 @@ const ConversationThread: React.FC = ({ } } + const { updateConversation } = useConversationUpdater({ + conversationId, + setConversations + }) + const { isLoading, setIsLoading, streamStarted, controller, streamResponse, aiResponseRef } = useStreamResponse({ conversationId, setConversations, @@ -133,14 +139,10 @@ const ConversationThread: React.FC = ({ setInput('') setIsLoading(true) - setConversations((prev) => { - const updated = [...prev] - const conv = updated.find((c) => c.id === conversationId) - if (conv && conv.messages?.[conv.messages.length - 1] !== userMessage) { - conv.messages.push(userMessage) - } - return updated - }) + updateConversation((conv) => ({ + ...conv, + messages: [...conv.messages, userMessage] + })) await streamResponse([...currentConversation.messages, userMessage]) diff --git a/examples/chat-ui/src/hooks/useConversationUpdater.ts b/examples/chat-ui/src/hooks/useConversationUpdater.ts new file mode 100644 index 0000000..4293f57 --- /dev/null +++ b/examples/chat-ui/src/hooks/useConversationUpdater.ts @@ -0,0 +1,27 @@ +import { type Conversation } from '../types' + +interface UseConversationUpdaterProps { + conversationId?: number + setConversations: React.Dispatch> +} + +export const useConversationUpdater = ({ conversationId, setConversations }: UseConversationUpdaterProps) => { + + const updateConversation = (updater: (conversation: Conversation) => Conversation) => { + setConversations((prev) => { + const conversation = prev.find((c) => c.id === conversationId) + if (!conversation) { + console.error(`Missing conversation for ID ${conversationId}`) + return prev + } + + const updatedConversation = updater(conversation) + + return prev.map((c) => + c.id === conversationId ? updatedConversation : c + ) + }) + } + + return { updateConversation } +} diff --git a/examples/chat-ui/src/hooks/useStreamResponse.ts b/examples/chat-ui/src/hooks/useStreamResponse.ts index 59ce5bf..f5d7550 100644 --- a/examples/chat-ui/src/hooks/useStreamResponse.ts +++ b/examples/chat-ui/src/hooks/useStreamResponse.ts @@ -6,6 +6,7 @@ import { type Message, type Conversation, type UserMessage, type AssistantMessag import { type Model } from '../types/models' import { getApiKey } from '../utils/apiKeys' import { type Tool } from 'use-mcp/react' +import { useConversationUpdater } from './useConversationUpdater' // Debug logging for message operations const debugMessages = (...args: any[]) => { @@ -40,6 +41,11 @@ export const useStreamResponse = ({ const [streamStarted, setStreamStarted] = useState(false) const [controller, setController] = useState(new AbortController()) const aiResponseRef = useRef('') + + const { updateConversation } = useConversationUpdater({ + conversationId, + setConversations + }) // Convert MCP tools to the format expected by the 'ai' package const convertMcpToolsToAiTools = (mcpTools: Tool[]) => { @@ -71,17 +77,17 @@ export const useStreamResponse = ({ } // Check if a model supports reasoning - const supportsReasoning = (model: Model) => { - const reasoningModels = [ - 'qwen/qwen3-32b', - 'qwen-qwq-32b', - 'deepseek-r1-distill-llama-70b', - 'claude-4-opus-20250514', - 'claude-4-sonnet-20250514', - 'claude-3-7-sonnet-20250219', - ] - return reasoningModels.includes(model.modelId) - } + // const supportsReasoning = (model: Model) => { + // const reasoningModels = [ + // 'qwen/qwen3-32b', + // 'qwen-qwq-32b', + // 'deepseek-r1-distill-llama-70b', + // 'claude-4-opus-20250514', + // 'claude-4-sonnet-20250514', + // 'claude-3-7-sonnet-20250219', + // ] + // return reasoningModels.includes(model.modelId) + // } const getModelInstance = (model: Model, apiKey: string) => { let baseModel @@ -156,12 +162,7 @@ export const useStreamResponse = ({ // Use fullStream to get all events including tool calls, results, and text for await (const event of result.fullStream) { console.log({ 'result.fullStream.event': event }) - setConversations((prev) => { - const conv = prev.find((c) => c.id === conversationId) - if (!conv) { - console.error(`Missing conversation for ID ${conversationId}! Ignoring ${JSON.stringify(event)}`) - return prev - } + updateConversation((conv) => { const lastMessage = conv.messages?.at(-1) console.log({ setConversations: event, conv: JSON.parse(JSON.stringify(conv)) }) let updatedMessage: Message | undefined @@ -242,35 +243,30 @@ export const useStreamResponse = ({ } catch (e) { console.error('Error in full stream processing:', e) } - let updated = prev + if (updatedMessage || newMessage) { - updated = prev.map((c) => { - if (c.id !== conversationId) return c - const messages = [...c.messages] + const messages = [...conv.messages] - if (updatedMessage) { - messages.splice(-1, 1, updatedMessage) - } + if (updatedMessage) { + messages.splice(-1, 1, updatedMessage) + } - if (newMessage) { - messages.push(newMessage) - } + if (newMessage) { + messages.push(newMessage) + } - return { ...c, messages } - }) + console.log({ updatedMessage, newMessage }) + return { ...conv, messages } } - console.log({ updatedMessage, newMessage }) - return updated + + return conv }) } // Show final conversation state - setConversations((prev) => { - const conv = prev.find((c) => c.id === conversationId) - if (conv) { - debugMessages('Final conversation state:', JSON.stringify(conv.messages)) - } - return prev + updateConversation((conv) => { + debugMessages('Final conversation state:', JSON.stringify(conv.messages)) + return conv }) // // // Final cleanup - ensure reasoning streaming is stopped