feat: chat panel with SSE streaming and tool call cards

This commit is contained in:
Ken
2026-05-26 19:28:12 +00:00
parent c24fd11f03
commit 3b01753336
4 changed files with 422 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import { useState, useRef, useEffect } from 'react'
import type { KeyboardEvent } from 'react'
import ReactMarkdown from 'react-markdown'
import { IconSend } from '@tabler/icons-react'
import { ToolCallCard } from './ToolCallCard'
import type { ChatMessage } from '../hooks/useChat'
interface ChatPanelProps {
messages: ChatMessage[]
isStreaming: boolean
onSend: (content: string) => void
sessionId: string | null
}
export function ChatPanel({ messages, isStreaming, onSend, sessionId }: ChatPanelProps) {
const [input, setInput] = useState('')
const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
function handleSend() {
if (input.trim() && !isStreaming) {
onSend(input.trim())
setInput('')
}
}
function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
if (sessionId === null) {
return (
<div className='chat-panel'>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
}}
>
<span style={{ color: '#6a6a8a', fontSize: '1.1rem' }}>
Select or create a session to start researching
</span>
</div>
</div>
)
}
return (
<div className='chat-panel'>
<div className='chat-messages'>
{messages.map(message => (
<div key={message.id}>
{message.role === 'assistant' ? (
<div>
<ReactMarkdown>{message.content}</ReactMarkdown>
{message.toolCalls?.map(tc => (
<ToolCallCard key={tc.id} toolCall={tc} />
))}
</div>
) : (
<div>{message.content}</div>
)}
</div>
))}
{isStreaming && messages.length > 0 && <div>Researching...</div>}
<div ref={messagesEndRef} />
</div>
<div className='chat-input-area'>
<input
placeholder='Ask a research question...'
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isStreaming}
autoFocus
/>
<button onClick={handleSend} disabled={isStreaming || !input.trim()}>
<IconSend size={18} />
</button>
</div>
</div>
)
}