| import { useEffect, useRef, useState } from "react"; |
| import { io, Socket } from "socket.io-client"; |
| import ChatMessage from "./components/ChatMessage"; |
|
|
| interface Message { |
| role: "user" | "assistant"; |
| content: string; |
| } |
|
|
| const SOCKET_URL = `${window.location.protocol}//${window.location.hostname}:8000`; |
|
|
| export default function App() { |
| const [messages, setMessages] = useState<Message[]>([]); |
| const [input, setInput] = useState(""); |
| const [loading, setLoading] = useState(false); |
| const [connected, setConnected] = useState(false); |
| const socketRef = useRef<Socket | null>(null); |
| const bottomRef = useRef<HTMLDivElement>(null); |
|
|
| useEffect(() => { |
| const socket = io(SOCKET_URL); |
| socketRef.current = socket; |
|
|
| socket.on("connect", () => setConnected(true)); |
| socket.on("disconnect", () => setConnected(false)); |
|
|
| socket.on("answer", (data: { answer?: string; error?: string }) => { |
| const content = data.answer ?? `Lỗi: ${data.error}`; |
| setMessages((prev) => [...prev, { role: "assistant", content }]); |
| setLoading(false); |
| }); |
|
|
| return () => { |
| socket.disconnect(); |
| }; |
| }, []); |
|
|
| useEffect(() => { |
| bottomRef.current?.scrollIntoView({ behavior: "smooth" }); |
| }, [messages, loading]); |
|
|
| const handleSend = () => { |
| const question = input.trim(); |
| if (!question || loading || !connected) return; |
|
|
| setMessages((prev) => [...prev, { role: "user", content: question }]); |
| setInput(""); |
| setLoading(true); |
| socketRef.current?.emit("ask", { question }); |
| }; |
|
|
| const handleKeyDown = (e: React.KeyboardEvent) => { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| handleSend(); |
| } |
| }; |
|
|
| return ( |
| <div className="flex flex-col h-screen bg-white"> |
| {/* Header */} |
| <header className="bg-blue-700 text-white px-6 py-4 shadow-md"> |
| <h1 className="text-xl font-bold">Hỏi đáp Pháp luật Việt Nam</h1> |
| <p className="text-blue-200 text-sm mt-0.5"> |
| RAG Agent - Tra cứu văn bản pháp luật |
| </p> |
| </header> |
| |
| {/* Messages */} |
| <main className="flex-1 overflow-y-auto px-4 py-6 md:px-8"> |
| {messages.length === 0 && !loading && ( |
| <div className="text-center text-gray-400 mt-20"> |
| <p className="text-lg">Chào mừng bạn!</p> |
| <p className="text-sm mt-1"> |
| Hãy đặt câu hỏi về pháp luật Việt Nam. |
| </p> |
| </div> |
| )} |
| {messages.map((msg, i) => ( |
| <ChatMessage key={i} role={msg.role} content={msg.content} /> |
| ))} |
| {loading && ( |
| <div className="flex justify-start mb-4"> |
| <div className="bg-gray-100 rounded-2xl px-4 py-3 text-gray-500"> |
| <p className="text-sm font-medium mb-1 opacity-70"> |
| Trợ lý Pháp luật |
| </p> |
| <span className="inline-flex gap-1"> |
| <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:0ms]" /> |
| <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:150ms]" /> |
| <span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:300ms]" /> |
| </span> |
| </div> |
| </div> |
| )} |
| <div ref={bottomRef} /> |
| </main> |
| |
| {/* Input */} |
| <footer className="border-t bg-white px-4 py-3 md:px-8"> |
| <div className="flex items-center gap-3 max-w-4xl mx-auto"> |
| <input |
| type="text" |
| className="flex-1 border border-gray-300 rounded-xl px-4 py-3 text-[15px] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-50" |
| placeholder={ |
| connected ? "Nhập câu hỏi..." : "Đang kết nối server..." |
| } |
| value={input} |
| onChange={(e) => setInput(e.target.value)} |
| onKeyDown={handleKeyDown} |
| disabled={!connected || loading} |
| /> |
| <button |
| onClick={handleSend} |
| disabled={!input.trim() || loading || !connected} |
| className="bg-blue-600 text-white px-5 py-3 rounded-xl font-medium hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors" |
| > |
| Gửi |
| </button> |
| </div> |
| {!connected && ( |
| <p className="text-center text-red-500 text-xs mt-2"> |
| Chưa kết nối được server. Hãy chắc chắn backend đang chạy tại{" "} |
| {SOCKET_URL} |
| </p> |
| )} |
| </footer> |
| </div> |
| ); |
| } |
|
|