From 6a6305e8d1d4fca2f2d4f0f349134565ff5a386e Mon Sep 17 00:00:00 2001 From: jlightner Date: Sat, 4 Apr 2026 07:53:50 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Updated=20streamChat()=20API,=20ChatWid?= =?UTF-8?q?get,=20and=20ChatPage=20to=20thread=20conv=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/api/chat.ts" - "frontend/src/components/ChatWidget.tsx" - "frontend/src/pages/ChatPage.tsx" - "frontend/src/pages/ChatPage.module.css" GSD-Task: S04/T02 --- frontend/src/api/chat.ts | 16 +- frontend/src/components/ChatWidget.tsx | 26 +- frontend/src/pages/ChatPage.module.css | 103 ++++++-- frontend/src/pages/ChatPage.tsx | 314 +++++++++++++++++-------- 4 files changed, 332 insertions(+), 127 deletions(-) diff --git a/frontend/src/api/chat.ts b/frontend/src/api/chat.ts index 82a1a24..cc75c36 100644 --- a/frontend/src/api/chat.ts +++ b/frontend/src/api/chat.ts @@ -21,10 +21,15 @@ export interface ChatSource { section_heading: string; } +export interface ChatDoneMeta { + cascade_tier: string; + conversation_id: string; +} + export interface ChatCallbacks { onSources: (sources: ChatSource[]) => void; onToken: (text: string) => void; - onDone: (meta: { cascade_tier: string }) => void; + onDone: (meta: ChatDoneMeta) => void; onError: (message: string) => void; } @@ -36,6 +41,7 @@ export function streamChat( query: string, callbacks: ChatCallbacks, creator?: string, + conversationId?: string, ): AbortController { const controller = new AbortController(); @@ -57,7 +63,11 @@ export function streamChat( fetch(`${BASE}/chat`, { method: "POST", headers, - body: JSON.stringify({ query, creator: creator ?? null }), + body: JSON.stringify({ + query, + creator: creator ?? null, + conversation_id: conversationId ?? null, + }), signal: controller.signal, }) .then(async (res) => { @@ -116,7 +126,7 @@ export function streamChat( callbacks.onToken(JSON.parse(rawData) as string); break; case "done": - callbacks.onDone(JSON.parse(rawData) as { cascade_tier: string }); + callbacks.onDone(JSON.parse(rawData) as ChatDoneMeta); break; case "error": { const err = JSON.parse(rawData) as { message?: string }; diff --git a/frontend/src/components/ChatWidget.tsx b/frontend/src/components/ChatWidget.tsx index 0a64071..1f45a1e 100644 --- a/frontend/src/components/ChatWidget.tsx +++ b/frontend/src/components/ChatWidget.tsx @@ -120,6 +120,7 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps) const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [streaming, setStreaming] = useState(false); + const [conversationId, setConversationId] = useState(undefined); const abortRef = useRef(null); const messagesEndRef = useRef(null); @@ -151,15 +152,24 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps) }; }, []); + const handleClose = useCallback(() => { + abortRef.current?.abort(); + setOpen(false); + setMessages([]); + setInput(""); + setStreaming(false); + setConversationId(undefined); + }, []); + // Escape key closes panel useEffect(() => { if (!open) return; const handleKey = (e: KeyboardEvent) => { - if (e.key === "Escape") setOpen(false); + if (e.key === "Escape") handleClose(); }; document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); - }, [open]); + }, [open, handleClose]); const sendMessage = useCallback( (text: string) => { @@ -168,6 +178,10 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps) abortRef.current?.abort(); + // Generate conversation_id on first message + const cid = conversationId ?? crypto.randomUUID(); + if (!conversationId) setConversationId(cid); + const userMsg: Message = { role: "user", text: q, sources: [], done: true }; const assistantMsg: Message = { role: "assistant", text: "", sources: [], done: false }; const newMessages = [...messages, userMsg, assistantMsg]; @@ -192,7 +206,8 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps) ), ); }, - onDone: () => { + onDone: (meta) => { + setConversationId(meta.conversation_id); setMessages((prev) => prev.map((m, i) => (i === assistantIndex ? { ...m, done: true } : m)), ); @@ -208,9 +223,10 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps) }, }, creatorName, + cid, ); }, - [messages, streaming, creatorName], + [messages, streaming, creatorName, conversationId], ); const handleSubmit = (e: React.FormEvent) => { @@ -249,7 +265,7 @@ export default function ChatWidget({ creatorName, techniques }: ChatWidgetProps) + )} + + + {!hasMessages && !streaming && ( +
+ 💬 +

Ask any question about music production techniques.

+

Responses include citations linking to source pages and videos.

+
+ )} + + {hasMessages && ( +
+ {messages.map((msg, i) => { + if (msg.role === "user") { + return ( +
+ {msg.text} +
+ ); + } + + // Assistant message + return ( +
+ {msg.error ? ( +
{msg.error}
+ ) : ( + <> +
+ {parseChatCitations(msg.text, msg.sources)} + {!msg.done && } +
+ {msg.done && msg.sources.length > 0 && ( +
+

Sources

+
    + {msg.sources.map((src) => { + const href = src.section_anchor + ? `/techniques/${src.slug}#${src.section_anchor}` + : `/techniques/${src.slug}`; + return ( +
  1. + + {src.number} + +
    + + {src.title} + {src.section_heading + ? ` — ${src.section_heading}` + : ""} + + {src.creator_name && ( + + {" "} + · {src.creator_name} + + )} +
    +
  2. + ); + })} +
+
+ )} + + )} +
+ ); + })} + + {/* Typing indicator — before first token arrives */} + {streaming && + messages.length > 0 && + messages[messages.length - 1]?.role === "assistant" && + messages[messages.length - 1]?.text === "" && + !messages[messages.length - 1]?.error && ( +
+
+ Thinking… +
+ )} + +
+
+ )}
setQuery(e.target.value)} + value={input} + onChange={(e) => setInput(e.target.value)} placeholder="Ask about music production techniques…" disabled={streaming} autoFocus @@ -155,66 +322,11 @@ export default function ChatPage() {
- - {error &&
{error}
} - - {loading && ( -
-
- Searching for sources… -
- )} - - {hasResponse && ( -
-
- {parseChatCitations(responseText, sources)} - {streaming && } -
- - {sources.length > 0 && (done || !streaming) && ( -
-

Sources

-
    - {sources.map((src) => { - const href = src.section_anchor - ? `/techniques/${src.slug}#${src.section_anchor}` - : `/techniques/${src.slug}`; - return ( -
  1. - {src.number} -
    - - {src.title} - {src.section_heading ? ` — ${src.section_heading}` : ""} - - {src.creator_name && ( - - {" "}· {src.creator_name} - - )} -
    -
  2. - ); - })} -
-
- )} -
- )} - - {!hasResponse && !loading && !error && ( -
- 💬 -

Ask any question about music production techniques.

-

Responses include citations linking to source pages and videos.

-
- )}
); }