import { defineStore } from 'pinia'; import type { CardInstance, ChatMessage, TaskState } from '../api/types'; import { connectChatStream, type ParsedSseEvent } from '../api/sse'; import { fetchCard, submitCardAction } from '../api/client'; export type ContextPanelMode = 'idle' | 'loading' | 'task' | 'error' | 'completed'; export type ConnectionStatus = 'online' | 'disconnected'; export interface CardPreview { id: string; cardKey: string; title: string; subtitle: string; } const CARD_TITLE_MAP: Record = { 'department-selection': '科室推荐', 'doctor-selection': '医生选择', 'time-slot-selection': '就诊时间', 'confirm-appointment': '确认预约', 'payment-qrcode': '支付', 'appointment-success': '挂号成功', }; export const useTerminalStore = defineStore('terminal', { state: () => ({ deviceId: 'EMOON-WEB-DEMO-001', deviceType: 'self_service_kiosk', conversationId: '' as string, currentTraceId: '' as string, messages: [] as ChatMessage[], streaming: false, task: null as TaskState | null, activeCard: null as CardInstance | null, cardHistory: [] as CardInstance[], viewingCardId: null as string | null, cardPreviews: [] as CardPreview[], errorMessage: '' as string, connectionStatus: 'online' as ConnectionStatus, inputText: '', pendingActionKey: '', demoMode: import.meta.env.VITE_DEMO_MODE === 'true', sending: false, selection: {} as Record, }), getters: { contextPanelMode(state): ContextPanelMode { if (state.errorMessage) return 'error'; if (state.sending && !state.activeCard && !state.viewingCardId) return 'loading'; if (this.displayedCard?.cardKey === 'appointment-success') return 'completed'; if (this.displayedCard) return 'task'; return 'idle'; }, displayedCard(state): CardInstance | null { if (state.viewingCardId) { return state.cardHistory.find((c) => c.cardInstanceId === state.viewingCardId) ?? state.activeCard; } return state.activeCard; }, isDemo(): boolean { return this.demoMode; }, }, actions: { addMessage(message: ChatMessage) { this.messages.push(message); }, appendAssistantDelta(text: string) { const last = this.messages[this.messages.length - 1]; if (last?.role === 'assistant' && last.streaming) { last.content += text; return; } this.messages.push({ id: crypto.randomUUID(), role: 'assistant', content: text, streaming: true, }); }, completeAssistantMessage() { const last = this.messages[this.messages.length - 1]; if (last?.role === 'assistant') last.streaming = false; }, setTask(task: TaskState) { this.task = task; }, setActiveCard(card: CardInstance) { this.activeCard = card; this.viewingCardId = null; this.cardHistory.push(card); this.errorMessage = ''; // Push mini card preview for chat feed const cardKey = card.cardKey; const title = CARD_TITLE_MAP[cardKey] || cardKey; const data = card.cardData || {}; let subtitle = ''; if (cardKey === 'department-selection') { const depts = data.departments as Array<{ name: string }> | undefined; subtitle = depts?.[0]?.name ? `${depts[0].name}${depts.length > 1 ? '等' + depts.length + '个科室' : ''}` : ''; } else if (cardKey === 'doctor-selection') { const docs = data.doctors as Array<{ doctorName: string }> | undefined; subtitle = docs?.[0]?.doctorName || ''; } else if (cardKey === 'time-slot-selection') { const slots = data.slots as Array<{ timePeriod: string }> | undefined; subtitle = slots?.[0]?.timePeriod || ''; } else if (cardKey === 'confirm-appointment') { subtitle = '请确认预约信息'; } else if (cardKey === 'payment-qrcode') { const amount = data.amountYuan || '0.00'; subtitle = `Mock支付 ¥${amount}`; } else if (cardKey === 'appointment-success') { subtitle = '预约已确认'; } this.cardPreviews.push({ id: card.cardInstanceId, cardKey, title, subtitle }); }, setError(message: string, traceId = '') { this.errorMessage = message; this.currentTraceId = traceId; }, viewCard(cardInstanceId: string | null) { this.viewingCardId = cardInstanceId; }, resetToIdle() { this.task = null; this.activeCard = null; this.viewingCardId = null; this.cardHistory = []; this.cardPreviews = []; this.errorMessage = ''; this.pendingActionKey = ''; this.selection = {}; }, saveSelection(payload: Record) { for (const [key, value] of Object.entries(payload)) { if (typeof value === 'string') { this.selection[key] = value; } } }, handleSseEvent(event: ParsedSseEvent) { switch (event.type) { case 'task_updated': { const data = event.data as unknown as TaskState; this.setTask(data); break; } case 'message_delta': { const text = event.data.text as string; if (text) this.appendAssistantDelta(text); this.streaming = true; break; } case 'message_completed': { this.completeAssistantMessage(); this.streaming = false; if (event.data.conversationId) { this.conversationId = event.data.conversationId as string; } break; } case 'card_created': { const cardInstanceId = event.data.cardInstanceId as string; if (cardInstanceId) { fetchCard(cardInstanceId) .then((card) => this.setActiveCard(card)) .catch((err) => this.setError(err.message)); } break; } case 'error': { this.setError( (event.data.message as string) ?? '未知错误', (event.data.traceId as string) ?? '', ); this.streaming = false; break; } case 'completed': { this.streaming = false; break; } } }, async sendMessage(text: string): Promise { this.addMessage({ id: crypto.randomUUID(), role: 'user', content: text }); this.errorMessage = ''; this.sending = true; this.streaming = true; return new Promise((resolve, reject) => { connectChatStream({ deviceId: this.deviceId, deviceType: this.deviceType, text, conversationId: this.conversationId || undefined, onEvent: (event) => this.handleSseEvent(event), onError: (error) => { this.setError(error.message); this.streaming = false; this.sending = false; reject(error); }, onComplete: () => { this.streaming = false; this.sending = false; resolve(); }, }); }); }, async submitCard(payload: Record): Promise { if (!this.activeCard) return; const actionName = this.activeCard.actions[0]?.actionName; if (!actionName) return; this.pendingActionKey = `${this.activeCard.cardInstanceId}-${actionName}`; this.errorMessage = ''; try { const result = await submitCardAction( this.activeCard.cardInstanceId, actionName, payload, ); if (result.nextCard?.cardInstanceId) { const fullCard = await fetchCard(result.nextCard.cardInstanceId); this.setActiveCard(fullCard); } } catch (err) { this.setError((err as Error).message); throw err; } }, }, });