Spaces:
Sleeping
Sleeping
| 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<string, unknown>): TraceItem { | |
| return { | |
| id: nextId('trace'), | |
| kind, | |
| title, | |
| detail, | |
| status, | |
| payload, | |
| timestamp: nowLabel(), | |
| }; | |
| } | |
| function uiActions(ui: Record<string, unknown>): 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<string, unknown> | 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<string, unknown>) : {}; | |
| 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<string, unknown>; | |
| 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<MockAgentResult> { | |
| 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<string, unknown> | undefined, locale), | |
| ...uiToCards(statsPayload?.ui as Record<string, unknown> | undefined, locale), | |
| ]; | |
| const summary = assistantSummary(assistant.messages || [], locale); | |
| const payload: Record<string, unknown> = { | |
| 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, | |
| }; | |
| } | |