Big sigh, finally getting control of my markup
This commit is contained in:
parent
52c3e8d727
commit
ba845e7852
10 changed files with 253 additions and 284 deletions
|
|
@ -1,64 +0,0 @@
|
|||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
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
|
||||
}
|
||||
|
||||
const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
|
||||
// Handle tool calls and results differently
|
||||
if (message.role === 'tool-call') {
|
||||
return <ToolCallMessage message={message} />
|
||||
}
|
||||
|
||||
if (message.role === 'tool-result') {
|
||||
return <ToolResultMessage message={message} />
|
||||
}
|
||||
|
||||
// Handle regular messages (user, assistant, system)
|
||||
return (
|
||||
<div className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div
|
||||
className={`
|
||||
${
|
||||
message.role === 'user'
|
||||
? 'max-w-[80%] rounded-2xl px-3 py-2 border border-zinc-200/10 bg-white text-black'
|
||||
: ' text-zinc-900 w-full'
|
||||
}`}
|
||||
>
|
||||
{/* Show reasoning block for assistant messages with reasoning */}
|
||||
{message.role === 'assistant' && 'reasoning' in message && (message.reasoning || message.isReasoningStreaming) && (
|
||||
<ReasoningBlock
|
||||
reasoning={message.reasoning || ''}
|
||||
isStreaming={message.isReasoningStreaming}
|
||||
startTime={message.reasoningStartTime}
|
||||
endTime={message.reasoningEndTime}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Only render content div if there's actual content */}
|
||||
{message.content && message.content.trim() && (
|
||||
<div className="prose prose-zinc">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
components={{
|
||||
table: ({ children }) => <div className="overflow-x-scroll text-sm">{children}</div>,
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatMessage
|
||||
|
|
@ -6,7 +6,7 @@ import { type Conversation, type Message } from '../types'
|
|||
import { type Model } from '../types/models'
|
||||
import { type IDBPDatabase } from 'idb'
|
||||
import { type Tool } from 'use-mcp/react'
|
||||
import ChatMessage from './ChatMessage'
|
||||
import ChatMessage from './messages/ChatMessage.tsx'
|
||||
import ChatInput from './ChatInput'
|
||||
import ModelSelectionModal from './ModelSelectionModal'
|
||||
import McpServerModal from './McpServerModal'
|
||||
|
|
@ -180,43 +180,12 @@ const ConversationThread: React.FC<ConversationThreadProps> = ({
|
|||
>
|
||||
<div className="max-w-2xl mx-auto w-full px-4">
|
||||
{currentConversation.messages.length === 0 ? (
|
||||
<div className="text-center">
|
||||
{/* Title and description text hidden but kept in codebase */}
|
||||
{false && (
|
||||
<>
|
||||
<h1 className="text-4xl font-semibold text-zinc-800">What do you want to know?</h1>
|
||||
<div className="mt-4">
|
||||
<h2 className="mt-2 text-md opacity-70">
|
||||
AI chat template built with React, Vite and Cloudflare Workers AI.
|
||||
<div className="mt-1 w-full">
|
||||
Find the source code on{' '}
|
||||
<a className="text-blue-700" href="https://github.com/thomasgauvin/ai-chat-template">
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center">{/*<h1 className="text-9xl font-semibold text-zinc-800 h-20 overflow-auto">use-mcp</h1>*/}</div>
|
||||
) : (
|
||||
<div className="py-4 px-4 space-y-4">
|
||||
{currentConversation.messages
|
||||
.filter((message) => {
|
||||
// Filter out empty assistant messages (no content and no reasoning)
|
||||
if (
|
||||
message.role === 'assistant' &&
|
||||
(!message.content || !message.content.trim()) &&
|
||||
!('reasoning' in message && (message.reasoning || message.isReasoningStreaming))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.map((message, index) => (
|
||||
<ChatMessage key={index} message={message} />
|
||||
))}
|
||||
{currentConversation.messages.map((message, index) => (
|
||||
<ChatMessage key={index} message={message} />
|
||||
))}
|
||||
{isLoading && !streamStarted && <div className="text-center text-sm text-zinc-600">Thinking...</div>}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react'
|
||||
import { type AssistantMessage } from '../../types'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
|
||||
interface AssistantMessageProps {
|
||||
message: AssistantMessage
|
||||
}
|
||||
|
||||
const AssistantMessage: React.FC<AssistantMessageProps> = ({ message }) => {
|
||||
return (
|
||||
<div className={`flex justify-start`}>
|
||||
<div className={`text-zinc-900 w-full`}>
|
||||
<div className="prose prose-zinc">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
components={{
|
||||
table: ({ children }) => <div className="overflow-x-scroll text-sm">{children}</div>,
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssistantMessage
|
||||
53
examples/chat-ui/src/components/messages/ChatMessage.tsx
Normal file
53
examples/chat-ui/src/components/messages/ChatMessage.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
import { type Message } from '../../types'
|
||||
import ToolCallMessage from './ToolCallMessage.tsx'
|
||||
import ToolResultMessage from './ToolResultMessage.tsx'
|
||||
import AssistantMessage from './AssistantMessage.tsx'
|
||||
import ReasoningMessage from './ReasoningMessage.tsx'
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message
|
||||
}
|
||||
|
||||
const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
|
||||
// Handle tool calls and results differently
|
||||
if (message.role === 'tool-call') {
|
||||
return <ToolCallMessage message={message} />
|
||||
}
|
||||
|
||||
if (message.role === 'tool-result') {
|
||||
return <ToolResultMessage message={message} />
|
||||
}
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
if (message.type === 'reasoning') {
|
||||
return <ReasoningMessage message={message} />
|
||||
} else {
|
||||
return <AssistantMessage message={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="prose prose-zinc">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeHighlight]}
|
||||
components={{
|
||||
table: ({ children }) => <div className="overflow-x-scroll text-sm">{children}</div>,
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatMessage
|
||||
82
examples/chat-ui/src/components/messages/ReasoningBlock.tsx
Normal file
82
examples/chat-ui/src/components/messages/ReasoningBlock.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
interface ReasoningBlockProps {
|
||||
reasoning: string
|
||||
isStreaming?: boolean
|
||||
startTime?: number
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReasoningBlock
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import React 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 })
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReasoningMessage
|
||||
47
examples/chat-ui/src/components/messages/ToolCallMessage.tsx
Normal file
47
examples/chat-ui/src/components/messages/ToolCallMessage.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, Wrench } from 'lucide-react'
|
||||
import { type ToolCallMessage } from '../../types'
|
||||
|
||||
interface ToolCallMessageProps {
|
||||
message: ToolCallMessage
|
||||
}
|
||||
|
||||
const ToolCallMessage: React.FC<ToolCallMessageProps> = ({ message }) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const argsPreview = JSON.stringify(message.toolArgs).substring(0, 100)
|
||||
const shouldTruncate = JSON.stringify(message.toolArgs).length > 100
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 py-3 px-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<Wrench size={16} className="text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-grow min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-blue-800 text-sm">Tool Call</span>
|
||||
<span className="text-blue-600 text-sm font-mono bg-blue-100 px-2 py-0.5 rounded">{message.toolName}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`text-sm text-blue-700 font-mono bg-blue-100 p-2 rounded ${shouldTruncate ? 'cursor-pointer hover:bg-blue-200' : ''}`}
|
||||
onClick={() => shouldTruncate && setExpanded(!expanded)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<span className={expanded ? '' : 'truncate'}>
|
||||
{expanded ? JSON.stringify(message.toolArgs, null, 2) : `${argsPreview}${shouldTruncate ? '...' : ''}`}
|
||||
</span>
|
||||
{shouldTruncate && (
|
||||
<button className="ml-2 flex-shrink-0">{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}</button>
|
||||
)}
|
||||
</div>
|
||||
{expanded && shouldTruncate && (
|
||||
<pre className="mt-2 whitespace-pre-wrap break-words max-h-64 overflow-y-auto">{JSON.stringify(message.toolArgs, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolCallMessage
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, CheckCircle } from 'lucide-react'
|
||||
import { type ToolResultMessage } from '../../types'
|
||||
|
||||
interface ToolResultMessageProps {
|
||||
message: ToolResultMessage
|
||||
}
|
||||
|
||||
const ToolResultMessage: React.FC<ToolResultMessageProps> = ({ message }) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const resultPreview = JSON.stringify(message.toolResult).substring(0, 100)
|
||||
const shouldTruncate = JSON.stringify(message.toolResult).length > 100
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 py-3 px-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<CheckCircle size={16} className="text-green-600" />
|
||||
</div>
|
||||
<div className="flex-grow min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-green-800 text-sm">Tool Result</span>
|
||||
<span className="text-green-600 text-sm font-mono bg-green-100 px-2 py-0.5 rounded">{message.toolName}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`text-sm text-green-700 font-mono bg-green-100 p-2 rounded ${shouldTruncate ? 'cursor-pointer hover:bg-green-200' : ''}`}
|
||||
onClick={() => shouldTruncate && setExpanded(!expanded)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<span className={expanded ? '' : 'truncate'}>
|
||||
{expanded ? JSON.stringify(message.toolResult, null, 2) : `${resultPreview}${shouldTruncate ? '...' : ''}`}
|
||||
</span>
|
||||
{shouldTruncate && (
|
||||
<button className="ml-2 flex-shrink-0">{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}</button>
|
||||
)}
|
||||
</div>
|
||||
{expanded && shouldTruncate && (
|
||||
<pre className="mt-2 whitespace-pre-wrap break-words max-h-64 overflow-y-auto">
|
||||
{JSON.stringify(message.toolResult, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolResultMessage
|
||||
|
|
@ -113,9 +113,6 @@ export const useStreamResponse = ({
|
|||
}
|
||||
|
||||
const streamResponse = async (messages: Message[]) => {
|
||||
let aiResponse = ''
|
||||
let currentReasoning = ''
|
||||
|
||||
// Check if API key is available
|
||||
let apiKey = getApiKey(selectedModel.provider.id)
|
||||
if (!apiKey) {
|
||||
|
|
@ -138,7 +135,7 @@ export const useStreamResponse = ({
|
|||
const aiTools = convertMcpToolsToAiTools(mcpTools)
|
||||
|
||||
// Prepare stream options
|
||||
const streamOptions: any = {
|
||||
const streamOptions = {
|
||||
model: modelInstance,
|
||||
messages: messages
|
||||
.filter((msg) => ['user', 'assistant', 'system'].includes(msg.role) && hasContent(msg))
|
||||
|
|
@ -198,155 +195,127 @@ export const useStreamResponse = ({
|
|||
}
|
||||
|
||||
if (event.type === 'tool-call') {
|
||||
// End reasoning phase immediately when we see a tool call for linear flow
|
||||
if (currentReasoningStartTime && !currentReasoningEndTime) {
|
||||
currentReasoningEndTime = Date.now()
|
||||
|
||||
// Update reasoning end time and stop streaming immediately
|
||||
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.reasoningEndTime = currentReasoningEndTime
|
||||
assistantMessage.isReasoningStreaming = false
|
||||
}
|
||||
}
|
||||
return updated
|
||||
})
|
||||
|
||||
// Reset reasoning state for potential new reasoning session
|
||||
currentReasoningStartTime = undefined
|
||||
currentReasoningEndTime = undefined
|
||||
assistantMessageCreated = false
|
||||
}
|
||||
|
||||
if (event.toolName) {
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
if (conv) {
|
||||
conv.messages.push({
|
||||
role: 'tool-call',
|
||||
toolName: event.toolName,
|
||||
toolArgs: event.args || {},
|
||||
callId: event.toolCallId || 'unknown',
|
||||
})
|
||||
debugMessages('Added new tool-call message:', event.toolName, 'callId:', event.toolCallId)
|
||||
debugMessages('Current messages:', JSON.stringify(conv.messages))
|
||||
}
|
||||
return updated
|
||||
conv.messages.push({
|
||||
role: 'tool-call',
|
||||
toolName: event.toolName,
|
||||
toolArgs: event.args || {},
|
||||
callId: event.toolCallId || 'unknown',
|
||||
})
|
||||
debugMessages('Added new tool-call message:', event.toolName, 'callId:', event.toolCallId)
|
||||
debugMessages('Current messages:', JSON.stringify(conv.messages))
|
||||
scrollToBottom(true)
|
||||
} else {
|
||||
console.warn('Tool call event missing toolName:', event)
|
||||
}
|
||||
} else if ((event as any).type === 'tool-result') {
|
||||
if ((event as any).toolName && (event as any).toolCallId) {
|
||||
setConversations((prev) => {
|
||||
const updated = [...prev]
|
||||
const conv = updated.find((c) => c.id === conversationId)
|
||||
if (conv) {
|
||||
conv.messages.push({
|
||||
role: 'tool-result',
|
||||
toolName: (event as any).toolName,
|
||||
toolArgs: (event as any).args || {},
|
||||
toolResult: (event as any).result,
|
||||
callId: (event as any).toolCallId,
|
||||
})
|
||||
debugMessages('Added new tool-result message:', (event as any).toolName, 'callId:', (event as any).toolCallId)
|
||||
debugMessages('Current messages:', JSON.stringify(conv.messages))
|
||||
}
|
||||
return updated
|
||||
})
|
||||
scrollToBottom(true)
|
||||
} else {
|
||||
console.warn('Tool result event missing toolName or toolCallId:', event)
|
||||
}
|
||||
} else if (event.type === 'tool-result') {
|
||||
console.info(`TODO TOOL RESULT`, event)
|
||||
// if (event.toolName && event.toolCallId) {
|
||||
// setConversations((prev) => {
|
||||
// const updated = [...prev]
|
||||
// const conv = updated.find((c) => c.id === conversationId)
|
||||
// if (conv) {
|
||||
// conv.messages.push({
|
||||
// role: 'tool-result',
|
||||
// toolName: event.toolName,
|
||||
// toolArgs: event.args || {},
|
||||
// toolResult: event.result,
|
||||
// callId: event.toolCallId,
|
||||
// })
|
||||
// debugMessages('Added new tool-result message:', event.toolName, 'callId:', event.toolCallId)
|
||||
// debugMessages('Current messages:', JSON.stringify(conv.messages))
|
||||
// }
|
||||
// return updated
|
||||
// })
|
||||
// scrollToBottom(true)
|
||||
// } else {
|
||||
// console.warn('Tool result event missing toolName or toolCallId:', event)
|
||||
// }
|
||||
} else if (event.type === 'text-delta') {
|
||||
// Check if we have an existing reasoning message that needs to be finalized
|
||||
let needsNewAssistantMessage = false
|
||||
if (currentReasoningStartTime && !currentReasoningEndTime) {
|
||||
currentReasoningEndTime = Date.now()
|
||||
console.info(`TODO TEXT DELTA`, event)
|
||||
|
||||
// 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)
|
||||
// // 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)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error in full stream processing:', e)
|
||||
|
|
@ -355,8 +324,6 @@ export const useStreamResponse = ({
|
|||
})
|
||||
}
|
||||
|
||||
debugMessages('Stream processing completed. Final message count:', assistantMessageCreated ? 'created' : 'none')
|
||||
|
||||
// Show final conversation state
|
||||
setConversations((prev) => {
|
||||
const conv = prev.find((c) => c.id === conversationId)
|
||||
|
|
@ -365,48 +332,36 @@ export const useStreamResponse = ({
|
|||
}
|
||||
return prev
|
||||
})
|
||||
|
||||
// Final cleanup - ensure reasoning streaming is stopped
|
||||
if (assistantMessageCreated && currentReasoningStartTime) {
|
||||
if (!currentReasoningEndTime) {
|
||||
currentReasoningEndTime = Date.now()
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
//
|
||||
// // Final cleanup - ensure reasoning streaming is stopped
|
||||
// if (assistantMessageCreated && currentReasoningStartTime) {
|
||||
// if (!currentReasoningEndTime) {
|
||||
// currentReasoningEndTime = Date.now()
|
||||
// }
|
||||
//
|
||||
// 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
|
||||
// })
|
||||
// }
|
||||
} catch (error: unknown) {
|
||||
if (controller.signal.aborted) {
|
||||
} else {
|
||||
console.error('Error generating response:', error)
|
||||
// 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') {
|
||||
conv.messages.splice(assistantMessageIndex, 1)
|
||||
}
|
||||
return updated
|
||||
})
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setStreamStarted(false)
|
||||
setController(new AbortController())
|
||||
debugMessages('Stream response function completed')
|
||||
}
|
||||
return aiResponse
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export interface AssistantMessage extends BaseMessage {
|
|||
type: 'content'
|
||||
}
|
||||
|
||||
export interface AssistantReasoningMessage extends BaseMessage {
|
||||
export interface ReasoningMessage extends BaseMessage {
|
||||
role: 'assistant'
|
||||
type: 'reasoning'
|
||||
reasoningStartTime: number
|
||||
|
|
@ -38,7 +38,7 @@ export interface ToolResultMessage {
|
|||
callId: string
|
||||
}
|
||||
|
||||
export type Message = UserMessage | SystemMessage | AssistantMessage | AssistantReasoningMessage | ToolCallMessage | ToolResultMessage
|
||||
export type Message = UserMessage | SystemMessage | AssistantMessage | ReasoningMessage | ToolCallMessage | ToolResultMessage
|
||||
|
||||
export interface Conversation {
|
||||
id?: number
|
||||
|
|
|
|||
Loading…
Reference in a new issue