Handling reasoning then messages at least
This commit is contained in:
parent
0c7b9e0537
commit
5e41222c63
4 changed files with 87 additions and 168 deletions
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue