File size: 4,252 Bytes
921d377 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | "use client";
import { useEffect, useRef, useState } from "react";
import MessageList from "./MessageList";
import Composer from "./Composer";
type ActivityStage = "idle" | "parsing" | "thinking";
function ExpertActivity({ stage }: { stage: ActivityStage }) {
if (stage === "idle") return null;
const isParsing = stage === "parsing";
return (
<div
style={{
background: "#080808",
borderRadius: 16,
padding: "16px 18px",
marginBottom: 14,
border: "1px solid rgba(60,60,70,0.35)",
color: "#ddd",
}}
>
<style>{`
@keyframes hpDotBounce {
0%, 80%, 100% { transform: translateY(0); opacity: 0.7; }
40% { transform: translateY(-6px); opacity: 1; }
}
@keyframes hpGrokPulse {
0% { background: #222; transform: scale(1); opacity: .75; box-shadow: 0 0 2px rgba(255,255,255,.1); }
8% { background: #fff; transform: scale(1.55); opacity: 1; box-shadow: 0 0 8px rgba(255,255,255,.9), 0 0 14px rgba(180,220,255,.6); }
18% { background: #aaa; transform: scale(1.15); opacity: .9; box-shadow: 0 0 4px rgba(255,255,255,.4); }
28%, 100% { background: #222; transform: scale(1); opacity: .75; box-shadow: 0 0 2px rgba(255,255,255,.08); }
}
`}</style>
<div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 10 }}>
{isParsing ? (
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 5px)", gridTemplateRows: "repeat(3, 5px)", gap: 7 }}>
{Array.from({ length: 9 }).map((_, i) => (
<span
key={i}
style={{
width: 5,
height: 5,
borderRadius: "50%",
animation: "hpGrokPulse 2.4s infinite ease-in-out",
animationDelay: `${i * 0.2}s`,
}}
/>
))}
</div>
) : (
<div style={{ display: "flex", alignItems: "center", gap: 5 }}>
{[0, 1, 2].map((i) => (
<span
key={i}
style={{
width: 6,
height: 6,
borderRadius: "50%",
background: "#888",
animation: "hpDotBounce 1.3s infinite ease-in-out",
animationDelay: `${i * 0.15}s`,
}}
/>
))}
</div>
)}
<div style={{ color: "#7aaae0", fontSize: 15 }}>
{isParsing ? "Parsing question · sending to backend" : "Thinking"}
</div>
</div>
<div style={{ color: "#8e8e9a", fontSize: 13 }}>
{isParsing
? "Analyzing query structure and preparing tool context."
: "Processing reasoning step-by-step."}
</div>
</div>
);
}
export default function ChatWindow() {
const [messages, setMessages] = useState<string[]>([]);
const [stage, setStage] = useState<ActivityStage>("idle");
const parseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (parseTimer.current) clearTimeout(parseTimer.current);
};
}, []);
async function sendMessage(text: string) {
setStage("parsing");
parseTimer.current = setTimeout(() => setStage("thinking"), 1200);
try {
const resp = await fetch("http://localhost:8000/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session_id: "default", message: text }),
});
const data = await resp.json();
if (parseTimer.current) clearTimeout(parseTimer.current);
setStage("thinking");
setMessages((prev) => [...prev, `User: ${text}`, `Expert: ${data.answer}`]);
} catch (err) {
if (parseTimer.current) clearTimeout(parseTimer.current);
setMessages((prev) => [...prev, `User: ${text}`, `Expert: Failed to get response: ${String(err)}`]);
} finally {
setStage("idle");
}
}
return (
<div style={{ padding: 24 }}>
<MessageList messages={messages} />
<ExpertActivity stage={stage} />
<Composer onSend={sendMessage} />
</div>
);
}
|