agents / frontend /src /App.tsx
rain1024's picture
Initial commit: RAG Agent chat UI for Vietnamese law
0fb0b85
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>
);
}