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>; 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 | 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 { 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 { 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(path: string, init: RequestInit = {}): Promise { 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>(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 { return await requestJson( '/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; locale: DemoLocale; }): Promise { return await requestJson( '/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 { return await requestJson( '/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 { 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. } } } }