|
|
@@ -11,7 +11,7 @@ function randomUUID(): string {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-export type ContextPanelMode = 'idle' | 'loading' | 'task' | 'error' | 'completed';
|
|
|
+export type ContextPanelMode = 'idle' | 'thinking' | 'loading' | 'task' | 'error' | 'completed';
|
|
|
export type ConnectionStatus = 'online' | 'disconnected';
|
|
|
|
|
|
export interface CardPreview {
|
|
|
@@ -19,6 +19,8 @@ export interface CardPreview {
|
|
|
cardKey: string;
|
|
|
title: string;
|
|
|
subtitle: string;
|
|
|
+ timestamp: string;
|
|
|
+ _seq?: number;
|
|
|
}
|
|
|
|
|
|
const CARD_TITLE_MAP: Record<string, string> = {
|
|
|
@@ -37,6 +39,7 @@ export const useTerminalStore = defineStore('terminal', {
|
|
|
conversationId: '' as string,
|
|
|
currentTraceId: '' as string,
|
|
|
messages: [] as ChatMessage[],
|
|
|
+ _timelineSeq: 0,
|
|
|
streaming: false,
|
|
|
task: null as TaskState | null,
|
|
|
activeCard: null as CardInstance | null,
|
|
|
@@ -55,9 +58,10 @@ export const useTerminalStore = defineStore('terminal', {
|
|
|
getters: {
|
|
|
contextPanelMode(state): ContextPanelMode {
|
|
|
if (state.errorMessage) return 'error';
|
|
|
- if (state.sending && !state.activeCard && !state.viewingCardId) return 'loading';
|
|
|
+ if (state.sending && !state.activeCard && !state.viewingCardId) return 'thinking';
|
|
|
if (this.displayedCard?.cardKey === 'appointment-success') return 'completed';
|
|
|
if (this.displayedCard) return 'task';
|
|
|
+ if (state.sending || state.streaming) return 'thinking';
|
|
|
return 'idle';
|
|
|
},
|
|
|
displayedCard(state): CardInstance | null {
|
|
|
@@ -72,6 +76,7 @@ export const useTerminalStore = defineStore('terminal', {
|
|
|
},
|
|
|
actions: {
|
|
|
addMessage(message: ChatMessage) {
|
|
|
+ message._seq = ++this._timelineSeq;
|
|
|
this.messages.push(message);
|
|
|
},
|
|
|
appendAssistantDelta(text: string) {
|
|
|
@@ -85,6 +90,8 @@ export const useTerminalStore = defineStore('terminal', {
|
|
|
role: 'assistant',
|
|
|
content: text,
|
|
|
streaming: true,
|
|
|
+ timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
|
|
+ _seq: ++this._timelineSeq,
|
|
|
});
|
|
|
},
|
|
|
completeAssistantMessage() {
|
|
|
@@ -108,20 +115,38 @@ export const useTerminalStore = defineStore('terminal', {
|
|
|
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 || '';
|
|
|
+ const docs = data.doctors as Array<{ name: string; title: string; doctorName?: string }> | undefined;
|
|
|
+ const top = docs?.[0];
|
|
|
+ if (top) {
|
|
|
+ const dept = (this.selection.departmentName as string) || '';
|
|
|
+ subtitle = `${top.name || top.doctorName || ''} ${top.title || ''}`.trim();
|
|
|
+ if (dept) subtitle += ` · ${dept}`;
|
|
|
+ }
|
|
|
} else if (cardKey === 'time-slot-selection') {
|
|
|
const slots = data.slots as Array<{ timePeriod: string }> | undefined;
|
|
|
+ const amount = data.amountYuan ?? data.amount ?? '';
|
|
|
subtitle = slots?.[0]?.timePeriod || '';
|
|
|
+ if (subtitle && amount) subtitle += ` · ¥${amount}`;
|
|
|
} else if (cardKey === 'confirm-appointment') {
|
|
|
- subtitle = '请确认预约信息';
|
|
|
+ const dept = (this.selection.departmentName as string) || (data.departmentName as string) || '';
|
|
|
+ const doc = (this.selection.doctorName as string) || (data.doctorName as string) || '';
|
|
|
+ const date = (data.visitDate as string) || '';
|
|
|
+ const time = (this.selection.timePeriod as string) || (data.visitTime as string) || '';
|
|
|
+ const amt = data.amountYuan ?? data.amount ?? '25.00';
|
|
|
+ subtitle = [dept, doc, date + (date && time ? ' ' : '') + time, `¥${amt}`].filter(Boolean).join(' · ');
|
|
|
} else if (cardKey === 'payment-qrcode') {
|
|
|
const amount = data.amountYuan || '0.00';
|
|
|
subtitle = `Mock支付 ¥${amount}`;
|
|
|
} else if (cardKey === 'appointment-success') {
|
|
|
- subtitle = '预约已确认';
|
|
|
+ const aptNo = (data.appointmentNo as string) || (data.appointmentNoStr as string) || '';
|
|
|
+ const visitDate = (data.visitDate as string) || (data.visitTime as string) || '';
|
|
|
+ const visitTime = (data.timePeriod as string) || (this.selection.timePeriod as string) || '';
|
|
|
+ const docName = (data.doctorName as string) || (this.selection.doctorName as string) || '';
|
|
|
+ const docTitle = (data.doctorTitle as string) || (this.selection.doctorTitle as string) || '';
|
|
|
+ const parts = [aptNo, visitDate + visitTime, docName + (docTitle ? ' ' + docTitle : '')].filter(Boolean);
|
|
|
+ subtitle = parts.join(' · ') || '预约已确认';
|
|
|
}
|
|
|
- this.cardPreviews.push({ id: card.cardInstanceId, cardKey, title, subtitle });
|
|
|
+ this.cardPreviews.push({ id: card.cardInstanceId, cardKey, title, subtitle, timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), _seq: ++this._timelineSeq });
|
|
|
},
|
|
|
setError(message: string, traceId = '') {
|
|
|
this.errorMessage = message;
|
|
|
@@ -196,7 +221,7 @@ export const useTerminalStore = defineStore('terminal', {
|
|
|
|
|
|
async sendMessage(text: string): Promise<void> {
|
|
|
if (this.sending) return; // 防重入:正在发送中不允许重复调用
|
|
|
- this.addMessage({ id: randomUUID(), role: 'user', content: text });
|
|
|
+ this.addMessage({ id: randomUUID(), role: 'user', content: text, timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) });
|
|
|
this.errorMessage = '';
|
|
|
this.sending = true;
|
|
|
this.streaming = true;
|
|
|
@@ -223,6 +248,75 @@ export const useTerminalStore = defineStore('terminal', {
|
|
|
});
|
|
|
},
|
|
|
|
|
|
+ addSelectionBubble(payload: Record<string, unknown>) {
|
|
|
+ const cardKey = this.activeCard?.cardKey ?? '';
|
|
|
+ let text = '';
|
|
|
+ if (cardKey === 'department-selection') text = (payload.departmentName as string) || '';
|
|
|
+ else if (cardKey === 'doctor-selection') text = `${payload.doctorTitle || ''} ${payload.doctorName || ''}`.trim();
|
|
|
+ else if (cardKey === 'time-slot-selection') text = (payload.timePeriod as string) || '';
|
|
|
+ else if (cardKey === 'confirm-appointment') text = '确认挂号';
|
|
|
+ else if (cardKey === 'payment-qrcode') return;
|
|
|
+ else text = JSON.stringify(payload);
|
|
|
+ if (text) {
|
|
|
+ this.addMessage({
|
|
|
+ id: randomUUID(),
|
|
|
+ role: 'user',
|
|
|
+ content: text,
|
|
|
+ timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ addTransitionMessage(nextCard: CardInstance) {
|
|
|
+ const currentKey = this.activeCard?.cardKey ?? '';
|
|
|
+ const nextKey = nextCard.cardKey;
|
|
|
+ let text = '';
|
|
|
+ if (currentKey === 'department-selection' && nextKey === 'doctor-selection') {
|
|
|
+ const deptName = (this.selection.departmentName as string) || '';
|
|
|
+ const docs = nextCard.cardData.doctors as Array<{ name: string; title: string }> | undefined;
|
|
|
+ if (docs?.[0]) {
|
|
|
+ text = `好的,以下是${deptName}可预约的医生。根据您的症状,建议优先选择${docs[0].title} ${docs[0].name}`;
|
|
|
+ } else {
|
|
|
+ text = `好的,以下是${deptName}可预约的医生`;
|
|
|
+ }
|
|
|
+ } else if (currentKey === 'doctor-selection' && nextKey === 'time-slot-selection') {
|
|
|
+ const dn = this.selection.doctorName || '';
|
|
|
+ const dt = this.selection.doctorTitle || '';
|
|
|
+ text = `好的,以下是${dt}${dn}的可预约时段`;
|
|
|
+ } else if (currentKey === 'time-slot-selection' && nextKey === 'confirm-appointment') {
|
|
|
+ text = '好的,请确认以下预约信息';
|
|
|
+ } else if (currentKey === 'confirm-appointment' && nextKey === 'payment-qrcode') {
|
|
|
+ text = '预约信息已确认,请扫码支付挂号费';
|
|
|
+ } else if (currentKey === 'payment-qrcode' && nextKey === 'appointment-success') {
|
|
|
+ text = '支付成功。您的挂号已确认,请按时到诊区签到候诊。系统将根据您的位置自动提醒签到。';
|
|
|
+ }
|
|
|
+ if (!text) return Promise.resolve();
|
|
|
+ return new Promise<void>((resolve) => {
|
|
|
+ const msgId = randomUUID();
|
|
|
+ const isSuccess = currentKey === 'payment-qrcode' && nextKey === 'appointment-success';
|
|
|
+ this.messages.push({
|
|
|
+ id: msgId,
|
|
|
+ role: 'assistant',
|
|
|
+ content: '',
|
|
|
+ streaming: true,
|
|
|
+ timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
|
|
+ _seq: ++this._timelineSeq,
|
|
|
+ _type: isSuccess ? 'success' : undefined,
|
|
|
+ });
|
|
|
+ let i = 0;
|
|
|
+ const timer = setInterval(() => {
|
|
|
+ const msg = this.messages.find((m) => m.id === msgId);
|
|
|
+ if (!msg) { clearInterval(timer); resolve(); return; }
|
|
|
+ msg.content += text[i++];
|
|
|
+ if (i >= text.length) {
|
|
|
+ clearInterval(timer);
|
|
|
+ msg.streaming = false;
|
|
|
+ resolve();
|
|
|
+ }
|
|
|
+ }, 40);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
async submitCard(payload: Record<string, unknown>): Promise<void> {
|
|
|
if (!this.activeCard || this.cardActionSubmitting) return;
|
|
|
|
|
|
@@ -252,6 +346,7 @@ export const useTerminalStore = defineStore('terminal', {
|
|
|
|
|
|
if (result.nextCard?.cardInstanceId) {
|
|
|
const fullCard = await fetchCard(result.nextCard.cardInstanceId);
|
|
|
+ await this.addTransitionMessage(fullCard);
|
|
|
this.setActiveCard(fullCard);
|
|
|
}
|
|
|
} catch (err) {
|