feat: chat panel with SSE streaming and tool call cards
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user