import { callVoiceTool, sendVoiceAssistantMessage } from './liveApi'; import type { LiveVoiceToolResponse } from './liveApi'; import type { DemoCard, DemoLocale, DemoTabId, MockAgentResult, ProgressStep, ProgressStepStatus, TraceItem } from './types'; type RunLiveAgentParams = { tab: DemoTabId; input: string; locale: DemoLocale; onWorkflowInit: (steps: ProgressStep[]) => void; onStepStatus: (stepId: string, status: ProgressStepStatus, detail?: string) => void; onTrace: (item: TraceItem) => void; }; function nowLabel(): string { return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } let sequence = 0; function nextId(prefix: string): string { sequence += 1; return `${prefix}-${Date.now().toString(36)}-${sequence.toString(36)}`; } function trace(kind: TraceItem['kind'], title: string, detail: string, status: TraceItem['status'], payload?: Record): TraceItem { return { id: nextId('trace'), kind, title, detail, status, payload, timestamp: nowLabel(), }; } function uiActions(ui: Record): string[] { const actions = Array.isArray(ui.actions) ? ui.actions : []; return actions .map((entry) => { if (typeof entry !== 'object' || !entry) return ''; const label = (entry as { label?: unknown }).label; return typeof label === 'string' ? label : ''; }) .filter(Boolean) .slice(0, 4); } function toMetric(label: string, value: unknown): { label: string; value: string } { return { label, value: typeof value === 'number' ? String(value) : typeof value === 'string' ? value : '-' }; } function uiToCards(ui: Record | undefined, locale: DemoLocale): DemoCard[] { if (!ui) return []; const kind = typeof ui.kind === 'string' ? ui.kind : ''; const title = typeof ui.title === 'string' ? ui.title : locale === 'zh-TW' ? '工具結果' : 'Tool Output'; const description = typeof ui.description === 'string' ? ui.description : undefined; const actions = uiActions(ui); if (kind === 'stats' || kind === 'marketplace_stats') { const items = Array.isArray(ui.items) ? ui.items : []; const metrics = items .map((item) => { if (typeof item !== 'object' || !item) return null; const label = (item as { label?: unknown }).label; const value = (item as { value?: unknown }).value; return typeof label === 'string' ? toMetric(label, value) : null; }) .filter((metric): metric is { label: string; value: string } => Boolean(metric)) .slice(0, 6); return [{ title, subtitle: description, metrics, actions }]; } if (kind === 'product_created' || kind === 'product_updated') { const product = typeof ui.product === 'object' && ui.product ? (ui.product as Record) : {}; const metrics = [ toMetric(locale === 'zh-TW' ? '價格' : 'Price', product.price), toMetric(locale === 'zh-TW' ? '庫存' : 'Stock', product.stock), toMetric(locale === 'zh-TW' ? '分類' : 'Category', product.category), ]; return [ { title, subtitle: typeof product.name === 'string' ? product.name : description, metrics, actions, }, ]; } if (kind === 'product_list' || kind === 'store_list' || kind === 'marketing_list') { const items = Array.isArray(ui.items) ? ui.items : []; const cards = items.slice(0, 3).map((item, index) => { if (typeof item !== 'object' || !item) { return { title: `${title} ${index + 1}` } as DemoCard; } const row = item as Record; const rowTitle = typeof row.name === 'string' ? row.name : typeof row.title === 'string' ? row.title : `${title} ${index + 1}`; const metrics: Array<{ label: string; value: string }> = []; if (row.price !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '價格' : 'Price', row.price)); if (row.stock !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '庫存' : 'Stock', row.stock)); if (row.location !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '地區' : 'Location', row.location)); if (row.type !== undefined) metrics.push(toMetric(locale === 'zh-TW' ? '類型' : 'Type', row.type)); return { title: rowTitle, subtitle: typeof row.description === 'string' ? row.description : undefined, metrics: metrics.length ? metrics : undefined, } as DemoCard; }); return cards.length ? cards : [{ title, subtitle: description, actions }]; } return [{ title, subtitle: description, actions }]; } function assistantSummary(messages: Array<{ role?: unknown; text?: unknown }>, locale: DemoLocale): string { const textList = messages .filter((message) => message && message.role === 'assistant' && typeof message.text === 'string') .map((message) => String(message.text).trim()) .filter(Boolean); if (!textList.length) { return locale === 'zh-TW' ? '工具流程已完成。' : 'Tool workflow finished.'; } return textList[textList.length - 1]; } export async function runLiveAgent(params: RunLiveAgentParams): Promise { const { tab, input, locale, onWorkflowInit, onStepStatus, onTrace } = params; if (tab !== 'chat') { throw new Error('Live agent is currently supported for chat tab only.'); } const steps: ProgressStep[] = [ { id: 'live-step-1', label: locale === 'zh-TW' ? '呼叫語音助理後端' : 'Call voice assistant backend', detail: locale === 'zh-TW' ? '傳送需求並等待回覆。' : 'Submitting request and waiting for response.', status: 'pending', }, { id: 'live-step-2', label: locale === 'zh-TW' ? '補充工具資料' : 'Fetch supplemental tool data', detail: locale === 'zh-TW' ? '取得即時市集統計。' : 'Fetching live marketplace stats.', status: 'pending', }, { id: 'live-step-3', label: locale === 'zh-TW' ? '渲染回覆卡片' : 'Render response cards', detail: locale === 'zh-TW' ? '整理訊息與結構化輸出。' : 'Formatting messages and structured payload.', status: 'pending', }, ]; onWorkflowInit(steps); onStepStatus(steps[0].id, 'in_progress', steps[0].detail); onTrace( trace( 'planner', locale === 'zh-TW' ? '聊天請求' : 'Chat request', locale === 'zh-TW' ? '正在呼叫 /api/voice/assistant。' : 'Calling /api/voice/assistant.', 'running' ) ); const assistant = await sendVoiceAssistantMessage({ message: input, locale, pageRoute: '/hf-demo/chat', pageContext: locale === 'zh-TW' ? 'Hugging Face Chat Agent Demo' : 'Hugging Face Chat Agent Demo', }); onStepStatus(steps[0].id, 'completed', locale === 'zh-TW' ? '已收到助理回覆。' : 'Assistant response received.'); onTrace( trace( 'planner', locale === 'zh-TW' ? '聊天回覆' : 'Chat response', locale === 'zh-TW' ? '語音助理回覆已到達。' : 'Voice assistant returned output.', 'ok', { messageCount: assistant.messages?.length ?? 0, uiKind: assistant.ui?.kind || null } ) ); onStepStatus(steps[1].id, 'in_progress', steps[1].detail); onTrace( trace( 'tool', locale === 'zh-TW' ? '補充工具查詢' : 'Supplemental tool query', locale === 'zh-TW' ? '呼叫 marketplace_get_stats。' : 'Calling marketplace_get_stats.', 'running' ) ); let statsPayload: LiveVoiceToolResponse | null = null; try { statsPayload = await callVoiceTool({ name: 'marketplace_get_stats', locale, }); onStepStatus(steps[1].id, 'completed', locale === 'zh-TW' ? '即時統計已取得。' : 'Live stats fetched.'); onTrace( trace( 'tool', locale === 'zh-TW' ? '統計工具結果' : 'Stats tool result', locale === 'zh-TW' ? '市集統計已附加。' : 'Marketplace stats attached.', 'ok', { uiKind: statsPayload.ui?.kind || null } ) ); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown live tool error'; onStepStatus(steps[1].id, 'error', message); onTrace( trace( 'tool', locale === 'zh-TW' ? '統計工具失敗' : 'Stats tool failed', message, 'error' ) ); } onStepStatus(steps[2].id, 'in_progress', steps[2].detail); onTrace( trace( 'renderer', locale === 'zh-TW' ? '結果渲染' : 'Result rendering', locale === 'zh-TW' ? '正在整理卡片與輸出。' : 'Formatting cards and payload.', 'running' ) ); const cards = [ ...uiToCards(assistant.ui as Record | undefined, locale), ...uiToCards(statsPayload?.ui as Record | undefined, locale), ]; const summary = assistantSummary(assistant.messages || [], locale); const payload: Record = { assistant, statsPayload, source: 'live-backend', }; onStepStatus(steps[2].id, 'completed', locale === 'zh-TW' ? '即時回覆已完成。' : 'Live response rendered.'); onTrace( trace( 'renderer', locale === 'zh-TW' ? '渲染完成' : 'Rendering complete', locale === 'zh-TW' ? '回覆內容與載荷已就緒。' : 'Response and payload are ready.', 'ok', { cards: cards.length } ) ); return { summary, cards, payload, }; }