comparison love/epi/src/hooks/useChatWebsocket.ts @ 38:cf9caa4abc3e

[Love] FE and BE. Can chat and render images. Also created MCP for powerpoint generations.
author MrJuneJune <me@mrjunejune.com>
date Mon, 01 Dec 2025 20:35:56 -0800
parents
children
comparison
equal deleted inserted replaced
37:fb9bcd3145cb 38:cf9caa4abc3e
1 import { wsUrl } from '@/utils';
2 import { useEffect } from 'react';
3
4 export type Payload = {
5 chatId: string;
6 content: string;
7 action: 'append' | 'done';
8 } | {
9 chatId: string;
10 title: string;
11 action:'title_updated';
12 } | {
13 chatId: string;
14 url: string;
15 action:'image';
16 }
17
18 export type OnMessage = (payload: Payload) => void;
19
20 type WebSocketEvent = {
21 chatId: string;
22 payload: Payload;
23 };
24
25 // TODO: Make this into class so we can mock for test.
26 const wsSingleton = new Map<string, WebSocket>();
27 const listeners = new Map<string, Set<OnMessage>>();
28 const pendingMessages = new Map<string, Array<{ content: string; resolve: () => void }>>();
29
30 function getOrCreateWebSocket(chatId: string): WebSocket {
31 let ws = wsSingleton.get(chatId);
32
33 if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
34 return ws;
35 }
36
37 ws = new WebSocket(wsUrl(`/chats/${chatId}/ws`));
38
39 ws.onopen = () => {
40 console.log(`[WS] Connected for chat ${chatId}`);
41 const queue = pendingMessages.get(chatId);
42 if (queue) {
43 queue.forEach(({ content }) => {
44 ws!.send(JSON.stringify({ role: 'user', content }));
45 });
46 pendingMessages.delete(chatId);
47 }
48 };
49
50 // ADD IT HERE:
51 ws.onmessage = (event) => {
52 const payload = JSON.parse(event.data);
53 console.log('[WS] Received message:', payload);
54
55 wsEventTarget.dispatchEvent(
56 new CustomEvent('message', {
57 detail: { chatId, payload },
58 }),
59 );
60 };
61
62 ws.onerror = (error) => {
63 console.error(`[WS] Error for chat ${chatId}:`, error);
64 };
65
66 ws.onclose = () => {
67 console.log(`[WS] Closed for chat ${chatId}`);
68 wsSingleton.delete(chatId);
69 };
70
71 wsSingleton.set(chatId, ws);
72 return ws;
73 }
74
75 // TODO: This could be not done rather cancel?
76 export function broadcastDone(chatId: string) {
77 const set = listeners.get(chatId);
78 // TODO: This does not update the histroy to be canceled on so there is decrepency but it should be fine for now.
79 if (set) {
80 set.forEach((cb) => cb({ chatId, content: '', action: 'done' }));
81 }
82 }
83
84 export function useChatWebSocket(
85 chatId: string | null,
86 onMessage: OnMessage,
87 onLoadingChange: (loading: boolean) => void,
88 ) {
89
90 useEffect(() => {
91 if (!chatId) return;
92
93 // Only register listener to given chatId as chatId websocket has not been made yet.
94 let set = listeners.get(chatId);
95 if (!set) {
96 set = new Set();
97 listeners.set(chatId, set);
98 }
99 set.add(onMessage);
100
101 return () => {
102 if (!chatId) return;
103 const set = listeners.get(chatId);
104 if (set) {
105 set.delete(onMessage);
106 if (set.size === 0) {
107 listeners.delete(chatId);
108 const ws = wsSingleton.get(chatId);
109 if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
110 ws.close();
111 }
112 }
113 }
114 };
115 }, [chatId, onMessage]);
116
117 const sendMessage = (content: string) => {
118 if (!chatId) return;
119
120 const ws = getOrCreateWebSocket(chatId);
121
122 onLoadingChange(true);
123
124 if (ws.readyState === WebSocket.OPEN) {
125 ws.send(JSON.stringify({ role: 'user', content }));
126 } else if (ws.readyState === WebSocket.CONNECTING) {
127 // Queue message until open
128 let queue = pendingMessages.get(chatId);
129 if (!queue) {
130 queue = [];
131 pendingMessages.set(chatId, queue);
132 }
133 console.log('Queue: ', queue);
134 if (queue.length > 1) return;
135 queue.push({ content, resolve: () => {} });
136 } else {
137 console.warn('[WS] WebSocket not in usable state', ws.readyState);
138 onLoadingChange(false);
139 }
140 };
141
142 return { sendMessage };
143 }
144
145 const wsEventTarget = new EventTarget();
146
147 export function subscribeToChat(chatId: string, callback: (payload: Payload) => void) {
148 const handler = (event: Event) => {
149 const { chatId: eventChatId, payload } = (event as CustomEvent<WebSocketEvent>).detail;
150 if (eventChatId === chatId) {
151 callback(payload);
152 }
153 };
154
155 wsEventTarget.addEventListener('message', handler);
156
157 return () => wsEventTarget.removeEventListener('message', handler);
158 }
159
160 export function sendMessageChatId(content: string, chatId: string): void {
161 if (!content.trim() || !chatId) return;
162
163 const ws = getOrCreateWebSocket(chatId);
164
165 if (ws.readyState === WebSocket.OPEN) {
166 ws.send(JSON.stringify({ role: 'user', content }));
167 } else if (ws.readyState === WebSocket.CONNECTING) {
168 let queue = pendingMessages.get(chatId);
169 if (!queue) {
170 queue = [];
171 pendingMessages.set(chatId, queue);
172 }
173 if (!queue.some(m => m.content === content)) {
174 queue.push({ content, resolve: () => {} });
175 }
176 }
177 }