Spaces:
Sleeping
Sleeping
| 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. | |
| } | |
| } | |
| } | |
| } | |