119 lines
3.4 KiB
JavaScript
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 };
|
|
}
|
|
|
|
|