dev1/src/components/ChatDrawer.js

174 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ────────────────────────────────── ChatDrawer.jsx
import { useEffect, useRef, useState } from "react";
import { Sheet, SheetTrigger, SheetContent } from "./ui/sheet.js";
import { Button } from "./ui/button.js";
import { Input } from "./ui/input.js";
import { MessageCircle } from "lucide-react";
/* ---------------------------------------------------------------
Streams from /api/chat/free and executes UI-tool callbacks
----------------------------------------------------------------*/
export default function ChatDrawer({
pageContext = "Home",
snapshot = null,
}) {
/* state */
const [open, setOpen] = useState(false);
const [prompt, setPrompt] = useState("");
const [messages, setMessages] = useState([]); // { role, content }
const listRef = useRef(null);
/* auto-scroll */
useEffect(() => {
listRef.current &&
(listRef.current.scrollTop = listRef.current.scrollHeight);
}, [messages]);
/* helper: stream-friendly append */
const pushAssistant = (chunk) =>
setMessages((prev) => {
const last = prev.at(-1);
if (last?.role === "assistant") {
const updated = [...prev];
updated[updated.length - 1] = {
...last,
content: last.content + chunk
};
return updated;
}
return [...prev, { role: "assistant", content: chunk }];
});
/* send prompt */
async function sendPrompt() {
const text = prompt.trim();
if (!text) return;
setMessages((m) => [...m, { role: "user", content: text }]);
setPrompt("");
const body = JSON.stringify({
prompt: text,
pageContext,
chatHistory: messages,
snapshot
});
try {
const token = localStorage.getItem("token") || "";
const headers = {
"Content-Type": "application/json",
Accept : "text/event-stream",
...(token ? { Authorization: `Bearer ${token}` } : {})
};
const resp = await fetch("/api/chat/free", {
method: "POST",
headers,
body
});
if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`);
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = "";
/* ─────────────── STREAM LOOP ─────────────── */
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (!value) continue;
buf += decoder.decode(value, { stream: true });
let nl;
while ((nl = buf.indexOf("\n")) !== -1) {
const line = buf.slice(0, nl).trim(); // one full line
buf = buf.slice(nl + 1); // keep remainder
/* 2⃣ normal assistant text */
if (line) pushAssistant(line + "\n");
}
}
/* ───────── END STREAM LOOP ───────── */
if (buf.trim()) pushAssistant(buf);
} catch (err) {
console.error("[ChatDrawer] stream error", err);
pushAssistant("Sorry — something went wrong. Please try again later.");
}
}
/* Enter submits */
const handleKeyDown = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendPrompt();
}
};
/* UI */
return (
<Sheet open={open} onOpenChange={setOpen}>
{/* floating button */}
<SheetTrigger>
<button
aria-label="Open chat"
className="fixed bottom-6 right-6 z-50 rounded-full bg-blue-600 p-3 text-white shadow-lg hover:bg-blue-700"
>
<MessageCircle size={24} />
</button>
</SheetTrigger>
{/* drawer */}
<SheetContent
side="right"
className="flex max-h-screen w-[380px] flex-col px-0 md:w-[420px]"
>
<div className="sticky top-0 z-10 border-b bg-white px-4 py-3 font-semibold">
{pageContext}
</div>
{/* transcript */}
<div
ref={listRef}
className="flex-1 space-y-4 overflow-y-auto px-4 py-2 text-sm"
>
{messages.map((m, i) => (
<div
key={i}
className={
m.role === "user" ? "text-right" : "text-left text-gray-800"
}
>
{m.content}
</div>
))}
</div>
{/* prompt box */}
<div className="border-t p-4">
<form
onSubmit={(e) => {
e.preventDefault();
sendPrompt();
}}
className="flex gap-2"
>
<Input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask me anything…"
className="flex-1"
/>
<Button type="submit" disabled={!prompt.trim()}>
Send
</Button>
</form>
</div>
</SheetContent>
</Sheet>
);
}