Untitled

 avatar
unknown
tsx
10 months ago
16 kB
17
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