Farm2Market / src /mockAgent.ts
pixel3user
Update Farm2Market demo frontend
806144f
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),
};
}