Farm2Market / src /liveApi.ts
pixel3user
Update Farm2Market demo frontend
e46c4bd
import { getApp, getApps, initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously } from 'firebase/auth';
import type { DemoLocale } from './types';
type FirebaseEnvConfig = {
apiKey: string;
authDomain: string;
projectId: string;
appId: string;
messagingSenderId?: string;
storageBucket?: string;
};
type VoiceRole = 'assistant' | 'system';
export type LiveVoiceMessage = {
role: VoiceRole;
text: string;
};
export type LiveVoiceToolAction = {
label?: string;
prompt?: string;
href?: string;
auto?: boolean;
};
export type LiveVoiceToolUi = {
kind?: string;
title?: string;
description?: string;
items?: Array<Record<string, unknown>>;
actions?: LiveVoiceToolAction[];
[key: string]: unknown;
};
export type LiveVoiceAssistantResponse = {
messages?: LiveVoiceMessage[];
ui?: LiveVoiceToolUi;
};
export type LiveVoiceToolResponse = {
messages?: LiveVoiceMessage[];
ui?: LiveVoiceToolUi;
};
export type LiveMarketingStreamEvent =
| { type: 'text'; text: string }
| { type: 'image'; base64: string; mimeType?: string }
| { type: 'done' }
| { type: 'error'; message: string };
export type LiveInvoiceItem = {
description?: string;
quantity?: number;
unitPrice?: number;
lineTotal?: number;
};
export type LiveInvoiceExtraction = {
provider?: 'gemini' | 'mock' | string;
model?: string;
confidence?: number;
warnings?: string[];
rawText?: string;
invoice?: {
direction?: 'sale' | 'purchase' | string;
status?: 'draft' | 'open' | 'paid' | 'overdue' | string;
invoiceNumber?: string;
counterpartyName?: string;
description?: string;
currency?: string;
total?: number;
paid?: number;
issueDate?: string;
dueDate?: string;
deliveryDate?: string;
items?: LiveInvoiceItem[];
};
};
export type LiveInvoiceScanResponse = {
extraction?: LiveInvoiceExtraction;
created?: { id?: string } | null;
};
const DEMO_USER_ID = (import.meta.env.VITE_DEMO_USER_ID as string | undefined)?.trim() || '';
const HF_DEMO_API_KEY = (import.meta.env.VITE_HF_DEMO_API_KEY as string | undefined)?.trim() || '';
const firebaseConfig: FirebaseEnvConfig = {
apiKey: (import.meta.env.VITE_FIREBASE_API_KEY as string | undefined)?.trim() || '',
authDomain: (import.meta.env.VITE_FIREBASE_AUTH_DOMAIN as string | undefined)?.trim() || '',
projectId: (import.meta.env.VITE_FIREBASE_PROJECT_ID as string | undefined)?.trim() || '',
appId: (import.meta.env.VITE_FIREBASE_APP_ID as string | undefined)?.trim() || '',
messagingSenderId: (import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID as string | undefined)?.trim() || undefined,
storageBucket: (import.meta.env.VITE_FIREBASE_STORAGE_BUCKET as string | undefined)?.trim() || undefined,
};
let authTokenPromise: Promise<string | null> | null = null;
function getLiveApiBaseUrl(): string {
const raw =
(import.meta.env.VITE_DEMO_API_BASE_URL as string | undefined)?.trim() ||
(import.meta.env.VITE_API_BASE_URL as string | undefined)?.trim() ||
'';
if (!raw) {
throw new Error('Missing `VITE_DEMO_API_BASE_URL` for live mode.');
}
return raw.endsWith('/api') ? raw.slice(0, -4) : raw.replace(/\/$/, '');
}
function hasFirebaseConfig(config: FirebaseEnvConfig): boolean {
return Boolean(config.apiKey && config.authDomain && config.projectId && config.appId);
}
async function getIdToken(): Promise<string | null> {
if (!hasFirebaseConfig(firebaseConfig)) return null;
if (!authTokenPromise) {
authTokenPromise = (async () => {
const app = getApps().some((entry) => entry.name === 'hf-demo-app')
? getApp('hf-demo-app')
: initializeApp(firebaseConfig, 'hf-demo-app');
const auth = getAuth(app);
if (!auth.currentUser) {
await signInAnonymously(auth);
}
return auth.currentUser ? await auth.currentUser.getIdToken(true) : null;
})();
}
return authTokenPromise;
}
async function parseErrorResponse(response: Response): Promise<string> {
try {
const json = (await response.json()) as { error?: string; message?: string; hint?: string };
return json.message || json.error || `Request failed with status ${response.status}`;
} catch {
const text = await response.text().catch(() => '');
return text || `Request failed with status ${response.status}`;
}
}
async function requestJson<T>(path: string, init: RequestInit = {}): Promise<T> {
const baseUrl = getLiveApiBaseUrl();
const token = await getIdToken();
const headers = new Headers(init.headers || {});
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
if (HF_DEMO_API_KEY) {
headers.set('x-hf-demo-key', HF_DEMO_API_KEY);
}
const response = await fetch(`${baseUrl}${path}`, {
...init,
headers,
});
if (!response.ok) {
const message = await parseErrorResponse(response);
throw new Error(message);
}
return (await response.json()) as T;
}
function withDemoUser<T extends Record<string, unknown>>(body: T): T {
if (!DEMO_USER_ID) return body;
return { ...body, userId: DEMO_USER_ID };
}
export function parseDataUrl(dataUrl: string): { base64: string; mimeType: string } {
const trimmed = dataUrl.trim();
const match = trimmed.match(/^data:([^;]+);base64,(.+)$/);
if (!match) {
return { base64: trimmed, mimeType: 'image/png' };
}
return { mimeType: match[1], base64: match[2] };
}
export async function sendVoiceAssistantMessage(params: {
message: string;
locale: DemoLocale;
pageRoute?: string;
pageContext?: string;
}): Promise<LiveVoiceAssistantResponse> {
return await requestJson<LiveVoiceAssistantResponse>(
'/api/voice/assistant',
{
method: 'POST',
body: JSON.stringify(
withDemoUser({
message: params.message,
locale: params.locale,
pageRoute: params.pageRoute,
pageContext: params.pageContext,
})
),
}
);
}
export async function callVoiceTool(params: {
name: string;
args?: Record<string, unknown>;
locale: DemoLocale;
}): Promise<LiveVoiceToolResponse> {
return await requestJson<LiveVoiceToolResponse>(
'/api/voice/tool',
{
method: 'POST',
body: JSON.stringify(
withDemoUser({
name: params.name,
args: params.args || {},
locale: params.locale,
})
),
}
);
}
export async function scanInvoiceImage(params: {
dataBase64: string;
mimeType?: string;
locale: DemoLocale;
createInvoice: boolean;
}): Promise<LiveInvoiceScanResponse> {
return await requestJson<LiveInvoiceScanResponse>(
'/api/invoices/scan',
{
method: 'POST',
body: JSON.stringify({
dataBase64: params.dataBase64,
mimeType: params.mimeType,
locale: params.locale,
createInvoice: params.createInvoice,
}),
}
);
}
export async function streamMarketingGeneration(params: {
prompt: string;
locale: DemoLocale;
product: { name: string; category: string; description?: string };
image?: { base64: string; mimeType: string };
signal?: AbortSignal;
onEvent: (event: LiveMarketingStreamEvent) => void;
}): Promise<void> {
const baseUrl = getLiveApiBaseUrl();
const token = await getIdToken();
const headers = new Headers({ 'Content-Type': 'application/json' });
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
if (HF_DEMO_API_KEY) {
headers.set('x-hf-demo-key', HF_DEMO_API_KEY);
}
const response = await fetch(`${baseUrl}/api/marketing/generate-stream`, {
method: 'POST',
headers,
signal: params.signal,
body: JSON.stringify({
prompt: params.prompt,
language: params.locale,
product: params.product,
image: params.image,
}),
});
if (!response.ok || !response.body) {
const message = await parseErrorResponse(response);
throw new Error(message);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const event = JSON.parse(trimmed) as LiveMarketingStreamEvent;
params.onEvent(event);
} catch {
// Ignore malformed chunks from network boundaries.
}
}
}
}