summaryrefslogtreecommitdiffhomepage
path: root/packages/frontend/src/lib/chat.svelte.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/lib/chat.svelte.ts')
-rw-r--r--packages/frontend/src/lib/chat.svelte.ts274
1 files changed, 274 insertions, 0 deletions
diff --git a/packages/frontend/src/lib/chat.svelte.ts b/packages/frontend/src/lib/chat.svelte.ts
new file mode 100644
index 0000000..54216ec
--- /dev/null
+++ b/packages/frontend/src/lib/chat.svelte.ts
@@ -0,0 +1,274 @@
+import { config } from "./config.js";
+import type { AgentEvent, ChatMessage, DebugInfo, ToolCallDisplay } from "./types.js";
+import { wsClient } from "./ws.svelte.js";
+
+function generateId() {
+ return Math.random().toString(36).slice(2, 11);
+}
+
+function makeDebugInfo(overrides: Partial<DebugInfo> = {}): DebugInfo {
+ return {
+ timestamp: new Date().toISOString(),
+ model: "deepseek-v4-flash-free",
+ apiBase: config.apiBase,
+ connectionStatus: wsClient.connectionStatus,
+ ...overrides,
+ };
+}
+
+function formatConversation(msgs: ChatMessage[]): string {
+ const lines: string[] = [];
+ lines.push("=== Dispatch Conversation ===");
+ lines.push(`Exported: ${new Date().toISOString()}`);
+ lines.push("");
+
+ for (const msg of msgs) {
+ const role = msg.role === "user" ? "User" : "Assistant";
+ lines.push(`--- ${role} ---`);
+ lines.push(msg.content);
+
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
+ for (const tc of msg.toolCalls) {
+ lines.push(` [Tool: ${tc.name}]`);
+ lines.push(` Args: ${JSON.stringify(tc.arguments)}`);
+ if (tc.result !== undefined) {
+ const prefix = tc.isError ? " Error: " : " Result: ";
+ lines.push(`${prefix}${tc.result}`);
+ }
+ }
+ }
+
+ if (msg.debugInfo) {
+ lines.push(" [Debug Info]");
+ lines.push(` Timestamp: ${msg.debugInfo.timestamp}`);
+ if (msg.debugInfo.error) lines.push(` Error: ${msg.debugInfo.error}`);
+ if (msg.debugInfo.model) lines.push(` Model: ${msg.debugInfo.model}`);
+ if (msg.debugInfo.apiBase) lines.push(` API Base: ${msg.debugInfo.apiBase}`);
+ if (msg.debugInfo.connectionStatus)
+ lines.push(` Connection: ${msg.debugInfo.connectionStatus}`);
+ if (msg.debugInfo.agentStatus) lines.push(` Agent Status: ${msg.debugInfo.agentStatus}`);
+ if (msg.debugInfo.httpStatus) lines.push(` HTTP Status: ${msg.debugInfo.httpStatus}`);
+ if (msg.debugInfo.httpBody) lines.push(` HTTP Body: ${msg.debugInfo.httpBody}`);
+ if (msg.debugInfo.rawEvent)
+ lines.push(` Raw Event: ${JSON.stringify(msg.debugInfo.rawEvent)}`);
+ }
+
+ lines.push("");
+ }
+
+ return lines.join("\n");
+}
+
+function createChatStore() {
+ let messages: ChatMessage[] = $state([]);
+ let agentStatus: "idle" | "running" | "error" = $state("idle");
+ let isConnected = $state(false);
+ let currentAssistantId: string | null = null;
+
+ wsClient.onEvent((event) => {
+ const connected = wsClient.connectionStatus === "connected";
+ if (connected !== isConnected) {
+ isConnected = connected;
+ }
+ handleEvent(event);
+ });
+
+ $effect.root(() => {
+ $effect(() => {
+ isConnected = wsClient.connectionStatus === "connected";
+ });
+ });
+
+ function getCurrentAssistantMessage(): ChatMessage | null {
+ if (!currentAssistantId) return null;
+ return messages.find((m) => m.id === currentAssistantId) ?? null;
+ }
+
+ function ensureCurrentAssistantMessage(): ChatMessage {
+ let msg = getCurrentAssistantMessage();
+ if (!msg) {
+ const id = generateId();
+ currentAssistantId = id;
+ const newMsg: ChatMessage = {
+ id,
+ role: "assistant",
+ content: "",
+ toolCalls: [],
+ isStreaming: true,
+ };
+ messages = [...messages, newMsg];
+ msg = newMsg;
+ }
+ return msg;
+ }
+
+ function handleEvent(event: AgentEvent) {
+ switch (event.type) {
+ case "status": {
+ agentStatus = event.status;
+ if (event.status === "idle" || event.status === "error") {
+ currentAssistantId = null;
+ }
+ break;
+ }
+ case "text-delta": {
+ ensureCurrentAssistantMessage();
+ messages = messages.map((m) => {
+ if (m.id === currentAssistantId) {
+ return {
+ ...m,
+ content: m.content + event.delta,
+ isStreaming: true,
+ };
+ }
+ return m;
+ });
+ break;
+ }
+ case "tool-call": {
+ ensureCurrentAssistantMessage();
+ const toolCall: ToolCallDisplay = {
+ id: event.toolCall.id,
+ name: event.toolCall.name,
+ arguments: event.toolCall.arguments,
+ isExpanded: false,
+ };
+ messages = messages.map((m) => {
+ if (m.id === currentAssistantId) {
+ return { ...m, toolCalls: [...(m.toolCalls ?? []), toolCall] };
+ }
+ return m;
+ });
+ break;
+ }
+ case "tool-result": {
+ messages = messages.map((m) => {
+ if (m.id === currentAssistantId) {
+ return {
+ ...m,
+ toolCalls: (m.toolCalls ?? []).map((tc) => {
+ if (tc.id === event.toolResult.toolCallId) {
+ return {
+ ...tc,
+ result: event.toolResult.result,
+ isError: event.toolResult.isError,
+ };
+ }
+ return tc;
+ }),
+ };
+ }
+ return m;
+ });
+ break;
+ }
+ case "done": {
+ messages = messages.map((m) => {
+ if (m.id === currentAssistantId) {
+ return {
+ ...m,
+ content: event.message.content,
+ isStreaming: false,
+ };
+ }
+ return m;
+ });
+ currentAssistantId = null;
+ break;
+ }
+ case "error": {
+ const errMsg: ChatMessage = {
+ id: generateId(),
+ role: "assistant",
+ content: `Error: ${event.error}`,
+ isStreaming: false,
+ debugInfo: makeDebugInfo({
+ error: event.error,
+ agentStatus,
+ rawEvent: event,
+ }),
+ };
+ messages = [...messages, errMsg];
+ currentAssistantId = null;
+ agentStatus = "error";
+ break;
+ }
+ }
+ }
+
+ async function sendMessage(text: string) {
+ const userMsg: ChatMessage = {
+ id: generateId(),
+ role: "user",
+ content: text,
+ };
+ messages = [...messages, userMsg];
+ currentAssistantId = null;
+
+ const url = `${config.apiBase}/chat`;
+ try {
+ const res = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: text }),
+ });
+ if (!res.ok) {
+ const body = await res.text();
+ const errMsg: ChatMessage = {
+ id: generateId(),
+ role: "assistant",
+ content: `Error: Failed to send message (HTTP ${res.status})`,
+ isStreaming: false,
+ debugInfo: makeDebugInfo({
+ error: `POST ${url} returned ${res.status}`,
+ agentStatus,
+ httpStatus: res.status,
+ httpBody: body,
+ }),
+ };
+ messages = [...messages, errMsg];
+ }
+ } catch (err) {
+ const errorText = err instanceof Error ? err.message : String(err);
+ const errMsg: ChatMessage = {
+ id: generateId(),
+ role: "assistant",
+ content: "Error: Could not reach the server",
+ isStreaming: false,
+ debugInfo: makeDebugInfo({
+ error: `POST ${url} failed: ${errorText}`,
+ agentStatus,
+ }),
+ };
+ messages = [...messages, errMsg];
+ }
+ }
+
+ function copyConversation(): string {
+ return formatConversation(messages);
+ }
+
+ function clear() {
+ messages = [];
+ currentAssistantId = null;
+ agentStatus = "idle";
+ }
+
+ return {
+ get messages() {
+ return messages;
+ },
+ get agentStatus() {
+ return agentStatus;
+ },
+ get isConnected() {
+ return isConnected;
+ },
+ sendMessage,
+ handleEvent,
+ copyConversation,
+ clear,
+ };
+}
+
+export const chatStore = createChatStore();