Files
chat-proxy/frontend/src/useChatStream.js
T
2025-08-22 10:35:55 +00:00

119 lines
3.4 KiB
JavaScript

// useChatStream.js
import { useState, useCallback, useRef } from "react";
import { getSessionId, getUserId } from './useSessionId';
export function useChatStream() {
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const abortRef = useRef(null);
const sessionId = getSessionId();
const userId = getUserId();
const sendMessage = useCallback(
async (input) => {
if (!input.trim()) return;
// Abort any previous stream
if (abortRef.current) {
abortRef.current.abort();
}
const controller = new AbortController();
abortRef.current = controller;
const userMessage = { role: "user", content: input };
const assistantIndex = messages.length + 1;
setMessages((prev) => [...prev, userMessage, { role: "assistant", content: "" }]);
setLoading(true);
try {
const res = await fetch("/v1/chat-stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ user_id: userId, session_id: sessionId, message: input }),
signal: controller.signal,
});
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
let acc = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop() || "";
for (const part of parts) {
const line = part
.split("\n")
.map((l) => l.trim())
.find((l) => l.startsWith("data:"));
if (!line) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") {
setMessages((prev) => {
const next = [...prev];
next[assistantIndex] = { role: "assistant", content: acc };
return next;
});
setLoading(false);
return;
}
try {
const obj = JSON.parse(data);
const piece = obj?.choices?.[0]?.delta?.content ?? obj?.choices?.[0]?.text ?? "";
if (piece) {
acc += piece;
setMessages((prev) => {
const next = [...prev];
next[assistantIndex] = { role: "assistant", content: acc };
return next;
});
}
} catch {
/* ignore malformed JSON chunks */
}
}
}
} catch (err) {
if (err.name !== "AbortError") {
setMessages((prev) => {
const next = [...prev];
next[assistantIndex] = {
role: "assistant",
content: `Error: ${String(err)}`,
};
return next;
});
}
} finally {
setLoading(false);
abortRef.current = null;
}
},
[messages]
);
const stopGenerating = useCallback(() => {
if (abortRef.current) {
abortRef.current.abort();
abortRef.current = null;
setLoading(false);
}
}, []);
return { messages, loading, sendMessage, stopGenerating, setMessages };
}