BeefStewBibi's picture
Quality of life update for code
fc2011e
import { useEffect, useRef, useState } from "react";
const API_BASE = import.meta.env.VITE_API_BASE_URL || "";
interface Message {
role: "user" | "assistant";
content: string;
}
export default function ChatPanel() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, loading]);
async function submitMessage() {
const trimmed = input.trim();
if (!trimmed || loading) return;
setError(null);
setMessages((prev) => [...prev, { role: "user", content: trimmed }]);
setInput("");
setLoading(true);
try {
const res = await fetch(`${API_BASE}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: trimmed }),
});
if (res.status === 429) {
setError("Rate limit exceeded. Please wait a moment and try again.");
return;
}
if (!res.ok) {
const detail = await res.json().catch(() => null);
setError(detail?.detail ?? `Error: ${res.status}`);
return;
}
const data = await res.json();
setMessages((prev) => [
...prev,
{ role: "assistant", content: data.generated_code ?? "No code generated." },
]);
} catch {
setError("Failed to connect to the server.");
} finally {
setLoading(false);
}
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
submitMessage();
}
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submitMessage();
}
}
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
<h1 className="text-xl font-bold mb-4 text-center">AVP RAG Chat</h1>
<div className="flex-1 overflow-y-auto space-y-3 mb-4">
{messages.map((msg, i) => {
const isUser = msg.role === "user";
return (
<div key={i} className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 ${
isUser ? "bg-blue-600 text-white" : "bg-slate-700 text-slate-100"
}`}
>
{isUser ? (
<p className="whitespace-pre-wrap">{msg.content}</p>
) : (
<pre className="whitespace-pre-wrap font-mono text-sm">{msg.content}</pre>
)}
</div>
</div>
);
})}
{loading && (
<div className="flex justify-start">
<div className="bg-slate-700 rounded-lg px-4 py-2 text-slate-400">
<span className="animate-pulse">Generating...</span>
</div>
</div>
)}
{error && (
<div className="flex justify-center">
<div className="bg-red-900/50 text-red-300 rounded-lg px-4 py-2 text-sm">
{error}
</div>
</div>
)}
<div ref={bottomRef} />
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask me to generate AVP code..."
rows={2}
className="flex-1 bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-slate-100 placeholder-slate-500 resize-none focus:outline-none focus:border-blue-500"
/>
<button
type="submit"
disabled={loading || !input.trim()}
className="bg-blue-600 hover:bg-blue-500 disabled:bg-slate-700 disabled:text-slate-500 text-white px-4 rounded-lg font-medium transition-colors"
>
Send
</button>
</form>
</div>
);
}