Untitled
unknown
tsx
10 months ago
16 kB
15
Indexable
'use client'
import { useState, useRef, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Loader2, Send, User, Bot, Activity, CheckCircle, AlertCircle } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
interface ChatMessage {
id: string
type: 'user' | 'assistant'
content: string
timestamp: Date
status?: 'streaming' | 'complete' | 'error'
progress?: {
stage: string
worker?: string
percentage?: number
}
}
interface StreamEvent {
type: 'progress' | 'partial' | 'complete' | 'error'
data: any
}
export default function ChatStreamDemo() {
const [messages, setMessages] = useState<ChatMessage[]>([])
const [input, setInput] = useState('')
const [isStreaming, setIsStreaming] = useState(false)
const [currentProgress, setCurrentProgress] = useState<string>('')
const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
useEffect(() => {
scrollToBottom()
}, [messages])
const handleSendMessage = async () => {
if (!input.trim() || isStreaming) return
const userMessage: ChatMessage = {
id: Date.now().toString(),
type: 'user',
content: input.trim(),
timestamp: new Date(),
}
setMessages((prev) => [...prev, userMessage])
setInput('')
setIsStreaming(true)
setCurrentProgress('Đang khởi tạo...')
// Create assistant message placeholder
const assistantMessageId = (Date.now() + 1).toString()
const assistantMessage: ChatMessage = {
id: assistantMessageId,
type: 'assistant',
content: '',
timestamp: new Date(),
status: 'streaming',
}
setMessages((prev) => [...prev, assistantMessage])
try {
const response = await fetch('/api/v1/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify({
message: userMessage.content,
sessionId: 'demo-session',
userId: 'demo-user',
conversationHistory: messages.map((m) => ({
role: m.type === 'user' ? 'user' : 'assistant',
content: m.content,
})),
}),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('No reader available')
}
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const eventData = JSON.parse(line.slice(6))
console.log('[v0] Received SSE event:', eventData)
handleStreamEvent(eventData, assistantMessageId)
} catch (e) {
console.error('[v0] Error parsing SSE data:', e, 'Line:', line)
}
}
}
}
} catch (error) {
console.error('[v0] Stream error:', error)
setMessages((prev) =>
prev.map((msg) =>
msg.id === assistantMessageId
? { ...msg, content: 'Lỗi kết nối. Vui lòng thử lại.', status: 'error' }
: msg,
),
)
} finally {
setIsStreaming(false)
setCurrentProgress('')
}
}
const handleStreamEvent = (event: any, messageId: string) => {
if (!event || typeof event !== 'object') {
console.warn('[v0] Invalid event received:', event)
return
}
console.log('[v0] Processing stream event:', event.type, event)
switch (event.type) {
case 'connection':
setCurrentProgress('Đã kết nối')
break
case 'status':
setCurrentProgress(event.message || 'Đang xử lý...')
setMessages((prev) =>
prev.map((msg) =>
msg.id === messageId
? {
...msg,
progress: {
stage: event.message || 'Đang xử lý...',
},
}
: msg,
),
)
break
case 'intent':
setCurrentProgress(`Phân tích ý định: ${event.data?.intent || ''}`)
break
case 'workers':
const workers = Array.isArray(event.data) ? event.data.join(', ') : 'Unknown'
setCurrentProgress(`Chọn chuyên gia: ${workers}`)
break
case 'worker_start':
setCurrentProgress(event.message || 'Đang xử lý với chuyên gia...')
break
case 'worker_complete':
setCurrentProgress(event.message || 'Hoàn thành xử lý')
break
case 'text_chunk':
const chunk = event.text || ''
if (chunk) {
setMessages((prev) =>
prev.map((msg) =>
msg.id === messageId ? { ...msg, content: msg.content + chunk } : msg,
),
)
}
break
case 'complete':
setMessages((prev) =>
prev.map((msg) =>
msg.id === messageId
? {
...msg,
content: msg.content || event.message || 'Phản hồi hoàn tất',
status: 'complete',
progress: undefined,
}
: msg,
),
)
setCurrentProgress('Hoàn thành')
break
case 'error':
const errorMessage = event.error || event.message || 'Có lỗi xảy ra'
setMessages((prev) =>
prev.map((msg) =>
msg.id === messageId
? {
...msg,
content: `Lỗi: ${errorMessage}`,
status: 'error',
progress: undefined,
}
: msg,
),
)
setCurrentProgress('Lỗi')
break
case 'worker_error':
console.warn('[v0] Worker error:', event.message)
setCurrentProgress(event.message || 'Lỗi từ chuyên gia')
break
default:
console.warn('[v0] Unknown event type:', event.type, event)
}
}
const getStatusIcon = (status?: string) => {
switch (status) {
case 'streaming':
return <Loader2 className="h-4 w-4 animate-spin text-blue-500" />
case 'complete':
return <CheckCircle className="h-4 w-4 text-green-500" />
case 'error':
return <AlertCircle className="h-4 w-4 text-red-500" />
default:
return null
}
}
const getProgressBadge = (progress?: ChatMessage['progress']) => {
if (!progress) return null
return (
<div className="flex items-center gap-2 mt-2">
<Badge variant="outline" className="text-xs">
<Activity className="h-3 w-3 mr-1" />
{progress.stage}
</Badge>
{progress.worker && (
<Badge variant="secondary" className="text-xs">
{progress.worker}
</Badge>
)}
{progress.percentage && (
<Badge variant="default" className="text-xs">
{progress.percentage}%
</Badge>
)}
</div>
)
}
const quickMessages = [
'Tôi cần báo cáo trading tuần này',
'Phân tích performance tài khoản MT5',
'Giải thích về Risk Management',
'Tôi có bao nhiêu tài khoản MT5?',
]
return (
<div className="max-w-4xl mx-auto">
<Card className="h-[600px] flex flex-col">
<CardHeader className="border-b">
<CardTitle className="flex items-center gap-2">
<Bot className="h-5 w-5 text-blue-500" />
Trading Advisor Chat Stream
{isStreaming && (
<Badge variant="outline" className="ml-auto">
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
{currentProgress}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-gray-500 mt-8">
<Bot className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p className="mb-4">Chào bạn! Tôi là Trading Advisor AI.</p>
<p className="text-sm mb-6">Hãy thử một trong những câu hỏi sau:</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{quickMessages.map((msg, index) => (
<Button
key={index}
variant="outline"
size="sm"
onClick={() => setInput(msg)}
className="text-left justify-start"
>
{msg}
</Button>
))}
</div>
</div>
)}
{messages.map((message) => (
<div
key={message.id}
className={`flex gap-3 ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`flex gap-3 max-w-[80%] ${message.type === 'user' ? 'flex-row-reverse' : 'flex-row'}`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
message.type === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-600'
}`}
>
{message.type === 'user' ? (
<User className="h-4 w-4" />
) : (
<Bot className="h-4 w-4" />
)}
</div>
<div
className={`rounded-lg p-3 ${
message.type === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-900'
}`}
>
<div className="flex items-start gap-2">
<div className="flex-1">
{message.content &&
(message.type === 'user' ? (
<p className="whitespace-pre-wrap">{message.content}</p>
) : (
<div className="prose prose-sm max-w-none prose-headings:text-gray-900 prose-p:text-gray-900 prose-strong:text-gray-900 prose-code:text-gray-800 prose-code:bg-gray-200 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-pre:bg-gray-800 prose-pre:text-gray-100">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code: ({ node, inline, className, children, ...props }: any) => {
return inline ? (
<code
className="bg-gray-200 text-gray-800 px-1 py-0.5 rounded text-sm"
{...props}
>
{children}
</code>
) : (
<code
className="block bg-gray-800 text-gray-100 p-3 rounded-lg text-sm overflow-x-auto"
{...props}
>
{children}
</code>
)
},
ul: ({ children }: any) => (
<ul className="list-disc list-inside space-y-1 my-2">
{children}
</ul>
),
ol: ({ children }: any) => (
<ol className="list-decimal list-inside space-y-1 my-2">
{children}
</ol>
),
h1: ({ children }: any) => (
<h1 className="text-lg font-bold mt-4 mb-2 text-gray-900">
{children}
</h1>
),
h2: ({ children }: any) => (
<h2 className="text-base font-semibold mt-3 mb-2 text-gray-900">
{children}
</h2>
),
h3: ({ children }: any) => (
<h3 className="text-sm font-medium mt-2 mb-1 text-gray-900">
{children}
</h3>
),
p: ({ children }: any) => (
<p className="mb-2 last:mb-0 text-gray-900">{children}</p>
),
blockquote: ({ children }: any) => (
<blockquote className="border-l-4 border-gray-300 pl-4 italic my-2 text-gray-700">
{children}
</blockquote>
),
}}
>
{message.content}
</ReactMarkdown>
</div>
))}
{message.type === 'assistant' &&
!message.content &&
message.status === 'streaming' && (
<div className="flex items-center gap-2 text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Đang soạn phản hồi...</span>
</div>
)}
</div>
{message.type === 'assistant' && getStatusIcon(message.status)}
</div>
{message.type === 'assistant' && getProgressBadge(message.progress)}
<div className="text-xs opacity-70 mt-2">
{message.timestamp.toLocaleTimeString()}
</div>
</div>
</div>
</div>
))}
<div ref={messagesEndRef} />
</CardContent>
<div className="border-t p-4">
<div className="flex gap-2">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Nhập tin nhắn của bạn..."
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
disabled={isStreaming}
className="flex-1"
/>
<Button onClick={handleSendMessage} disabled={!input.trim() || isStreaming} size="icon">
{isStreaming ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</Button>
</div>
</div>
</Card>
</div>
)
}
Editor is loading...
Leave a Comment