From 62000dcdac773ddce816b71e152db7b7c50f586a Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Tue, 24 Jun 2025 16:41:56 +1000 Subject: [PATCH] feat: Add reasoning token support with expandable thinking blocks - Add reasoning field to AssistantMessage type - Create ReasoningBlock component with truncate/expand functionality - Add reasoning-capable models (Qwen QwQ, DeepSeek R1, Qwen 3 32B) - Implement reasoning event handling in stream processing - Enable reasoning format for Groq and Anthropic models - Style reasoning blocks with grey border and reduced font size - Display reasoning above assistant messages when available The reasoning blocks show truncated thinking tokens by default and expand on click to show full reasoning process from models like Qwen3-32b that support step-by-step reasoning output. --- .../chat-ui/src/components/ChatMessage.tsx | 6 + .../src/components/ConversationThread.tsx | 4 +- .../chat-ui/src/components/ReasoningBlock.tsx | 54 ++ .../chat-ui/src/hooks/useStreamResponse.ts | 621 +++++++++--------- examples/chat-ui/src/types/index.ts | 1 + examples/chat-ui/src/types/models.ts | 14 +- 6 files changed, 392 insertions(+), 308 deletions(-) create mode 100644 examples/chat-ui/src/components/ReasoningBlock.tsx diff --git a/examples/chat-ui/src/components/ChatMessage.tsx b/examples/chat-ui/src/components/ChatMessage.tsx index b9984d9..d904b74 100644 --- a/examples/chat-ui/src/components/ChatMessage.tsx +++ b/examples/chat-ui/src/components/ChatMessage.tsx @@ -5,6 +5,7 @@ import rehypeHighlight from "rehype-highlight"; import { type Message } from "../types"; import ToolCallMessage from "./ToolCallMessage"; import ToolResultMessage from "./ToolResultMessage"; +import ReasoningBlock from "./ReasoningBlock"; interface ChatMessageProps { message: Message; @@ -37,6 +38,11 @@ const ChatMessage: React.FC = ({ : " text-zinc-900 w-full" }`} > + {/* Show reasoning block for assistant messages with reasoning */} + {message.role === "assistant" && "reasoning" in message && message.reasoning && ( + + )} +
= ({ } }, [conversations]) - console.log({ currentConversation }) + // console.log({ currentConversation }) return (
@@ -233,7 +233,7 @@ const ConversationThread: React.FC = ({ 🧠 {hasApiKey(selectedModel.provider.id) ? ( - selectedModel.name.toLowerCase() + selectedModel.name.toLowerCase() + ' (' + selectedModel.provider.name.toLowerCase() + ')' ) : ( "none" )} diff --git a/examples/chat-ui/src/components/ReasoningBlock.tsx b/examples/chat-ui/src/components/ReasoningBlock.tsx new file mode 100644 index 0000000..1e0701d --- /dev/null +++ b/examples/chat-ui/src/components/ReasoningBlock.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; + +interface ReasoningBlockProps { + reasoning: string; +} + +const ReasoningBlock: React.FC = ({ reasoning }) => { + const [isExpanded, setIsExpanded] = useState(false); + + if (!reasoning || reasoning.trim().length === 0) { + return null; + } + + const toggleExpanded = () => { + setIsExpanded(!isExpanded); + }; + + // Truncate the reasoning text for the collapsed state + const maxLength = 120; + const shouldTruncate = reasoning.length > maxLength; + const displayText = isExpanded || !shouldTruncate + ? reasoning + : reasoning.substring(0, maxLength) + "..."; + + return ( +
+
+
+ 💭 +
+ {displayText} +
+ {shouldTruncate && ( + + )} +
+
+
+ ); +}; + +export default ReasoningBlock; diff --git a/examples/chat-ui/src/hooks/useStreamResponse.ts b/examples/chat-ui/src/hooks/useStreamResponse.ts index b57d777..bcc4c3c 100644 --- a/examples/chat-ui/src/hooks/useStreamResponse.ts +++ b/examples/chat-ui/src/hooks/useStreamResponse.ts @@ -9,329 +9,340 @@ import { type Tool } from 'use-mcp/react' // Debug logging helper const debugLog = (...args: any[]) => { - if (typeof window !== 'undefined' && localStorage.getItem('USE_MCP_DEBUG') === 'true') { - console.log(...args) - } + if (typeof window !== 'undefined' && localStorage.getItem('USE_MCP_DEBUG') === 'true') { + console.log(...args) + } } // Type guard for messages with content const hasContent = (message: Message): message is UserMessage | AssistantMessage | SystemMessage => { - return 'content' in message + return 'content' in message } interface UseStreamResponseProps { - conversationId?: number - setConversations: React.Dispatch> - scrollToBottom: () => void - selectedModel: Model - onApiKeyRequired: (model: Model) => Promise - mcpTools: Tool[] + conversationId?: number + setConversations: React.Dispatch> + scrollToBottom: () => void + selectedModel: Model + onApiKeyRequired: (model: Model) => Promise + mcpTools: Tool[] } export const useStreamResponse = ({ - conversationId, - setConversations, - scrollToBottom, - selectedModel, - onApiKeyRequired, - mcpTools, + conversationId, + setConversations, + scrollToBottom, + selectedModel, + onApiKeyRequired, + mcpTools, }: UseStreamResponseProps) => { - const [isLoading, setIsLoading] = useState(false) - const [streamStarted, setStreamStarted] = useState(false) - const [controller, setController] = useState(new AbortController()) - const aiResponseRef = useRef('') + const [isLoading, setIsLoading] = useState(false) + const [streamStarted, setStreamStarted] = useState(false) + const [controller, setController] = useState(new AbortController()) + const aiResponseRef = useRef('') - // Convert MCP tools to the format expected by the 'ai' package - const convertMcpToolsToAiTools = (mcpTools: Tool[]) => { - const aiTools: Record = {} - - debugLog(`[useStreamResponse] Converting ${mcpTools.length} MCP tools to AI format`) - - mcpTools.forEach((mcpTool) => { - const schema = mcpTool.inputSchema || { type: 'object', properties: {} } - debugLog(`[useStreamResponse] Converting tool: ${mcpTool.name}`, { description: mcpTool.description, schema }) - - aiTools[mcpTool.name] = tool({ - description: mcpTool.description, - parameters: jsonSchema(schema as any), - execute: async (args: unknown, options?: any) => { - debugLog(`[useStreamResponse] Tool "${mcpTool.name}" execute called with args:`, args) - debugLog(`[useStreamResponse] Tool execute options:`, options) - - try { - // Call the MCP tool using the callTool method attached to the tool - if ('callTool' in mcpTool && typeof mcpTool.callTool === 'function') { - debugLog(`[useStreamResponse] Calling MCP tool "${mcpTool.name}"...`) - const result = await mcpTool.callTool(args as Record) - debugLog(`[useStreamResponse] Tool "${mcpTool.name}" returned result:`, result) - return result - } else { - throw new Error(`Tool ${mcpTool.name} does not have a callable implementation`) - } - } catch (error) { - console.error(`[useStreamResponse] Error calling MCP tool ${mcpTool.name}:`, error) - throw error - } - }, - }) - }) - - debugLog(`[useStreamResponse] Converted tools:`, Object.keys(aiTools)) - return aiTools - } + // Convert MCP tools to the format expected by the 'ai' package + const convertMcpToolsToAiTools = (mcpTools: Tool[]) => { + const aiTools: Record = {} - const getModelInstance = (model: Model, apiKey: string) => { - switch (model.provider.id) { - case 'groq': { - const groqProvider = createGroq({ apiKey }) - return groqProvider(model.modelId) - } - case 'anthropic': { - const anthropicProvider = createAnthropic({ - apiKey, - headers: { - 'anthropic-dangerous-direct-browser-access': 'true', - }, - }) - return anthropicProvider(model.modelId) - } - default: - throw new Error(`Unsupported provider: ${model.provider.id}`) - } - } + debugLog(`[useStreamResponse] Converting ${mcpTools.length} MCP tools to AI format`) - const streamResponse = async (messages: Message[]) => { - let aiResponse = '' - let assistantMessageIndex = -1 // Track the index of our assistant message - let assistantMessageCreated = false // Track if we've created the assistant message yet - - debugLog(`[useStreamResponse] streamResponse called with ${messages.length} messages`) - debugLog(`[useStreamResponse] Messages:`, messages) - debugLog(`[useStreamResponse] Current conversationId:`, conversationId) - debugLog(`[useStreamResponse] MCP tools available:`, mcpTools.length) + mcpTools.forEach((mcpTool) => { + const schema = mcpTool.inputSchema || { type: 'object', properties: {} } + debugLog(`[useStreamResponse] Converting tool: ${mcpTool.name}`, { description: mcpTool.description, schema }) - // Check if API key is available - let apiKey = getApiKey(selectedModel.provider.id) - if (!apiKey) { - const keyProvided = await onApiKeyRequired(selectedModel) - if (!keyProvided) { - return // User cancelled - } - apiKey = getApiKey(selectedModel.provider.id) - if (!apiKey) { - throw new Error('No API key provided') - } - } + aiTools[mcpTool.name] = tool({ + description: mcpTool.description, + parameters: jsonSchema(schema as any), + execute: async (args: unknown, options?: any) => { + debugLog(`[useStreamResponse] Tool "${mcpTool.name}" execute called with args:`, args) + debugLog(`[useStreamResponse] Tool execute options:`, options) - try { - const modelInstance = getModelInstance(selectedModel, apiKey) - - debugLog(`[useStreamResponse] Setting stream started to true`) - setStreamStarted(true) - - // Convert MCP tools to AI package format - const aiTools = convertMcpToolsToAiTools(mcpTools) - - debugLog(`[useStreamResponse] Starting streamText with ${Object.keys(aiTools).length} tools available`) - debugLog(`[useStreamResponse] Messages:`, messages.map(m => ({ - role: m.role, - content: hasContent(m) ? m.content.substring(0, 100) + '...' : 'no content' - }))) - - const result = await streamText({ - model: modelInstance, - messages: messages.filter(msg => - ['user', 'assistant', 'system'].includes(msg.role) && hasContent(msg) - ).map((msg) => { - const contentMsg = msg as UserMessage | AssistantMessage | SystemMessage - return { - role: contentMsg.role, - content: contentMsg.content, - } - }), - tools: Object.keys(aiTools).length > 0 ? aiTools : undefined, - maxSteps: 5, // Allow up to 5 steps for tool calling - abortSignal: controller.signal, - }) - - debugLog(`[useStreamResponse] streamText result obtained, starting to process fullStream`) - - // Use fullStream to get all events including tool calls, results, and text - const processedEvents = new Set() // Track processed events to avoid duplicates - - for await (const event of result.fullStream) { - try { - debugLog(`[useStreamResponse] Full stream event:`, event.type, event) - - if (event.type === 'tool-call') { - const eventKey = `tool-call-${event.toolCallId}` - if (processedEvents.has(eventKey)) { - debugLog(`[useStreamResponse] Skipping duplicate tool call:`, eventKey) - continue - } - processedEvents.add(eventKey) - - debugLog(`[useStreamResponse] Tool call event:`, event) - if (event.toolName) { - setConversations((prev) => { - const updated = [...prev] - const conv = updated.find((c) => c.id === conversationId) - if (conv) { - // Check if this tool call already exists - const existingToolCall = conv.messages.find( - (msg) => msg.role === 'tool-call' && 'callId' in msg && msg.callId === event.toolCallId - ) - if (!existingToolCall) { - conv.messages.push({ - role: 'tool-call', - toolName: event.toolName, - toolArgs: event.args || {}, - callId: event.toolCallId || 'unknown', - }) - } - } - return updated - }) - scrollToBottom() - } else { - console.warn('[useStreamResponse] Tool call event missing toolName:', event) - } - } else if (event.type === 'tool-result') { - const eventKey = `tool-result-${event.toolCallId}` - if (processedEvents.has(eventKey)) { - debugLog(`[useStreamResponse] Skipping duplicate tool result:`, eventKey) - continue - } - processedEvents.add(eventKey) - - debugLog(`[useStreamResponse] Tool result event:`, event) - if (event.toolName) { - setConversations((prev) => { - const updated = [...prev] - const conv = updated.find((c) => c.id === conversationId) - if (conv) { - // Check if this tool result already exists - const existingToolResult = conv.messages.find( - (msg) => msg.role === 'tool-result' && 'callId' in msg && msg.callId === event.toolCallId - ) - if (!existingToolResult) { - conv.messages.push({ - role: 'tool-result', - toolName: event.toolName, - toolArgs: event.args || {}, - toolResult: event.result, - callId: event.toolCallId || 'unknown', - }) - } - } - return updated - }) - scrollToBottom() - } else { - console.warn('[useStreamResponse] Tool result event missing toolName:', event) - } - } else if (event.type === 'text-delta') { - debugLog(`[useStreamResponse] Text delta:`, event.textDelta) - - // Create assistant message on first text delta (after all tool calls/results) - if (!assistantMessageCreated) { - debugLog(`[useStreamResponse] Creating assistant message after tool calls/results`) - setConversations((prev) => { - const updated = [...prev] - const conv = updated.find((c) => c.id === conversationId) - if (conv) { - conv.messages.push({ role: 'assistant', content: '' }) - assistantMessageIndex = conv.messages.length - 1 - debugLog(`[useStreamResponse] Created assistant message at index ${assistantMessageIndex}`) - } - return updated - }) - assistantMessageCreated = true - } - - aiResponse += event.textDelta - aiResponseRef.current = aiResponse - - //custom extraction of tag - const titleMatch = aiResponse.match(/(.*?)<\/chat-title>/) - if (titleMatch) { - const title = titleMatch[1].trim() - debugLog(`[useStreamResponse] Extracted title: ${title}`) - setConversations((prev) => { - const updated = [...prev] - const conv = updated.find((c) => c.id === conversationId) - if (conv) { - conv.title = title - } - return updated - }) - aiResponse = aiResponse.replace(/.*?<\/chat-title>/, '').trim() - } - - setConversations((prev) => { - const updated = [...prev] - const conv = updated.find((c) => c.id === conversationId) - if (conv) { - debugLog(`[useStreamResponse] Updating conversation message content (length: ${aiResponse.length})`) - // Update the specific assistant message we created, not just the last message - const assistantMessage = conv.messages[assistantMessageIndex] - if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') { - assistantMessage.content = aiResponse - debugLog(`[useStreamResponse] Updated assistant message at index ${assistantMessageIndex}`) - } else { - debugLog(`[useStreamResponse] Could not find assistant message at index ${assistantMessageIndex}`, assistantMessage) - } - } else { - debugLog(`[useStreamResponse] Could not find conversation with id: ${conversationId}`) - } - return updated + try { + // Call the MCP tool using the callTool method attached to the tool + if ('callTool' in mcpTool && typeof mcpTool.callTool === 'function') { + debugLog(`[useStreamResponse] Calling MCP tool "${mcpTool.name}"...`) + const result = await mcpTool.callTool(args as Record) + debugLog(`[useStreamResponse] Tool "${mcpTool.name}" returned result:`, result) + return result + } else { + throw new Error(`Tool ${mcpTool.name} does not have a callable implementation`) + } + } catch (error) { + console.error(`[useStreamResponse] Error calling MCP tool ${mcpTool.name}:`, error) + throw error + } + }, }) + }) - scrollToBottom() - } - } catch (e) { - console.error('[useStreamResponse] Error in full stream processing:', e) - } - } - - debugLog(`[useStreamResponse] Finished processing full stream. Final response length: ${aiResponse.length}`) - } catch (error: unknown) { - if (controller.signal.aborted) { - debugLog('[useStreamResponse] Stream aborted') - } else { - console.error('[useStreamResponse] Error generating response:', error) - // Log additional error details - if (error instanceof Error) { - console.error('[useStreamResponse] Error stack:', error.stack) - console.error('[useStreamResponse] Error message:', error.message) - } - // Remove the assistant message if there was an error and we created one - if (assistantMessageCreated) { - setConversations((prev) => { - const updated = [...prev] - const conv = updated.find((c) => c.id === conversationId) - if (conv && assistantMessageIndex >= 0 && conv.messages[assistantMessageIndex]?.role === 'assistant') { - debugLog('[useStreamResponse] Removing assistant message due to error') - conv.messages.splice(assistantMessageIndex, 1) - } - return updated - }) - } - } - } finally { - debugLog('[useStreamResponse] Cleaning up stream state') - setStreamStarted(false) - setController(new AbortController()) + debugLog(`[useStreamResponse] Converted tools:`, Object.keys(aiTools)) + return aiTools } - return aiResponse - } - return { - isLoading, - setIsLoading, - streamStarted, - controller, - streamResponse, - aiResponseRef, - } + // 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 getModelInstance = (model: Model, apiKey: string) => { + switch (model.provider.id) { + case 'groq': { + const groqProvider = createGroq({ apiKey }) + return groqProvider(model.modelId) + } + case 'anthropic': { + const anthropicProvider = createAnthropic({ + apiKey, + headers: { + 'anthropic-dangerous-direct-browser-access': 'true', + }, + }) + return anthropicProvider(model.modelId) + } + default: + throw new Error(`Unsupported provider: ${model.provider.id}`) + } + } + + const streamResponse = async (messages: Message[]) => { + let aiResponse = '' + let reasoning = '' + let assistantMessageIndex = -1 // Track the index of our assistant message + let assistantMessageCreated = false // Track if we've created the assistant message yet + + debugLog(`[useStreamResponse] streamResponse called with ${messages.length} messages`) + debugLog(`[useStreamResponse] Messages:`, messages) + debugLog(`[useStreamResponse] Current conversationId:`, conversationId) + debugLog(`[useStreamResponse] MCP tools available:`, mcpTools.length) + + // Check if API key is available + let apiKey = getApiKey(selectedModel.provider.id) + if (!apiKey) { + const keyProvided = await onApiKeyRequired(selectedModel) + if (!keyProvided) { + return // User cancelled + } + apiKey = getApiKey(selectedModel.provider.id) + if (!apiKey) { + throw new Error('No API key provided') + } + } + + try { + const modelInstance = getModelInstance(selectedModel, apiKey) + + debugLog(`[useStreamResponse] Setting stream started to true`) + setStreamStarted(true) + + // Convert MCP tools to AI package format + const aiTools = convertMcpToolsToAiTools(mcpTools) + + debugLog(`[useStreamResponse] Starting streamText with ${Object.keys(aiTools).length} tools available`) + debugLog(`[useStreamResponse] Messages:`, messages.map(m => ({ + role: m.role, + content: hasContent(m) ? m.content.substring(0, 100) + '...' : 'no content' + }))) + + // Prepare stream options + const streamOptions: any = { + model: modelInstance, + messages: messages.filter(msg => + ['user', 'assistant', 'system'].includes(msg.role) && hasContent(msg) + ).map((msg) => { + const contentMsg = msg as UserMessage | AssistantMessage | SystemMessage + return { + role: contentMsg.role, + content: contentMsg.content, + } + }), + tools: Object.keys(aiTools).length > 0 ? aiTools : undefined, + maxSteps: 5, // Allow up to 5 steps for tool calling + abortSignal: controller.signal, + } + + // Add reasoning parameters for supported models + if (supportsReasoning(selectedModel)) { + debugLog(`[useStreamResponse] Enabling reasoning for model: ${selectedModel.modelId}`) + + if (selectedModel.provider.id === 'groq') { + // For Groq models, use reasoning_format parameter + streamOptions.experimental_providerOptions = { + groq: { + reasoning_format: 'parsed' + } + } + } else if (selectedModel.provider.id === 'anthropic') { + // For Anthropic models, enable thinking + streamOptions.experimental_providerOptions = { + anthropic: { + thinking: { type: 'enabled', budgetTokens: 12000 } + } + } + } + } + + const result = await streamText(streamOptions) + + debugLog(`[useStreamResponse] streamText result obtained, starting to process fullStream`) + + // Use fullStream to get all events including tool calls, results, and text + const processedEvents = new Set() // Track processed events to avoid duplicates + + for await (const event of result.fullStream) { + try { + debugLog(`[useStreamResponse] Full stream event:`, event.type, event) + + if (event.type === 'reasoning') { + debugLog(`[useStreamResponse] Reasoning event:`, event) + reasoning += (event as any).textDelta || '' + } else if (event.type === 'tool-call') { + const eventKey = `tool-call-${event.toolCallId}` + if (processedEvents.has(eventKey)) { + debugLog(`[useStreamResponse] Skipping duplicate tool call:`, eventKey) + continue + } + processedEvents.add(eventKey) + + debugLog(`[useStreamResponse] Tool call event:`, event) + if (event.toolName) { + setConversations((prev) => { + const updated = [...prev] + const conv = updated.find((c) => c.id === conversationId) + if (conv) { + // Check if this tool call already exists + const existingToolCall = conv.messages.find( + (msg) => msg.role === 'tool-call' && 'callId' in msg && msg.callId === event.toolCallId + ) + if (!existingToolCall) { + conv.messages.push({ + role: 'tool-call', + toolName: event.toolName, + toolArgs: event.args || {}, + callId: event.toolCallId || 'unknown', + }) + } + } + return updated + }) + scrollToBottom() + } else { + console.warn('[useStreamResponse] Tool call event missing toolName:', event) + } + } else if (event.type === 'text-delta') { + debugLog(`[useStreamResponse] Text delta:`, event.textDelta) + + // Create assistant message on first text delta (after all tool calls/results) + if (!assistantMessageCreated) { + debugLog(`[useStreamResponse] Creating assistant message after tool calls/results`) + setConversations((prev) => { + const updated = [...prev] + const conv = updated.find((c) => c.id === conversationId) + if (conv) { + conv.messages.push({ role: 'assistant', content: '', reasoning: reasoning || undefined }) + assistantMessageIndex = conv.messages.length - 1 + debugLog(`[useStreamResponse] Created assistant message at index ${assistantMessageIndex}`) + } + return updated + }) + assistantMessageCreated = true + } + + aiResponse += event.textDelta + aiResponseRef.current = aiResponse + + //custom extraction of tag + const titleMatch = aiResponse.match(/(.*?)<\/chat-title>/) + if (titleMatch) { + const title = titleMatch[1].trim() + debugLog(`[useStreamResponse] Extracted title: ${title}`) + setConversations((prev) => { + const updated = [...prev] + const conv = updated.find((c) => c.id === conversationId) + if (conv) { + conv.title = title + } + return updated + }) + aiResponse = aiResponse.replace(/.*?<\/chat-title>/, '').trim() + } + + setConversations((prev) => { + const updated = [...prev] + const conv = updated.find((c) => c.id === conversationId) + if (conv) { + debugLog(`[useStreamResponse] Updating conversation message content (length: ${aiResponse.length})`) + // Update the specific assistant message we created, not just the last message + const assistantMessage = conv.messages[assistantMessageIndex] + if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') { + assistantMessage.content = aiResponse + // Update reasoning if available + if (reasoning) { + assistantMessage.reasoning = reasoning + } + debugLog(`[useStreamResponse] Updated assistant message at index ${assistantMessageIndex}`) + } else { + debugLog(`[useStreamResponse] Could not find assistant message at index ${assistantMessageIndex}`, assistantMessage) + } + } else { + debugLog(`[useStreamResponse] Could not find conversation with id: ${conversationId}`) + } + return updated + }) + + scrollToBottom() + } + } catch (e) { + console.error('[useStreamResponse] Error in full stream processing:', e) + } + } + + debugLog(`[useStreamResponse] Finished processing full stream. Final response length: ${aiResponse.length}`) + } catch (error: unknown) { + if (controller.signal.aborted) { + debugLog('[useStreamResponse] Stream aborted') + } else { + console.error('[useStreamResponse] Error generating response:', error) + // Log additional error details + if (error instanceof Error) { + console.error('[useStreamResponse] Error stack:', error.stack) + console.error('[useStreamResponse] Error message:', error.message) + } + // Remove the assistant message if there was an error and we created one + if (assistantMessageCreated) { + setConversations((prev) => { + const updated = [...prev] + const conv = updated.find((c) => c.id === conversationId) + if (conv && assistantMessageIndex >= 0 && conv.messages[assistantMessageIndex]?.role === 'assistant') { + debugLog('[useStreamResponse] Removing assistant message due to error') + conv.messages.splice(assistantMessageIndex, 1) + } + return updated + }) + } + } + } finally { + debugLog('[useStreamResponse] Cleaning up stream state') + setStreamStarted(false) + setController(new AbortController()) + } + return aiResponse + } + + return { + isLoading, + setIsLoading, + streamStarted, + controller, + streamResponse, + aiResponseRef, + } } diff --git a/examples/chat-ui/src/types/index.ts b/examples/chat-ui/src/types/index.ts index 79cbe7c..842859e 100644 --- a/examples/chat-ui/src/types/index.ts +++ b/examples/chat-ui/src/types/index.ts @@ -12,6 +12,7 @@ export interface SystemMessage extends BaseMessage { export interface AssistantMessage extends BaseMessage { role: "assistant"; + reasoning?: string; } export interface ToolCallMessage { diff --git a/examples/chat-ui/src/types/models.ts b/examples/chat-ui/src/types/models.ts index f67ee7b..37af698 100644 --- a/examples/chat-ui/src/types/models.ts +++ b/examples/chat-ui/src/types/models.ts @@ -42,10 +42,22 @@ export const availableModels: Model[] = [ }, { id: 'qwen-3-32b', - name: 'Qwen 3 32B', + name: 'Qwen 3 32B (Reasoning)', provider: providers.groq, modelId: 'qwen/qwen3-32b' }, + { + id: 'qwen-qwq-32b', + name: 'Qwen QwQ 32B (Reasoning)', + provider: providers.groq, + modelId: 'qwen-qwq-32b' + }, + { + id: 'deepseek-r1-distill-llama-70b', + name: 'DeepSeek R1 Distil Llama 70B (Reasoning)', + provider: providers.groq, + modelId: 'deepseek-r1-distill-llama-70b' + }, { id: 'claude-3-5-sonnet-20241022', name: 'Claude 4 Sonnet',