feat(webui): refactor chat page onto global client and Signal Deck

This commit is contained in:
xiaoxixi 2026-07-24 10:26:05 +08:00
parent 14ca5dd7b4
commit d9e3245031

View File

@ -2,13 +2,12 @@
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import { Tooltip } from "bits-ui"; import { Tooltip } from "bits-ui";
import { clientId, formatTime, randomId } from "../lib/api.js"; import { clientId, formatTime, randomId } from "../lib/api.js";
import { chat } from "../lib/chat.svelte.js";
import Markdown from "../lib/Markdown.svelte"; import Markdown from "../lib/Markdown.svelte";
import ToolCallCard from "../lib/ToolCallCard.svelte"; import ToolCallCard from "../lib/ToolCallCard.svelte";
import TurnView from "../lib/TurnView.svelte"; import TurnView from "../lib/TurnView.svelte";
let { notify } = $props(); let { notify } = $props();
let socket = $state(null);
let connected = $state(false);
let sessions = $state([]); let sessions = $state([]);
let currentId = $state(null); let currentId = $state(null);
let messages = $state([]); let messages = $state([]);
@ -27,8 +26,6 @@
let todoOpen = $state(false); let todoOpen = $state(false);
let messageBox; let messageBox;
let input; let input;
let reconnectTimer;
let stopped = false;
const currentSession = $derived(sessions.find((item) => item.session_id === currentId)); const currentSession = $derived(sessions.find((item) => item.session_id === currentId));
const currentPlan = $derived(currentId ? plansBySession[currentId] || null : null); const currentPlan = $derived(currentId ? plansBySession[currentId] || null : null);
const completedItems = $derived(currentPlan?.items?.filter((item) => item.status === "completed").length || 0); const completedItems = $derived(currentPlan?.items?.filter((item) => item.status === "completed").length || 0);
@ -52,28 +49,7 @@
}); });
function send(frame) { function send(frame) {
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(frame)); if (!chat.send(frame)) notify("聊天连接尚未就绪", true);
else notify("聊天连接尚未就绪", true);
}
function connect() {
const scheme = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${scheme}://${location.host}/ws?client_id=${encodeURIComponent(clientId())}`);
socket = ws;
ws.onopen = () => {
connected = true;
plansBySession = {};
unseenPlanSessions = {};
todoOpen = false;
send({ type: "list_sessions", include_archived: false });
send({ type: "get_slash_commands" });
};
ws.onerror = () => ws.close();
ws.onclose = () => {
connected = false;
if (!stopped) reconnectTimer = setTimeout(connect, 1800);
};
ws.onmessage = (event) => handleFrame(JSON.parse(event.data));
} }
function handleFrame(frame) { function handleFrame(frame) {
@ -218,7 +194,7 @@
function submit() { function submit() {
const content = draft.trim(); const content = draft.trim();
const ready = pendingUploads.filter((upload) => upload.status === "ready"); const ready = pendingUploads.filter((upload) => upload.status === "ready");
if ((!content && !ready.length) || !connected || pendingUploads.some((upload) => upload.status === "uploading")) return; if ((!content && !ready.length) || !chat.connected || pendingUploads.some((upload) => upload.status === "uploading")) return;
appendMessage("user", content, ready.map((upload, index) => ({ appendMessage("user", content, ready.map((upload, index) => ({
index, index,
name: upload.name, name: upload.name,
@ -258,7 +234,7 @@
} }
function uploadFile(file) { function uploadFile(file) {
if (!connected) return notify("聊天连接尚未就绪", true); if (!chat.connected) return notify("聊天连接尚未就绪", true);
const localId = randomId(); const localId = randomId();
const localUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : null; const localUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : null;
pendingUploads = [...pendingUploads, { pendingUploads = [...pendingUploads, {
@ -380,11 +356,20 @@
} }
onMount(() => { onMount(() => {
connect(); const unsubscribe = chat.subscribe(handleFrame);
const onOpen = (frame) => {
if (frame.type !== "_open") return;
plansBySession = {};
unseenPlanSessions = {};
todoOpen = false;
send({ type: "list_sessions", include_archived: false });
send({ type: "get_slash_commands" });
};
const unsubOpen = chat.subscribe(onOpen);
if (chat.connected) onOpen({ type: "_open" });
return () => { return () => {
stopped = true; unsubscribe();
clearTimeout(reconnectTimer); unsubOpen();
socket?.close();
clearPendingUploads(); clearPendingUploads();
}; };
}); });
@ -515,8 +500,8 @@
aria-activedescendant={commandSuggestions.length ? `slash-command-${selectedCommand}` : undefined} aria-activedescendant={commandSuggestions.length ? `slash-command-${selectedCommand}` : undefined}
placeholder="输入消息,输入 / 查看命令" placeholder="输入消息,输入 / 查看命令"
></textarea> ></textarea>
<button class="send" type="submit" aria-label="发送" disabled={!connected || (!draft.trim() && !pendingUploads.some((upload) => upload.status === "ready")) || pendingUploads.some((upload) => upload.status === "uploading")}>↑</button> <button class="send" type="submit" aria-label="发送" disabled={!chat.connected || (!draft.trim() && !pendingUploads.some((upload) => upload.status === "ready")) || pendingUploads.some((upload) => upload.status === "uploading")}>↑</button>
<small><span class:online={connected}>{connected ? "已连接" : "已断开,正在重连"}</span><span>/ 打开命令 · Shift+Enter 换行</span></small> <small><span class:online={chat.connected}>{chat.connected ? "已连接" : "已断开,正在重连"}</span><span>/ 打开命令 · Shift+Enter 换行</span></small>
</form> </form>
</div> </div>
{#if currentPlan} {#if currentPlan}