import { create } from 'zustand'; import type { AuthUser, ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun, ProcessWsEvent, Session, UiAgentDescriptor, UiMcpServerDescriptor, } from '@/types'; import type { WsStatus } from '@/lib/api'; const ACTIVE_SESSION_STORAGE_KEY = 'nanobot_active_session_id'; function getInitialSessionId(): string { if (typeof window === 'undefined') { return 'web:default'; } const saved = window.localStorage.getItem(ACTIVE_SESSION_STORAGE_KEY)?.trim(); return saved || 'web:default'; } function persistSessionId(id: string): void { if (typeof window === 'undefined') { return; } window.localStorage.setItem(ACTIVE_SESSION_STORAGE_KEY, id); } interface ChatStore { user: AuthUser | null; isAuthLoading: boolean; sessionId: string; messages: ChatMessage[]; isLoading: boolean; streamingContent: string; wsStatus: WsStatus; isThinking: boolean; nanobotReady: boolean | null; sessions: Session[]; processRuns: ProcessRun[]; processEvents: ProcessEvent[]; processArtifacts: ProcessArtifact[]; selectedRunId: string | null; selectedArtifactId: string | null; agentRegistry: UiAgentDescriptor[]; mcpRegistry: UiMcpServerDescriptor[]; mcpToolRegistry: Array<{ server_id: string; tools: Array> }>; lastCancelAck: { runId: string; ok: boolean } | null; setUser: (user: AuthUser | null) => void; setIsAuthLoading: (loading: boolean) => void; setSessionId: (id: string) => void; setMessages: (msgs: ChatMessage[]) => void; addMessage: (msg: ChatMessage) => void; setIsLoading: (loading: boolean) => void; setStreamingContent: (content: string) => void; appendStreamingContent: (chunk: string) => void; setSessions: (sessions: Session[]) => void; clearMessages: () => void; setWsStatus: (status: WsStatus) => void; setIsThinking: (thinking: boolean) => void; setNanobotReady: (ready: boolean | null) => void; resetProcessState: () => void; ingestProcessEvent: (event: ProcessWsEvent) => void; setSelectedRunId: (runId: string | null) => void; setSelectedArtifactId: (artifactId: string | null) => void; setAgentRegistry: (agents: UiAgentDescriptor[]) => void; setMcpRegistry: (servers: UiMcpServerDescriptor[]) => void; setMcpToolRegistry: (tools: Array<{ server_id: string; tools: Array> }>) => void; } function upsertRun(collection: ProcessRun[], run: ProcessRun): ProcessRun[] { const index = collection.findIndex((item) => item.run_id === run.run_id); if (index === -1) { return [...collection, run]; } const next = [...collection]; next[index] = { ...next[index], ...run, metadata: { ...(next[index].metadata ?? {}), ...(run.metadata ?? {}), }, }; return next; } function upsertArtifact(collection: ProcessArtifact[], artifact: ProcessArtifact): ProcessArtifact[] { const index = collection.findIndex((item) => item.artifact_id === artifact.artifact_id); if (index === -1) { return [...collection, artifact]; } const next = [...collection]; next[index] = artifact; return next; } function appendEvent(collection: ProcessEvent[], event: ProcessEvent): ProcessEvent[] { if (collection.some((item) => item.event_id === event.event_id)) { return collection; } return [...collection, event]; } function createEventId(event: ProcessWsEvent): string { if (event.type === 'process_cancel_ack') { return `${event.type}:${event.run_id}`; } const suffix = 'text' in event && typeof event.text === 'string' ? event.text : 'title' in event && typeof event.title === 'string' ? event.title : event.type; return `${event.type}:${event.run_id}:${event.created_at}:${suffix}`; } export const useChatStore = create((set) => ({ user: null, isAuthLoading: true, sessionId: getInitialSessionId(), messages: [], isLoading: false, streamingContent: '', wsStatus: 'disconnected', isThinking: false, nanobotReady: null, sessions: [], processRuns: [], processEvents: [], processArtifacts: [], selectedRunId: null, selectedArtifactId: null, agentRegistry: [], mcpRegistry: [], mcpToolRegistry: [], lastCancelAck: null, setUser: (user) => set({ user }), setIsAuthLoading: (loading) => set({ isAuthLoading: loading }), setSessionId: (id) => { persistSessionId(id); set({ sessionId: id }); }, setMessages: (msgs) => set({ messages: msgs }), addMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })), setIsLoading: (loading) => set({ isLoading: loading }), setStreamingContent: (content) => set({ streamingContent: content }), appendStreamingContent: (chunk) => set((s) => ({ streamingContent: s.streamingContent + chunk })), setSessions: (sessions) => set({ sessions }), clearMessages: () => set({ messages: [], streamingContent: '' }), setWsStatus: (status) => set({ wsStatus: status }), setIsThinking: (thinking) => set({ isThinking: thinking }), setNanobotReady: (ready) => set({ nanobotReady: ready }), resetProcessState: () => set({ processRuns: [], processEvents: [], processArtifacts: [], selectedRunId: null, selectedArtifactId: null, lastCancelAck: null, }), ingestProcessEvent: (event) => set((state) => { if (event.type === 'process_cancel_ack') { return { lastCancelAck: { runId: event.run_id, ok: event.ok, }, }; } const eventId = createEventId(event); let nextRuns = state.processRuns; let nextArtifacts = state.processArtifacts; let nextSelectedRunId = state.selectedRunId; const nextEvents = appendEvent(state.processEvents, { event_id: eventId, run_id: event.run_id, parent_run_id: 'parent_run_id' in event ? event.parent_run_id ?? null : null, kind: event.type === 'process_run_started' ? 'run_started' : event.type === 'process_run_progress' ? 'run_progress' : event.type === 'process_run_message' ? 'run_message' : event.type === 'process_run_status' ? 'run_status' : event.type === 'process_run_artifact' ? 'run_artifact' : event.type === 'process_run_finished' ? 'run_finished' : 'run_cancelled', actor_type: event.actor_type, actor_id: event.actor_id, actor_name: event.actor_name, text: 'text' in event && typeof event.text === 'string' ? event.text : 'summary' in event && typeof event.summary === 'string' ? event.summary : undefined, status: 'status' in event ? event.status : undefined, message_role: 'message_role' in event ? event.message_role : undefined, metadata: 'metadata' in event ? event.metadata : undefined, created_at: event.created_at, }); if (event.type === 'process_run_started') { nextRuns = upsertRun(nextRuns, { run_id: event.run_id, parent_run_id: event.parent_run_id ?? null, session_id: event.session_id ?? state.sessionId, actor_type: event.actor_type, actor_id: event.actor_id, actor_name: event.actor_name, title: event.title, source: event.source ?? null, status: event.status, started_at: event.created_at, metadata: event.metadata, }); } if (event.type === 'process_run_status') { nextRuns = upsertRun(nextRuns, { run_id: event.run_id, actor_type: event.actor_type, actor_id: event.actor_id, actor_name: event.actor_name, title: nextRuns.find((item) => item.run_id === event.run_id)?.title || event.actor_name, status: event.status, started_at: nextRuns.find((item) => item.run_id === event.run_id)?.started_at || event.created_at, }); } if (event.type === 'process_run_progress') { const current = nextRuns.find((item) => item.run_id === event.run_id); nextRuns = upsertRun(nextRuns, { run_id: event.run_id, actor_type: event.actor_type, actor_id: event.actor_id, actor_name: event.actor_name, title: current?.title || event.actor_name, status: current?.status || 'running', started_at: current?.started_at || event.created_at, }); } if (event.type === 'process_run_message') { const current = nextRuns.find((item) => item.run_id === event.run_id); nextRuns = upsertRun(nextRuns, { run_id: event.run_id, parent_run_id: current?.parent_run_id ?? event.parent_run_id ?? null, actor_type: event.actor_type, actor_id: event.actor_id, actor_name: event.actor_name, title: current?.title || event.actor_name, status: current?.status || 'running', started_at: current?.started_at || event.created_at, }); } if (event.type === 'process_run_artifact') { nextArtifacts = upsertArtifact(nextArtifacts, { artifact_id: `${event.run_id}:${event.created_at}:${event.title}`, run_id: event.run_id, actor_type: event.actor_type, actor_id: event.actor_id, actor_name: event.actor_name, title: event.title, artifact_type: event.artifact_type, content: event.content, data: event.data, file_id: event.file_id, url: event.url, metadata: event.metadata, created_at: event.created_at, }); } if (event.type === 'process_run_finished') { const current = nextRuns.find((item) => item.run_id === event.run_id); nextRuns = upsertRun(nextRuns, { run_id: event.run_id, actor_type: event.actor_type, actor_id: event.actor_id, actor_name: event.actor_name, title: current?.title || event.actor_name, status: event.status, started_at: current?.started_at || event.created_at, finished_at: event.created_at, summary: event.summary ?? current?.summary ?? null, metadata: event.metadata, }); } if (event.type === 'process_run_cancelled') { const current = nextRuns.find((item) => item.run_id === event.run_id); nextRuns = upsertRun(nextRuns, { run_id: event.run_id, actor_type: event.actor_type, actor_id: event.actor_id, actor_name: event.actor_name, title: current?.title || event.actor_name, status: 'cancelled', started_at: current?.started_at || event.created_at, finished_at: event.created_at, summary: current?.summary ?? '已取消', }); } return { processRuns: nextRuns, processEvents: nextEvents, processArtifacts: nextArtifacts, selectedRunId: nextSelectedRunId, }; }), setSelectedRunId: (runId) => set({ selectedRunId: runId }), setSelectedArtifactId: (artifactId) => set({ selectedArtifactId: artifactId }), setAgentRegistry: (agents) => set({ agentRegistry: agents }), setMcpRegistry: (servers) => set({ mcpRegistry: servers }), setMcpToolRegistry: (tools) => set({ mcpToolRegistry: tools }), }));