diff --git a/webui/src/lib/chat.svelte.js b/webui/src/lib/chat.svelte.js new file mode 100644 index 0000000..ab86d50 --- /dev/null +++ b/webui/src/lib/chat.svelte.js @@ -0,0 +1,60 @@ +import { clientId } from "./api.js"; + +class ChatClient { + connected = $state(false); + turn = $state(null); // 最新 turn 快照(任意 session),供活动脊 + #socket = null; + #handlers = new Set(); + #reconnectTimer = null; + #stopped = false; + + connect() { + if (this.#socket) return; + this.#stopped = false; + const scheme = location.protocol === "https:" ? "wss" : "ws"; + const ws = new WebSocket(`${scheme}://${location.host}/ws?client_id=${encodeURIComponent(clientId())}`); + this.#socket = ws; + ws.onopen = () => { + this.connected = true; + this.#dispatch({ type: "_open" }); + }; + ws.onerror = () => ws.close(); + ws.onclose = () => { + this.connected = false; + this.#socket = null; + this.#dispatch({ type: "_close" }); + if (!this.#stopped) this.#reconnectTimer = setTimeout(() => this.connect(), 1800); + }; + ws.onmessage = (event) => { + const frame = JSON.parse(event.data); + if (frame.type === "turn_updated" && frame.snapshot) this.turn = frame.snapshot; + this.#dispatch(frame); + }; + } + + disconnect() { + this.#stopped = true; + clearTimeout(this.#reconnectTimer); + this.#socket?.close(); + this.#socket = null; + } + + send(frame) { + if (this.#socket?.readyState === WebSocket.OPEN) { + this.#socket.send(JSON.stringify(frame)); + return true; + } + return false; + } + + subscribe(handler) { + this.#handlers.add(handler); + return () => this.#handlers.delete(handler); + } + + #dispatch(frame) { + for (const handler of this.#handlers) handler(frame); + } +} + +export const chat = new ChatClient();