diff --git a/examples/chat-ui/src/components/ChatMessage.tsx b/examples/chat-ui/src/components/ChatMessage.tsx index cdf674f..0cc7a2f 100644 --- a/examples/chat-ui/src/components/ChatMessage.tsx +++ b/examples/chat-ui/src/components/ChatMessage.tsx @@ -39,8 +39,13 @@ const ChatMessage: React.FC = ({ }`} > {/* Show reasoning block for assistant messages with reasoning */} - {message.role === "assistant" && "reasoning" in message && message.reasoning && ( - + {message.role === "assistant" && "reasoning" in message && (message.reasoning || message.isReasoningStreaming) && ( + )}
diff --git a/examples/chat-ui/src/components/ReasoningBlock.tsx b/examples/chat-ui/src/components/ReasoningBlock.tsx index 2b20ea8..c8b6d7f 100644 --- a/examples/chat-ui/src/components/ReasoningBlock.tsx +++ b/examples/chat-ui/src/components/ReasoningBlock.tsx @@ -1,11 +1,30 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; interface ReasoningBlockProps { reasoning: string; + isStreaming?: boolean; + startTime?: number; + endTime?: number; } -const ReasoningBlock: React.FC = ({ reasoning }) => { - const [isExpanded, setIsExpanded] = useState(false); +const ReasoningBlock: React.FC = ({ + reasoning, + isStreaming = false, + startTime, + endTime +}) => { + const [isExpanded, setIsExpanded] = useState(isStreaming); // Start expanded when streaming + + // Auto-collapse when streaming finishes + useEffect(() => { + if (!isStreaming && startTime && endTime) { + // Add a small delay before collapsing so user can see completion + const timer = setTimeout(() => { + setIsExpanded(false); + }, 500); + return () => clearTimeout(timer); + } + }, [isStreaming, startTime, endTime]); if (!reasoning || reasoning.trim().length === 0) { return null; @@ -15,36 +34,56 @@ const ReasoningBlock: React.FC = ({ reasoning }) => { 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) + "..."; + // Calculate thinking duration + const duration = startTime && endTime ? (endTime - startTime) / 1000 : null; + + // Format duration nicely + const formatDuration = (seconds: number) => { + if (seconds < 1) { + return `${Math.round(seconds * 1000)}ms`; + } + return `${seconds.toFixed(1)}s`; + }; return (
💭 -
- {displayText} -
- {/*{shouldTruncate && (*/} - {/* {*/} - {/* e.stopPropagation();*/} - {/* toggleExpanded();*/} - {/* }}*/} - {/* >*/} - {/* {isExpanded ? "↑" : "↓"}*/} - {/* */} - {/*)}*/} + + {isExpanded ? ( + // Expanded view: show reasoning content +
+ {reasoning} + {isStreaming && ( + + )} +
+ ) : ( + // Collapsed view: show timing summary +
+ + {isStreaming ? "Thinking..." : + duration ? `Thought for ${formatDuration(duration)}` : + "Thought process"} + +
+ )} + +
diff --git a/examples/chat-ui/src/hooks/useStreamResponse.ts b/examples/chat-ui/src/hooks/useStreamResponse.ts index abf6a4f..d8d0408 100644 --- a/examples/chat-ui/src/hooks/useStreamResponse.ts +++ b/examples/chat-ui/src/hooks/useStreamResponse.ts @@ -134,6 +134,8 @@ export const useStreamResponse = ({ const streamResponse = async (messages: Message[]) => { let aiResponse = '' let reasoning = '' + let reasoningStartTime: number | undefined = undefined + let reasoningEndTime: number | undefined = undefined let assistantMessageIndex = -1 // Track the index of our assistant message let assistantMessageCreated = false // Track if we've created the assistant message yet @@ -200,8 +202,37 @@ export const useStreamResponse = ({ if (event.type === 'reasoning') { debugLog(`[useStreamResponse] Reasoning event:`, event) + + // Track reasoning start time + if (!reasoningStartTime) { + reasoningStartTime = Date.now() + debugLog(`[useStreamResponse] Reasoning started at:`, reasoningStartTime) + } + reasoning += (event as any).textDelta || '' debugLog(`[useStreamResponse] Reasoning content so far:`, reasoning.length, 'chars') + + // Update the assistant message with streaming reasoning in real-time + if (assistantMessageCreated) { + setConversations((prev) => { + const updated = [...prev] + const conv = updated.find((c) => c.id === conversationId) + if (conv && assistantMessageIndex >= 0) { + const assistantMessage = conv.messages[assistantMessageIndex] + if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') { + // Clean the reasoning content as we stream + let cleanedReasoning = reasoning + cleanedReasoning = cleanedReasoning.replace(//g, '').replace(/<\/think>/g, '') + cleanedReasoning = cleanedReasoning.trim() + + assistantMessage.reasoning = cleanedReasoning + assistantMessage.isReasoningStreaming = true + assistantMessage.reasoningStartTime = reasoningStartTime + } + } + return updated + }) + } } else if (event.type === 'tool-call') { const eventKey = `tool-call-${event.toolCallId}` if (processedEvents.has(eventKey)) { @@ -241,6 +272,13 @@ export const useStreamResponse = ({ // Create assistant message on first text delta (after all tool calls/results) if (!assistantMessageCreated) { debugLog(`[useStreamResponse] Creating assistant message after tool calls/results`) + + // Mark reasoning end time when text starts (reasoning is done) + if (reasoningStartTime && !reasoningEndTime) { + reasoningEndTime = Date.now() + debugLog(`[useStreamResponse] Reasoning ended at:`, reasoningEndTime, 'Duration:', reasoningEndTime - reasoningStartTime, 'ms') + } + setConversations((prev) => { const updated = [...prev] const conv = updated.find((c) => c.id === conversationId) @@ -252,6 +290,22 @@ export const useStreamResponse = ({ return updated }) assistantMessageCreated = true + + // Update reasoning end time and stop streaming + if (reasoningEndTime) { + setConversations((prev) => { + const updated = [...prev] + const conv = updated.find((c) => c.id === conversationId) + if (conv && assistantMessageIndex >= 0) { + const assistantMessage = conv.messages[assistantMessageIndex] + if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') { + assistantMessage.isReasoningStreaming = false + assistantMessage.reasoningEndTime = reasoningEndTime + } + } + return updated + }) + } } aiResponse += event.textDelta @@ -320,18 +374,12 @@ export const useStreamResponse = ({ debugLog(`[useStreamResponse] Final aiResponse content:`, JSON.stringify(aiResponse)) debugLog(`[useStreamResponse] Final reasoning content:`, reasoning.length, 'chars:', reasoning.substring(0, 100)) - // Add reasoning to the final message if we collected any - if (reasoning && assistantMessageCreated) { - debugLog(`[useStreamResponse] Adding collected reasoning:`, reasoning.substring(0, 100)) - - // Clean the reasoning content by removing think tags - let cleanedReasoning = reasoning - // Remove opening and closing think tags - cleanedReasoning = cleanedReasoning.replace(//g, '').replace(/<\/think>/g, '') - // Trim any leading/trailing whitespace - cleanedReasoning = cleanedReasoning.trim() - - debugLog(`[useStreamResponse] Cleaned reasoning:`, cleanedReasoning.substring(0, 100)) + // Final cleanup - ensure reasoning streaming is stopped + if (assistantMessageCreated && reasoningStartTime) { + if (!reasoningEndTime) { + reasoningEndTime = Date.now() + debugLog(`[useStreamResponse] Final reasoning end time:`, reasoningEndTime) + } setConversations((prev) => { const updated = [...prev] @@ -339,14 +387,13 @@ export const useStreamResponse = ({ if (conv && assistantMessageIndex >= 0) { const assistantMessage = conv.messages[assistantMessageIndex] if (assistantMessage && hasContent(assistantMessage) && assistantMessage.role === 'assistant') { - assistantMessage.reasoning = cleanedReasoning - debugLog(`[useStreamResponse] Added cleaned reasoning to assistant message at index ${assistantMessageIndex}`) + assistantMessage.isReasoningStreaming = false + assistantMessage.reasoningEndTime = reasoningEndTime + debugLog(`[useStreamResponse] Final reasoning cleanup completed`) } } return updated }) - } else { - debugLog(`[useStreamResponse] No reasoning to add. reasoning length:`, reasoning.length, 'assistantMessageCreated:', assistantMessageCreated) } } catch (error: unknown) { if (controller.signal.aborted) { diff --git a/examples/chat-ui/src/types/index.ts b/examples/chat-ui/src/types/index.ts index 842859e..bcf6afd 100644 --- a/examples/chat-ui/src/types/index.ts +++ b/examples/chat-ui/src/types/index.ts @@ -13,6 +13,9 @@ export interface SystemMessage extends BaseMessage { export interface AssistantMessage extends BaseMessage { role: "assistant"; reasoning?: string; + reasoningStartTime?: number; + reasoningEndTime?: number; + isReasoningStreaming?: boolean; } export interface ToolCallMessage {