| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- 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<string, string> = {
- '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<string, string>,
- }),
- 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<string, unknown>) {
- 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<void> {
- 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<string, unknown>): Promise<void> {
- 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;
- }
- },
- },
- });
|