Spaces:
Sleeping
Sleeping
| import type { | |
| DemoCard, | |
| DemoLocale, | |
| DemoMode, | |
| DemoTabId, | |
| MockAgentResult, | |
| ProgressStep, | |
| ProgressStepStatus, | |
| TraceItem, | |
| } from './types'; | |
| interface WorkflowTemplate { | |
| label: string; | |
| detail: string; | |
| doneDetail: string; | |
| kind: TraceItem['kind']; | |
| durationMs: number; | |
| } | |
| interface RunMockAgentParams { | |
| tab: DemoTabId; | |
| input: string; | |
| locale: DemoLocale; | |
| mode: DemoMode; | |
| onWorkflowInit: (steps: ProgressStep[]) => void; | |
| onStepStatus: (stepId: string, status: ProgressStepStatus, detail?: string) => void; | |
| onTrace: (item: TraceItem) => void; | |
| } | |
| const WORKFLOWS: Record<DemoLocale, Record<DemoTabId, WorkflowTemplate[]>> = { | |
| en: { | |
| chat: [ | |
| { | |
| label: 'Understanding user intent', | |
| detail: 'Parsing the latest message and session context.', | |
| doneDetail: 'Intent and preferred action recognized.', | |
| kind: 'planner', | |
| durationMs: 600, | |
| }, | |
| { | |
| label: 'Planning tool route', | |
| detail: 'Selecting the best backend function and argument set.', | |
| doneDetail: 'Tool route and arguments prepared.', | |
| kind: 'planner', | |
| durationMs: 700, | |
| }, | |
| { | |
| label: 'Executing tool call', | |
| detail: 'Running product/store/statistics action.', | |
| doneDetail: 'Tool returned structured data.', | |
| kind: 'tool', | |
| durationMs: 900, | |
| }, | |
| { | |
| label: 'Rendering response', | |
| detail: 'Composing concise answer + rich cards.', | |
| doneDetail: 'Final message and cards generated.', | |
| kind: 'renderer', | |
| durationMs: 500, | |
| }, | |
| ], | |
| voice: [ | |
| { | |
| label: 'Normalizing transcript', | |
| detail: 'Detecting language and cleaning transcript text.', | |
| doneDetail: 'Transcript normalized.', | |
| kind: 'validator', | |
| durationMs: 600, | |
| }, | |
| { | |
| label: 'Choosing assistant tool', | |
| detail: 'Mapping spoken intent to product/store workflows.', | |
| doneDetail: 'Voice tool selected.', | |
| kind: 'planner', | |
| durationMs: 650, | |
| }, | |
| { | |
| label: 'Running voice tool', | |
| detail: 'Calling backend action and collecting UI payload.', | |
| doneDetail: 'Voice tool output ready.', | |
| kind: 'tool', | |
| durationMs: 900, | |
| }, | |
| { | |
| label: 'Synthesizing summary', | |
| detail: 'Producing short speech-safe answer.', | |
| doneDetail: 'Final voice summary composed.', | |
| kind: 'renderer', | |
| durationMs: 450, | |
| }, | |
| ], | |
| marketing: [ | |
| { | |
| label: 'Reading campaign brief', | |
| detail: 'Extracting tone, audience, and product context.', | |
| doneDetail: 'Campaign brief parsed.', | |
| kind: 'planner', | |
| durationMs: 700, | |
| }, | |
| { | |
| label: 'Generating marketing assets', | |
| detail: 'Streaming copy and image enhancement instructions.', | |
| doneDetail: 'Marketing draft generated.', | |
| kind: 'tool', | |
| durationMs: 1200, | |
| }, | |
| { | |
| label: 'Quality checks', | |
| detail: 'Verifying CTA clarity and consistency.', | |
| doneDetail: 'Draft passed validation.', | |
| kind: 'validator', | |
| durationMs: 550, | |
| }, | |
| { | |
| label: 'Preparing showcase output', | |
| detail: 'Building final campaign cards and metadata.', | |
| doneDetail: 'Marketing output ready.', | |
| kind: 'renderer', | |
| durationMs: 450, | |
| }, | |
| ], | |
| invoice: [ | |
| { | |
| label: 'Pre-processing invoice image', | |
| detail: 'Normalizing orientation and text contrast.', | |
| doneDetail: 'Image prepared for extraction.', | |
| kind: 'validator', | |
| durationMs: 700, | |
| }, | |
| { | |
| label: 'Extracting structured fields', | |
| detail: 'Detecting invoice number, totals, and dates.', | |
| doneDetail: 'Invoice fields extracted.', | |
| kind: 'tool', | |
| durationMs: 1100, | |
| }, | |
| { | |
| label: 'Verifying consistency', | |
| detail: 'Checking totals, currency, and item count.', | |
| doneDetail: 'Extraction validated.', | |
| kind: 'validator', | |
| durationMs: 650, | |
| }, | |
| { | |
| label: 'Formatting result view', | |
| detail: 'Generating clean JSON + summary cards.', | |
| doneDetail: 'Invoice output rendered.', | |
| kind: 'renderer', | |
| durationMs: 500, | |
| }, | |
| ], | |
| marketplace: [ | |
| { | |
| label: 'Interpreting filters', | |
| detail: 'Parsing category, location, and price limits.', | |
| doneDetail: 'Search filters resolved.', | |
| kind: 'planner', | |
| durationMs: 650, | |
| }, | |
| { | |
| label: 'Querying marketplace data', | |
| detail: 'Searching products and buyer/store entities.', | |
| doneDetail: 'Results retrieved.', | |
| kind: 'tool', | |
| durationMs: 950, | |
| }, | |
| { | |
| label: 'Ranking matches', | |
| detail: 'Sorting by relevance and availability.', | |
| doneDetail: 'Top matches selected.', | |
| kind: 'validator', | |
| durationMs: 600, | |
| }, | |
| { | |
| label: 'Composing answer', | |
| detail: 'Preparing concise brief with cards.', | |
| doneDetail: 'Marketplace summary ready.', | |
| kind: 'renderer', | |
| durationMs: 450, | |
| }, | |
| ], | |
| }, | |
| 'zh-TW': { | |
| chat: [ | |
| { | |
| label: '理解使用者意圖', | |
| detail: '解析最新訊息與對話上下文。', | |
| doneDetail: '意圖與偏好操作已辨識。', | |
| kind: 'planner', | |
| durationMs: 600, | |
| }, | |
| { | |
| label: '規劃工具路由', | |
| detail: '選擇最佳後端函式與參數組合。', | |
| doneDetail: '工具路徑與參數已準備。', | |
| kind: 'planner', | |
| durationMs: 700, | |
| }, | |
| { | |
| label: '執行工具呼叫', | |
| detail: '執行商品、店家或統計操作。', | |
| doneDetail: '工具已回傳結構化資料。', | |
| kind: 'tool', | |
| durationMs: 900, | |
| }, | |
| { | |
| label: '渲染回覆內容', | |
| detail: '整理精簡文字與卡片輸出。', | |
| doneDetail: '最終訊息與卡片已生成。', | |
| kind: 'renderer', | |
| durationMs: 500, | |
| }, | |
| ], | |
| voice: [ | |
| { | |
| label: '標準化語音轉寫', | |
| detail: '偵測語系並清理語音文字。', | |
| doneDetail: '語音文字已標準化。', | |
| kind: 'validator', | |
| durationMs: 600, | |
| }, | |
| { | |
| label: '選擇語音工具', | |
| detail: '將口語意圖映射到商品/市集流程。', | |
| doneDetail: '語音工具已選定。', | |
| kind: 'planner', | |
| durationMs: 650, | |
| }, | |
| { | |
| label: '執行語音工具', | |
| detail: '呼叫動作並收集 UI 載荷。', | |
| doneDetail: '語音工具輸出已就緒。', | |
| kind: 'tool', | |
| durationMs: 900, | |
| }, | |
| { | |
| label: '合成語音摘要', | |
| detail: '產生簡短可口語播報的回覆。', | |
| doneDetail: '最終語音摘要已完成。', | |
| kind: 'renderer', | |
| durationMs: 450, | |
| }, | |
| ], | |
| marketing: [ | |
| { | |
| label: '讀取行銷需求', | |
| detail: '擷取語氣、受眾與產品脈絡。', | |
| doneDetail: '活動需求解析完成。', | |
| kind: 'planner', | |
| durationMs: 700, | |
| }, | |
| { | |
| label: '生成行銷素材', | |
| detail: '串流輸出文案與圖像增強指令。', | |
| doneDetail: '行銷草案已生成。', | |
| kind: 'tool', | |
| durationMs: 1200, | |
| }, | |
| { | |
| label: '品質檢查', | |
| detail: '驗證 CTA 清晰度與內容一致性。', | |
| doneDetail: '草案通過檢查。', | |
| kind: 'validator', | |
| durationMs: 550, | |
| }, | |
| { | |
| label: '封裝展示輸出', | |
| detail: '建立最終活動卡片與執行資訊。', | |
| doneDetail: '行銷輸出已就緒。', | |
| kind: 'renderer', | |
| durationMs: 450, | |
| }, | |
| ], | |
| invoice: [ | |
| { | |
| label: '發票影像前處理', | |
| detail: '標準化方向與文字對比。', | |
| doneDetail: '影像已準備可供擷取。', | |
| kind: 'validator', | |
| durationMs: 700, | |
| }, | |
| { | |
| label: '擷取結構化欄位', | |
| detail: '辨識發票號碼、金額與日期。', | |
| doneDetail: '發票欄位擷取完成。', | |
| kind: 'tool', | |
| durationMs: 1100, | |
| }, | |
| { | |
| label: '一致性驗證', | |
| detail: '檢查總額、幣別與品項數。', | |
| doneDetail: '擷取結果驗證完成。', | |
| kind: 'validator', | |
| durationMs: 650, | |
| }, | |
| { | |
| label: '格式化結果檢視', | |
| detail: '生成乾淨 JSON 與摘要卡片。', | |
| doneDetail: '發票輸出已渲染。', | |
| kind: 'renderer', | |
| durationMs: 500, | |
| }, | |
| ], | |
| marketplace: [ | |
| { | |
| label: '解析篩選條件', | |
| detail: '整理品類、地區與價格限制。', | |
| doneDetail: '搜尋條件已完成解析。', | |
| kind: 'planner', | |
| durationMs: 650, | |
| }, | |
| { | |
| label: '查詢市集資料', | |
| detail: '搜尋商品與買家/店家實體。', | |
| doneDetail: '市集結果已取回。', | |
| kind: 'tool', | |
| durationMs: 950, | |
| }, | |
| { | |
| label: '匹配結果排序', | |
| detail: '依相關性與可用性排序。', | |
| doneDetail: '已選出最佳匹配項目。', | |
| kind: 'validator', | |
| durationMs: 600, | |
| }, | |
| { | |
| label: '組裝最終回覆', | |
| detail: '整理精簡摘要與結果卡片。', | |
| doneDetail: '市集摘要已就緒。', | |
| kind: 'renderer', | |
| durationMs: 450, | |
| }, | |
| ], | |
| }, | |
| }; | |
| const QUICK_PROMPTS: Record<DemoLocale, Record<DemoTabId, string[]>> = { | |
| en: { | |
| chat: [ | |
| 'Add eggs for NT$120 / box, stock 30', | |
| 'Show my latest products', | |
| 'Create a store in Hsinchu for organic buyers', | |
| ], | |
| voice: [ | |
| 'Find products below 200', | |
| 'Update egg stock to 48 boxes', | |
| 'Open analytics for this week', | |
| ], | |
| marketing: [ | |
| 'Generate a fresh campaign for free-range eggs', | |
| 'Create a weekend CTA with short hashtags', | |
| 'Polish this product photo for e-commerce', | |
| ], | |
| invoice: [ | |
| 'Extract invoice fields and confidence', | |
| 'Show invoice summary with line items', | |
| 'Validate invoice totals and due date', | |
| ], | |
| marketplace: [ | |
| 'Search fruits in Taoyuan below 250', | |
| 'Find nearby stores looking for eggs', | |
| 'Show marketplace demand snapshot', | |
| ], | |
| }, | |
| 'zh-TW': { | |
| chat: ['新增雞蛋 120/盒 庫存30', '列出我的最新產品', '新增新竹有機買家店家'], | |
| voice: ['找 200 以下產品', '把雞蛋庫存改成 48 箱', '開啟本週分析'], | |
| marketing: ['幫我做放牧雞蛋促銷活動', '產生週末 CTA 與 Hashtag', '優化這張產品圖成電商風格'], | |
| invoice: ['抽取發票欄位與信心值', '顯示發票摘要與明細', '驗證總額與到期日'], | |
| marketplace: ['搜尋桃園 250 以下水果', '找附近想買雞蛋的店家', '顯示市集供需快照'], | |
| }, | |
| }; | |
| let sequence = 0; | |
| function nextId(prefix: string): string { | |
| sequence += 1; | |
| return `${prefix}-${Date.now().toString(36)}-${sequence.toString(36)}`; | |
| } | |
| function nowLabel(): string { | |
| return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); | |
| } | |
| function sleep(ms: number): Promise<void> { | |
| return new Promise((resolve) => { | |
| setTimeout(resolve, ms); | |
| }); | |
| } | |
| function extractTopic(input: string): string { | |
| const trimmed = input.trim(); | |
| if (!trimmed) return 'seasonal farm products'; | |
| const chunk = trimmed.split(/\s+/).slice(0, 5).join(' '); | |
| return chunk || 'seasonal farm products'; | |
| } | |
| function buildToolPayload(tab: DemoTabId, input: string, mode: DemoMode): Record<string, unknown> { | |
| const topic = extractTopic(input); | |
| const base = { | |
| request: topic, | |
| mode, | |
| latencyMs: Math.floor(620 + Math.random() * 330), | |
| tokens: Math.floor(280 + Math.random() * 150), | |
| }; | |
| switch (tab) { | |
| case 'chat': | |
| return { ...base, toolName: 'product_search_public', resultCount: 4, nextAction: 'refine_filter' }; | |
| case 'voice': | |
| return { ...base, toolName: 'product_update', updatedField: 'stock', followUp: 'confirm_changes' }; | |
| case 'marketing': | |
| return { ...base, toolName: 'marketing_generate_stream', assets: ['headline', 'caption', 'cta', 'image'] }; | |
| case 'invoice': | |
| return { ...base, toolName: 'invoice_scan', confidence: 0.93, fieldsExtracted: 11 }; | |
| case 'marketplace': | |
| return { ...base, toolName: 'marketplace_get_stats', productsMatched: 6, storesMatched: 3 }; | |
| default: | |
| return base; | |
| } | |
| } | |
| function buildCards(tab: DemoTabId, input: string, locale: DemoLocale): DemoCard[] { | |
| const topic = extractTopic(input); | |
| if (tab === 'marketing') { | |
| return [ | |
| { | |
| title: locale === 'zh-TW' ? '活動主題' : 'Campaign Theme', | |
| subtitle: topic, | |
| tags: locale === 'zh-TW' ? ['新品曝光', '週末檔期', '社群貼文'] : ['launch', 'weekend', 'social'], | |
| actions: locale === 'zh-TW' ? ['複製文案', '開啟素材面板'] : ['Copy copy', 'Open assets panel'], | |
| }, | |
| { | |
| title: locale === 'zh-TW' ? '成效預估' : 'Projected Impact', | |
| metrics: [ | |
| { label: locale === 'zh-TW' ? '互動率' : 'Engagement', value: '+24%' }, | |
| { label: locale === 'zh-TW' ? '點擊率' : 'CTR', value: '+11%' }, | |
| { label: locale === 'zh-TW' ? '轉換率' : 'Conversion', value: '+7%' }, | |
| ], | |
| }, | |
| ]; | |
| } | |
| if (tab === 'invoice') { | |
| return [ | |
| { | |
| title: locale === 'zh-TW' ? '發票摘要' : 'Invoice Summary', | |
| subtitle: locale === 'zh-TW' ? '已抽取主要欄位' : 'Core fields extracted', | |
| metrics: [ | |
| { label: locale === 'zh-TW' ? '總額' : 'Total', value: 'NT$ 12,860' }, | |
| { label: locale === 'zh-TW' ? '稅額' : 'Tax', value: 'NT$ 643' }, | |
| { label: locale === 'zh-TW' ? '信心值' : 'Confidence', value: '93%' }, | |
| ], | |
| actions: locale === 'zh-TW' ? ['建立發票紀錄', '匯出 CSV'] : ['Create record', 'Export CSV'], | |
| }, | |
| ]; | |
| } | |
| if (tab === 'marketplace') { | |
| return [ | |
| { | |
| title: locale === 'zh-TW' ? '放牧雞蛋' : 'Free-range Eggs', | |
| subtitle: locale === 'zh-TW' ? '新竹・可週配' : 'Hsinchu • weekly delivery', | |
| metrics: [ | |
| { label: locale === 'zh-TW' ? '單價' : 'Price', value: locale === 'zh-TW' ? 'NT$ 118 / 盒' : 'NT$ 118 / box' }, | |
| { label: locale === 'zh-TW' ? '庫存' : 'Stock', value: '42' }, | |
| ], | |
| tags: [topic, locale === 'zh-TW' ? '熱門' : 'trending'], | |
| }, | |
| { | |
| title: locale === 'zh-TW' ? '有機蔬菜組' : 'Organic Veg Pack', | |
| subtitle: locale === 'zh-TW' ? '桃園・當日採收' : 'Taoyuan • same-day harvest', | |
| metrics: [ | |
| { label: locale === 'zh-TW' ? '單價' : 'Price', value: locale === 'zh-TW' ? 'NT$ 220 / 組' : 'NT$ 220 / set' }, | |
| { label: locale === 'zh-TW' ? '庫存' : 'Stock', value: '18' }, | |
| ], | |
| }, | |
| ]; | |
| } | |
| if (tab === 'voice') { | |
| return [ | |
| { | |
| title: locale === 'zh-TW' ? '語音動作完成' : 'Voice Action Completed', | |
| subtitle: locale === 'zh-TW' ? '已套用您的口語指令' : 'Applied your spoken command', | |
| actions: locale === 'zh-TW' ? ['再次修改', '查看產品'] : ['Edit again', 'Open product'], | |
| }, | |
| ]; | |
| } | |
| return [ | |
| { | |
| title: locale === 'zh-TW' ? '代理回覆摘要' : 'Agent Response Snapshot', | |
| subtitle: topic, | |
| metrics: [ | |
| { label: locale === 'zh-TW' ? '路由' : 'Route', value: locale === 'zh-TW' ? '工具優先' : 'Tool-first' }, | |
| { label: locale === 'zh-TW' ? '狀態' : 'Status', value: locale === 'zh-TW' ? '已完成' : 'Completed' }, | |
| ], | |
| actions: locale === 'zh-TW' ? ['複製結果', '繼續追問'] : ['Copy result', 'Ask follow-up'], | |
| }, | |
| ]; | |
| } | |
| function buildPayload(tab: DemoTabId, locale: DemoLocale): Record<string, unknown> { | |
| if (tab === 'invoice') { | |
| return { | |
| provider: 'gemini', | |
| model: 'gemini-2.5-flash', | |
| invoice: { | |
| invoiceNumber: 'FTL-2026-0312-09', | |
| counterpartyName: locale === 'zh-TW' ? '新竹青禾食堂' : 'Qinghe Kitchen', | |
| issueDate: '2026-03-11', | |
| dueDate: '2026-03-25', | |
| currency: 'TWD', | |
| total: 12860, | |
| paid: 0, | |
| }, | |
| confidence: 0.93, | |
| }; | |
| } | |
| if (tab === 'marketing') { | |
| return { | |
| headline: locale === 'zh-TW' ? '新鮮直送,今天就吃到安心蛋' : 'Fresh from farm, on your shelf today', | |
| cta: locale === 'zh-TW' ? '立即預購本週產地直送' : 'Pre-order this week\'s harvest now', | |
| hashtags: | |
| locale === 'zh-TW' | |
| ? ['#產地直送', '#放牧雞蛋', '#Farm2Market'] | |
| : ['#FarmFresh', '#DirectFromFarm', '#Farm2Market'], | |
| }; | |
| } | |
| if (tab === 'marketplace') { | |
| return { | |
| query: locale === 'zh-TW' ? '市集商品搜尋' : 'Marketplace search', | |
| stats: { | |
| sellers: 62, | |
| buyers: 87, | |
| products: 426, | |
| stores: 198, | |
| }, | |
| ranking: locale === 'zh-TW' ? '價格 + 庫存 + 地區' : 'price + stock + location', | |
| }; | |
| } | |
| return { | |
| mode: tab, | |
| summary: locale === 'zh-TW' ? '已完成工具流程並輸出結果。' : 'Tool workflow completed and response generated.', | |
| }; | |
| } | |
| function buildSummary(tab: DemoTabId, locale: DemoLocale, input: string): string { | |
| const topic = extractTopic(input); | |
| if (locale === 'zh-TW') { | |
| switch (tab) { | |
| case 'voice': | |
| return `已完成語音指令處理,並把「${topic}」轉成可執行動作。`; | |
| case 'marketing': | |
| return `行銷草案與素材已生成,主題聚焦在「${topic}」,可直接進行 A/B 測試。`; | |
| case 'invoice': | |
| return `發票欄位已抽取並完成一致性檢查,建議下一步可直接建立帳務紀錄。`; | |
| case 'marketplace': | |
| return `已完成市集搜尋與排序,先展示最符合「${topic}」條件的供應項目。`; | |
| default: | |
| return `流程已完成:我先做工具規劃,再回覆「${topic}」的可執行結果。`; | |
| } | |
| } | |
| switch (tab) { | |
| case 'voice': | |
| return `Voice command execution completed. "${topic}" was converted into an actionable workflow.`; | |
| case 'marketing': | |
| return `Marketing draft and assets are ready, tuned around "${topic}", and suitable for quick A/B testing.`; | |
| case 'invoice': | |
| return 'Invoice fields were extracted and validated. Next step can directly create an accounting record.'; | |
| case 'marketplace': | |
| return `Marketplace search and ranking finished. Top matches for "${topic}" are now prioritized.`; | |
| default: | |
| return `Completed the full tool-first workflow and produced an actionable response for "${topic}".`; | |
| } | |
| } | |
| function buildTraceItem( | |
| template: WorkflowTemplate, | |
| status: TraceItem['status'], | |
| detail: string, | |
| payload?: Record<string, unknown> | |
| ): TraceItem { | |
| return { | |
| id: nextId('trace'), | |
| kind: template.kind, | |
| title: template.label, | |
| detail, | |
| status, | |
| payload, | |
| timestamp: nowLabel(), | |
| }; | |
| } | |
| export function getQuickPrompts(tab: DemoTabId, locale: DemoLocale): string[] { | |
| return QUICK_PROMPTS[locale][tab]; | |
| } | |
| export function buildWelcomeText(tab: DemoTabId, locale: DemoLocale): string { | |
| if (locale === 'zh-TW') { | |
| switch (tab) { | |
| case 'voice': | |
| return '語音代理已就緒。輸入一句口語指令,我會顯示推理進度與工具結果。'; | |
| case 'marketing': | |
| return '行銷工作台已就緒。可測試文案、CTA 與圖片優化流程。'; | |
| case 'invoice': | |
| return '發票 AI 已就緒。可展示 OCR 抽取與欄位驗證。'; | |
| case 'marketplace': | |
| return '市集代理已就緒。可示範搜尋、排序與統計摘要。'; | |
| default: | |
| return 'Farm2Market AI Demo 已就緒。輸入需求,我會顯示模型處理進度。'; | |
| } | |
| } | |
| switch (tab) { | |
| case 'voice': | |
| return 'Voice agent is ready. Send a spoken-style command and watch live step tracking.'; | |
| case 'marketing': | |
| return 'Marketing studio is ready. Test copy, CTA, and image-generation workflows.'; | |
| case 'invoice': | |
| return 'Invoice AI is ready. Demonstrate OCR extraction and validation steps.'; | |
| case 'marketplace': | |
| return 'Marketplace agent is ready. Demonstrate search, ranking, and stats summaries.'; | |
| default: | |
| return 'Farm2Market AI Demo is ready. Send a request to see model progress in real time.'; | |
| } | |
| } | |
| export async function runMockAgent(params: RunMockAgentParams): Promise<MockAgentResult> { | |
| const { tab, input, locale, mode, onStepStatus, onTrace, onWorkflowInit } = params; | |
| const workflow = WORKFLOWS[locale][tab]; | |
| const steps: ProgressStep[] = workflow.map((item, index) => ({ | |
| id: `step-${index + 1}`, | |
| label: item.label, | |
| detail: item.detail, | |
| status: 'pending', | |
| })); | |
| onWorkflowInit(steps); | |
| for (let index = 0; index < workflow.length; index += 1) { | |
| const template = workflow[index]; | |
| const stepId = steps[index].id; | |
| onStepStatus(stepId, 'in_progress', template.detail); | |
| onTrace(buildTraceItem(template, 'running', template.detail)); | |
| await sleep(template.durationMs); | |
| const payload = template.kind === 'tool' ? buildToolPayload(tab, input, mode) : undefined; | |
| onStepStatus(stepId, 'completed', template.doneDetail); | |
| onTrace(buildTraceItem(template, 'ok', template.doneDetail, payload)); | |
| } | |
| return { | |
| summary: buildSummary(tab, locale, input), | |
| cards: buildCards(tab, input, locale), | |
| payload: buildPayload(tab, locale), | |
| }; | |
| } | |