mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-02-04 18:06:48 +00:00
feat: add useTranscriptChat WebSocket hook
Task 5: Frontend WebSocket Hook - Creates React hook for bidirectional chat WebSocket - Handles token streaming with proper state accumulation - Manages conversation history (user + assistant messages) - Prevents memory leaks with isMounted check - Proper cleanup on unmount - Type-safe Message interface Validated: - No React dependency issues (removed currentStreamingText from deps) - No stale closure bugs (using ref for streaming text) - Proper mounted state tracking - Lint passes with no errors - TypeScript types correctly defined - WebSocket cleanup on unmount ~100 lines
This commit is contained in:
103
www/app/(app)/transcripts/useTranscriptChat.ts
Normal file
103
www/app/(app)/transcripts/useTranscriptChat.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { WEBSOCKET_URL } from "../../lib/apiClient";
|
||||
|
||||
export type Message = {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
text: string;
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
export type UseTranscriptChat = {
|
||||
messages: Message[];
|
||||
sendMessage: (text: string) => void;
|
||||
isStreaming: boolean;
|
||||
currentStreamingText: string;
|
||||
};
|
||||
|
||||
export const useTranscriptChat = (transcriptId: string): UseTranscriptChat => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [currentStreamingText, setCurrentStreamingText] = useState("");
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const streamingTextRef = useRef<string>("");
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
const url = `${WEBSOCKET_URL}/v1/transcripts/${transcriptId}/chat`;
|
||||
const ws = new WebSocket(url);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("Chat WebSocket connected");
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
const msg = JSON.parse(event.data);
|
||||
|
||||
switch (msg.type) {
|
||||
case "token":
|
||||
setIsStreaming(true);
|
||||
streamingTextRef.current += msg.text;
|
||||
setCurrentStreamingText(streamingTextRef.current);
|
||||
break;
|
||||
|
||||
case "done":
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
role: "assistant",
|
||||
text: streamingTextRef.current,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
streamingTextRef.current = "";
|
||||
setCurrentStreamingText("");
|
||||
setIsStreaming(false);
|
||||
break;
|
||||
|
||||
case "error":
|
||||
console.error("Chat error:", msg.message);
|
||||
setIsStreaming(false);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("Chat WebSocket closed");
|
||||
};
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
ws.close();
|
||||
};
|
||||
}, [transcriptId]);
|
||||
|
||||
const sendMessage = (text: string) => {
|
||||
if (!wsRef.current) return;
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
role: "user",
|
||||
text,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
wsRef.current.send(JSON.stringify({ type: "message", text }));
|
||||
};
|
||||
|
||||
return { messages, sendMessage, isStreaming, currentStreamingText };
|
||||
};
|
||||
Reference in New Issue
Block a user