Handling reasoning then messages at least

This commit is contained in:
Glen Maddern 2025-06-25 20:20:14 +10:00
commit 5e41222c63
4 changed files with 87 additions and 168 deletions

View file

@ -33,7 +33,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
// Handle regular messages (user, assistant, system)
return (
<div className={`flex justify-end`}>
<div className={`max-w-[80%] rounded-2xl px-3 py-2 shadow bg-white text-black`}>
<div className={`max-w-[80%] rounded-2xl px-5 py-3 shadow bg-white text-black`}>
<div className="prose prose-zinc">
<ReactMarkdown
remarkPlugins={[remarkGfm]}

View file

@ -7,76 +7,6 @@ interface ReasoningBlockProps {
endTime?: number
}
const ReasoningBlock: React.FC<ReasoningBlockProps> = ({ reasoning, isStreaming = false, startTime, endTime }) => {
const [isExpanded, setIsExpanded] = useState(isStreaming) // Start expanded when streaming
// Auto-collapse instantly when streaming finishes
useEffect(() => {
if (!isStreaming && startTime && endTime) {
setIsExpanded(false)
}
}, [isStreaming, startTime, endTime])
if (!reasoning || reasoning.trim().length === 0) {
return null
}
const toggleExpanded = () => {
setIsExpanded(!isExpanded)
}
// 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 (
<div className="mb-3">
<div
className={`text-sm text-zinc-600 border border-zinc-200 rounded-lg p-3 bg-zinc-50 cursor-pointer hover:bg-zinc-100 transition-all duration-300 ${
isExpanded ? 'border-zinc-300' : ''
}`}
onClick={toggleExpanded}
>
<div className="flex items-start gap-2">
<span className="text-zinc-400 text-xs mt-0.5 flex-shrink-0">💭</span>
{isExpanded ? (
// Expanded view: show reasoning content
<div className={`flex-1 ${isStreaming ? 'overflow-hidden whitespace-nowrap flex justify-end' : 'whitespace-pre-wrap'}`}>
<span className={isStreaming ? 'inline-block' : ''}>
{reasoning}
{isStreaming && <span className="inline-block w-2 h-4 bg-zinc-400 ml-1 animate-pulse"></span>}
</span>
</div>
) : (
// Collapsed view: show timing summary
<div className="flex-1">
<span className="text-zinc-500">
{isStreaming ? 'Thinking...' : duration ? `Thought for ${formatDuration(duration)}` : 'Thought process'}
</span>
</div>
)}
<button
className="text-zinc-400 hover:text-zinc-600 text-xs ml-auto flex-shrink-0"
onClick={(e) => {
e.stopPropagation()
toggleExpanded()
}}
>
{isExpanded ? '↑' : '↓'}
</button>
</div>
</div>
</div>
)
}
const ReasoningBlock: React.FC<ReasoningBlockProps> = ({ reasoning, isStreaming = false, startTime, endTime }) => {}
export default ReasoningBlock

View file

@ -1,22 +1,82 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { type ReasoningMessage } from '../../types'
import ReasoningBlock from './ReasoningBlock.tsx'
interface ReasoningMessageProps {
message: ReasoningMessage
}
const ReasoningMessage: React.FC<ReasoningMessageProps> = ({ message }) => {
console.log({ ReasoningMessage: message })
const [isExpanded, setIsExpanded] = useState(message.isReasoningStreaming) // Start expanded when streaming
// Auto-collapse instantly when streaming finishes
useEffect(() => {
if (!message.isReasoningStreaming && message.reasoningStartTime && message.reasoningEndTime) {
setIsExpanded(false)
}
}, [message.isReasoningStreaming, message.reasoningStartTime, message.reasoningEndTime])
if (!message.content || message.content.trim().length === 0) {
return null
}
const toggleExpanded = () => {
setIsExpanded(!isExpanded)
}
// Calculate thinking duration
const duration =
message.reasoningStartTime && message.reasoningEndTime ? (message.reasoningEndTime - message.reasoningStartTime) / 1000 : null
// Format duration nicely
const formatDuration = (seconds: number) => {
if (seconds < 1) {
return `${Math.round(seconds * 1000)}ms`
}
return `${seconds.toFixed(1)}s`
}
return (
<div className={`flex justify-start`}>
<div className={`text-zinc-900 w-full`}>
<ReasoningBlock
reasoning={message.content}
isStreaming={message.isReasoningStreaming}
startTime={message.reasoningStartTime}
endTime={message.reasoningEndTime}
/>
<div
className={`text-xs/5 text-zinc-600 border opacity-80 hover:opacity-100 border-zinc-200 rounded-lg p-3 bg-zinc-50 cursor-pointer hover:bg-zinc-100 transition-all duration-300 ${
isExpanded ? 'border-zinc-300 opacity-100' : ''
}`}
onClick={toggleExpanded}
>
<div className="flex items-start gap-2">
<span className="text-zinc-400 text-xs mt-0.5 flex-shrink-0">💭</span>
{isExpanded ? (
// Expanded view: show reasoning content
<div
className={`flex-1 ${message.isReasoningStreaming ? 'overflow-hidden whitespace-nowrap flex justify-end' : 'whitespace-pre-wrap'}`}
>
<span className={message.isReasoningStreaming ? 'inline-block' : ''}>
{message.content}
{message.isReasoningStreaming && <span className="inline-block w-2 h-4 bg-zinc-400 ml-1 animate-pulse"></span>}
</span>
</div>
) : (
// Collapsed view: show timing summary
<div className="flex-1">
<span className="text-zinc-500">
{message.isReasoningStreaming ? 'Thinking...' : duration ? `Thought for ${formatDuration(duration)}` : 'Thought process'}
</span>
</div>
)}
<button
className="text-zinc-400 hover:text-zinc-600 text-xs ml-auto flex-shrink-0"
onClick={(e) => {
e.stopPropagation()
toggleExpanded()
}}
>
{/*{isExpanded ? '↑' : '↓'}*/}
</button>
</div>
</div>
</div>
</div>
)

View file

@ -172,7 +172,7 @@ export const useStreamResponse = ({
// We have an existing reasoning block!
updatedMessage = {
...lastMessage,
content: lastMessage.content + event.textDelta.trim(),
content: lastMessage.content + event.textDelta,
}
} else {
// We need a new reasoning block!
@ -183,7 +183,7 @@ export const useStreamResponse = ({
newMessage = {
role: 'assistant',
type: 'reasoning',
content: event.textDelta.trim(),
content: event.textDelta,
isReasoningStreaming: true,
reasoningStartTime: currentReasoningStartTime!,
}
@ -234,90 +234,19 @@ export const useStreamResponse = ({
// console.warn('Tool result event missing toolName or toolCallId:', event)
// }
} else if (event.type === 'text-delta') {
console.info(`TODO TEXT DELTA`, event)
// // Check if we have an existing reasoning message that needs to be finalized
// let needsNewAssistantMessage = false
// if (currentReasoningStartTime && !currentReasoningEndTime) {
// currentReasoningEndTime = Date.now()
//
// // Update reasoning end time and stop streaming
// 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 = currentReasoningEndTime
// }
// }
// return updated
// })
//
// // Reset reasoning state after reasoning ends
// currentReasoningStartTime = undefined
// currentReasoningEndTime = undefined
// // Force creation of new assistant message for content
// needsNewAssistantMessage = true
// assistantMessageCreated = false
// }
//
// // Create assistant message on first text delta if not already created OR if we just finished reasoning
// if (!assistantMessageCreated || needsNewAssistantMessage) {
// 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
// debugMessages(
// 'Added new content assistant message at index',
// assistantMessageIndex,
// '(after reasoning:',
// needsNewAssistantMessage,
// ')'
// )
// debugMessages('Current messages:', JSON.stringify(conv.messages))
// }
// return updated
// })
// assistantMessageCreated = true
// }
//
// aiResponse += event.textDelta
// aiResponseRef.current = aiResponse
//
// //custom extraction of <chat-title> tag
// const titleMatch = aiResponse.match(/<chat-title>(.*?)<\/chat-title>/)
// if (titleMatch) {
// const title = titleMatch[1].trim()
// 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>.*?<\/chat-title>/, '').trim()
// }
//
// setConversations((prev) => {
// const updated = [...prev]
// const conv = updated.find((c) => c.id === conversationId)
// if (conv) {
// // 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
// }
// } else {
// }
// return updated
// })
//
// scrollToBottom(true)
if (lastMessage?.role !== 'assistant' || lastMessage.type !== 'content') {
newMessage = {
role: 'assistant',
content: event.textDelta,
type: 'content',
}
} else {
updatedMessage = {
...lastMessage,
content: lastMessage.content + event.textDelta,
}
}
scrollToBottom(true)
}
}
} catch (e) {
@ -327,7 +256,7 @@ export const useStreamResponse = ({
if (updatedMessage || newMessage) {
updated = prev.map((c) => {
if (c.id !== conversationId) return c
let messages = [...c.messages]
const messages = [...c.messages]
if (updatedMessage) {
messages.splice(-1, 1, updatedMessage)