@oshara/voice-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +198 -0
  2. package/dist/appearance-CNWT8x1G.cjs +2 -0
  3. package/dist/appearance-CNWT8x1G.cjs.map +1 -0
  4. package/dist/appearance-i6QBkpCk.js +650 -0
  5. package/dist/appearance-i6QBkpCk.js.map +1 -0
  6. package/dist/consent-CK9VXNPa.js +54 -0
  7. package/dist/consent-CK9VXNPa.js.map +1 -0
  8. package/dist/consent-D7QNSkQD.cjs +2 -0
  9. package/dist/consent-D7QNSkQD.cjs.map +1 -0
  10. package/dist/core/analytics.d.ts +30 -0
  11. package/dist/core/appearance.d.ts +113 -0
  12. package/dist/core/audioSettings.d.ts +69 -0
  13. package/dist/core/consent.d.ts +17 -0
  14. package/dist/core/createVoiceAgent.d.ts +79 -0
  15. package/dist/core/events.d.ts +103 -0
  16. package/dist/core/formController.d.ts +28 -0
  17. package/dist/core/forms.d.ts +235 -0
  18. package/dist/core/index.d.ts +29 -0
  19. package/dist/core/prevContext.d.ts +26 -0
  20. package/dist/core/transport.d.ts +30 -0
  21. package/dist/core/types.d.ts +49 -0
  22. package/dist/core/voice.d.ts +79 -0
  23. package/dist/createVoiceAgent-BM3HODS6.js +1058 -0
  24. package/dist/createVoiceAgent-BM3HODS6.js.map +1 -0
  25. package/dist/createVoiceAgent-CJWxWzz6.cjs +4 -0
  26. package/dist/createVoiceAgent-CJWxWzz6.cjs.map +1 -0
  27. package/dist/index.cjs +2 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.js +44 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/react/index.d.ts +60 -0
  32. package/dist/react.cjs +2 -0
  33. package/dist/react.cjs.map +1 -0
  34. package/dist/react.js +115 -0
  35. package/dist/react.js.map +1 -0
  36. package/dist/styles.css +1838 -0
  37. package/dist/ui/index.d.ts +21 -0
  38. package/dist/ui/ui.d.ts +165 -0
  39. package/dist/ui.cjs +284 -0
  40. package/dist/ui.cjs.map +1 -0
  41. package/dist/ui.js +1153 -0
  42. package/dist/ui.js.map +1 -0
  43. package/package.json +67 -0
  44. package/src/core/analytics.ts +111 -0
  45. package/src/core/appearance.ts +464 -0
  46. package/src/core/audioSettings.ts +180 -0
  47. package/src/core/consent.ts +78 -0
  48. package/src/core/createVoiceAgent.ts +280 -0
  49. package/src/core/events.ts +120 -0
  50. package/src/core/formController.ts +317 -0
  51. package/src/core/forms.ts +861 -0
  52. package/src/core/index.ts +121 -0
  53. package/src/core/prevContext.ts +153 -0
  54. package/src/core/transport.ts +118 -0
  55. package/src/core/types.ts +66 -0
  56. package/src/core/voice.ts +1179 -0
  57. package/src/react/index.ts +238 -0
  58. package/src/ui/index.ts +507 -0
  59. package/src/ui/styles.css +1838 -0
  60. package/src/ui/ui.ts +1672 -0
  61. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance-i6QBkpCk.js","sources":["../src/core/forms.ts","../src/core/appearance.ts"],"sourcesContent":["/**\n * Agent-driven form support for the chat widget.\n *\n * The voice agent can prompt the visitor to fill a structured form (book a\n * demo, table reservation, lead capture, etc.) by publishing a LiveKit data\n * message. The widget listens on `RoomEvent.DataReceived`, matches the message\n * against a registered form definition, prefills any draft fields, and opens\n * a review screen so the user can confirm and submit.\n *\n * Form definitions are generic — `book-demo` ships as the default, but\n * additional forms (reservation, support ticket, lead capture, etc.) can be\n * added either by extending DEFAULT_FORM_DEFINITIONS below or by sending them\n * from the backend through the appearance config (`appearance.forms`).\n *\n * ---------------------------------------------------------------------------\n * Agent → widget protocol (LiveKit data message payload, UTF-8 JSON):\n *\n * 1. Topic-based match (preferred):\n * topic: \"form.book-demo\" | \"book-demo.form\" | \"book-demo.review\"\n * payload: { ...draft fields }\n *\n * 2. Form-id field:\n * payload: { form_id: \"reservation\", ...draft fields }\n *\n * 3. Event-type field (back-compat with the help-desk-call payload shape):\n * payload: { type: \"book_demo_form\", form: { ...draft fields } }\n *\n * Draft fields can live at the payload root or nested under `form`/`payload`/\n * `data`/`fields`/`values` — extractFormDraft() looks in all of them.\n */\n\nexport type FormFieldType =\n | \"text\"\n | \"email\"\n | \"tel\"\n | \"textarea\"\n | \"select\"\n | \"number\"\n | \"date\"\n | \"time\"\n | \"checkbox\"\n | \"radio\"\n | \"display\";\n\n/** Option for select/radio/checkbox-group. A plain string is shorthand for { value, label }. */\nexport type FormFieldOption = string | { value: string; label?: string };\n\nexport interface FormFieldDef {\n /** Field key — sent in the POST body and matched against incoming drafts.\n * Not required for `display` blocks (which render no input). */\n name?: string;\n label: string;\n type: FormFieldType;\n placeholder?: string;\n required?: boolean;\n /** Options for select/radio/checkbox-group. */\n options?: FormFieldOption[];\n /** Row count for `type: \"textarea\"`. */\n rows?: number;\n /** Optional default applied when the form is opened with no draft value. */\n default_value?: string;\n /** Helper text rendered beneath the input (or as the body of a `display` block). */\n help_text?: string;\n /** Optional regex (string form) to validate text-like fields on submit. */\n pattern?: string;\n /** Min/max for `type: \"number\"` (or min/max length for text — not enforced in v1). */\n min?: number;\n max?: number;\n /** Layout width when the form's `field_layout: \"grid\"`. \"full\" (default) or \"half\". */\n width?: \"full\" | \"half\";\n}\n\nexport interface FormStep {\n id: string;\n title?: string;\n subtitle?: string;\n fields: FormFieldDef[];\n next_label?: string;\n back_label?: string;\n}\n\nexport interface FormLayout {\n field_layout?: \"stack\" | \"grid\";\n density?: \"comfortable\" | \"compact\";\n label_position?: \"top\" | \"inline\";\n}\n\nexport interface FormDefinition {\n /** Stable identifier — used to match topics, event types, and form_id. */\n id: string;\n title: string;\n subtitle?: string;\n /** Single-page fields. Ignored when `steps` is set. */\n fields: FormFieldDef[];\n /** When set, the form renders as a stepper wizard. */\n steps?: FormStep[];\n /** Form-level layout knobs. */\n layout?: FormLayout;\n /** When true, the auto-derived agent tool is skipped (form is hidden from the LLM). */\n disabled?: boolean;\n /**\n * Where to POST the form on submit. Absolute URL (starts with http) is\n * used as-is; otherwise treated as a path joined onto BootConfig.apiUrl.\n * When null, the platform stores the response in managed storage.\n */\n submit_url: string | null;\n submit_method?: \"POST\" | \"PUT\" | \"PATCH\";\n submit_label?: string;\n success_message?: string;\n /** Extra LiveKit topics to match against (in addition to the id-based ones). */\n topics?: string[];\n /** Extra event-type aliases to match (e.g. legacy \"book_demo_form\"). */\n event_types?: string[];\n /**\n * LiveKit topic used when echoing the confirmation back to the agent.\n * Defaults to \"voice.user_text\".\n */\n confirmation_topic?: string;\n /**\n * `type` field on the confirmation payload sent back to the agent.\n * Defaults to `<id>_submitted`.\n */\n confirmation_type?: string;\n}\n\nexport interface FormSession {\n definition: FormDefinition;\n values: Record<string, string>;\n}\n\n/**\n * Minimal surface a form controller must expose so the agent can drive it\n * via data-channel messages (step / submit / close). The widget's\n * `createFormController` returns an object that satisfies this shape; we\n * keep the type narrow so the dispatcher below can't accidentally reach\n * into internals.\n */\nexport interface FormActionTarget {\n current: () => string | null;\n step: (direction: \"next\" | \"back\" | number) => void;\n submit: () => void;\n close: () => void;\n}\n\n/**\n * Snapshot of the open form that the widget publishes back to the agent on\n * every meaningful change (field edit, step move, open, close). Lives on\n * the `form.state` LiveKit topic.\n */\n/** Compact field descriptor sent to the agent so it can build accurate,\n * enum-constrained voice-fill tools for forms that aren't defined in the\n * session token (appearance.forms). Mirrors the subset of FormFieldDef the\n * agent's _field_to_json_schema needs. */\nexport interface FormFieldSchema {\n name: string;\n label: string;\n type: FormFieldType;\n required: boolean;\n /** Zero-based step this field lives on (0 for single-page forms). Lets the\n * agent guide the visitor through a stepper one step at a time. */\n step: number;\n options?: { value: string; label: string }[];\n /** Validation rules mirrored so the agent can reject malformed input in its\n * submit guard — before claiming success — exactly like the widget does. */\n pattern?: string;\n min?: number;\n max?: number;\n}\n\nexport interface FormStateSnapshot {\n type: \"form_state\";\n form_id: string;\n is_open: boolean;\n step_index: number;\n total_steps: number;\n values: Record<string, string>;\n /** Schema of the form's input fields, so the agent can register\n * enum-aware render tools even without appearance.forms in the token. */\n fields: FormFieldSchema[];\n}\n\n/** Build the compact field schema the agent needs to construct voice-fill\n * tools (enum values for select/radio/checkbox, types for the rest) and to\n * guide the visitor through a multi-step form one step at a time. */\nexport function buildFieldSchema(\n definition: FormDefinition,\n): FormFieldSchema[] {\n const toSchema = (f: FormFieldDef, step: number): FormFieldSchema => {\n const entry: FormFieldSchema = {\n name: f.name!,\n label: f.label,\n type: f.type,\n required: Boolean(f.required),\n step,\n };\n if (f.options?.length) {\n entry.options = f.options.map((opt) =>\n typeof opt === \"string\"\n ? { value: opt, label: opt }\n : { value: opt.value, label: opt.label ?? opt.value },\n );\n }\n if (f.pattern) entry.pattern = f.pattern;\n if (f.min !== undefined) entry.min = f.min;\n if (f.max !== undefined) entry.max = f.max;\n return entry;\n };\n\n if (definition.steps?.length) {\n const out: FormFieldSchema[] = [];\n definition.steps.forEach((s, idx) => {\n for (const f of s.fields) {\n if (f.type !== \"display\" && f.name) out.push(toSchema(f, idx));\n }\n });\n return out;\n }\n return collectInputFields(definition).map((f) => toSchema(f, 0));\n}\n\n/**\n * Topic pattern the agent uses to drive form actions: `form.{id}.action`.\n * Payload carries `{type: \"form_step\"|\"form_submit\"|\"form_close\", form_id,\n * direction?, step_index?}`.\n *\n * Returns true if the message was a form-action and was dispatched (so the\n * caller can short-circuit further matching), false otherwise.\n */\nexport function handleFormAction(\n topic: string | undefined,\n value: unknown,\n target: FormActionTarget,\n): boolean {\n if (!topic) return false;\n const normalized = topic.trim().toLowerCase();\n // Expect \"form.<id>.action\".\n if (!normalized.startsWith(\"form.\") || !normalized.endsWith(\".action\")) {\n return false;\n }\n const formId = normalized.slice(\"form.\".length, -\".action\".length);\n if (!formId) return false;\n if (!value || typeof value !== \"object\") return false;\n const payload = value as Record<string, unknown>;\n\n // Guard: action must target the currently-open form. We don't want a\n // stale event from a prior form to drive whatever the user opened next.\n const openId = target.current();\n if (!openId || openId.trim().toLowerCase() !== formId) return false;\n\n // Match by payload.form_id when present (belt + suspenders).\n const payloadFormId = stringField(payload.form_id);\n if (payloadFormId && payloadFormId.trim().toLowerCase() !== formId) {\n return false;\n }\n\n const eventType = (\n stringField(payload.type) ?? stringField(payload.event) ?? \"\"\n )\n .trim()\n .toLowerCase();\n\n if (eventType === \"form_step\") {\n const direction = stringField(payload.direction)?.trim().toLowerCase();\n if (direction === \"next\" || direction === \"back\") {\n target.step(direction);\n return true;\n }\n const stepIndex = payload.step_index;\n if (typeof stepIndex === \"number\" && Number.isFinite(stepIndex)) {\n target.step(stepIndex);\n return true;\n }\n // Malformed step event — swallow so it doesn't fall through to the\n // generic form matcher and accidentally re-render.\n return true;\n }\n\n if (eventType === \"form_submit\") {\n target.submit();\n return true;\n }\n\n if (eventType === \"form_close\") {\n target.close();\n return true;\n }\n\n return false;\n}\n\n/** Flatten a form to its full field list — single-page or stepper. Excludes display blocks. */\nexport function collectInputFields(definition: FormDefinition): FormFieldDef[] {\n const all = definition.steps?.length\n ? definition.steps.flatMap((s) => s.fields)\n : definition.fields;\n return all.filter((f) => f.type !== \"display\" && Boolean(f.name));\n}\n\n/** Fields for a specific step (or the whole form when no stepper). */\nexport function fieldsForStep(\n definition: FormDefinition,\n stepIndex: number,\n): FormFieldDef[] {\n if (definition.steps?.length) {\n const step = definition.steps[Math.max(0, Math.min(stepIndex, definition.steps.length - 1))];\n return step?.fields ?? [];\n }\n return definition.fields;\n}\n\nexport function totalSteps(definition: FormDefinition): number {\n return definition.steps?.length || 1;\n}\n\n/** A single field that failed local validation. */\nexport interface FieldValidationError {\n name: string;\n label: string;\n message: string;\n}\n\n// Loose, intentionally permissive formats — the goal is to catch obvious\n// typos before submit, not to reject every unusual-but-valid value.\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst TEL_ALLOWED_RE = /^[+()\\-.\\s\\d]+$/;\n\n/**\n * Validate the given values against their field definitions, returning one\n * error per offending field. Runs entirely in the browser so obvious mistakes\n * (missing required fields, malformed email/phone, out-of-range numbers, custom\n * pattern mismatches) are caught before anything is sent to the server/agent.\n *\n * Format checks are skipped for empty optional fields — only `required` flags\n * empties. `display` blocks and unnamed fields are ignored.\n */\nexport function validateFields(\n fields: FormFieldDef[],\n values: Record<string, string>,\n): FieldValidationError[] {\n const errors: FieldValidationError[] = [];\n for (const field of fields) {\n if (field.type === \"display\" || !field.name) continue;\n const raw = values[field.name];\n const value = (raw ?? \"\").trim();\n\n if (!value) {\n if (field.required) {\n errors.push({ name: field.name, label: field.label, message: `${field.label} is required.` });\n }\n // Nothing more to validate on an empty optional field.\n continue;\n }\n\n if (field.type === \"email\" && !EMAIL_RE.test(value)) {\n errors.push({ name: field.name, label: field.label, message: \"Enter a valid email address.\" });\n continue;\n }\n\n if (field.type === \"tel\") {\n const digits = value.replace(/\\D/g, \"\");\n if (!TEL_ALLOWED_RE.test(value) || digits.length < 7) {\n errors.push({ name: field.name, label: field.label, message: \"Enter a valid phone number.\" });\n continue;\n }\n }\n\n if (field.type === \"number\") {\n const num = Number(value);\n if (!Number.isFinite(num)) {\n errors.push({ name: field.name, label: field.label, message: \"Enter a valid number.\" });\n continue;\n }\n if (field.min !== undefined && num < field.min) {\n errors.push({ name: field.name, label: field.label, message: `Must be at least ${field.min}.` });\n continue;\n }\n if (field.max !== undefined && num > field.max) {\n errors.push({ name: field.name, label: field.label, message: `Must be at most ${field.max}.` });\n continue;\n }\n }\n\n if (field.pattern) {\n try {\n if (!new RegExp(field.pattern).test(value)) {\n errors.push({ name: field.name, label: field.label, message: `Please enter a valid ${field.label.toLowerCase()}.` });\n continue;\n }\n } catch {\n // A malformed pattern in the form config must never break submission.\n }\n }\n }\n return errors;\n}\n\nconst BOOK_DEMO_INTEREST_OPTIONS = [\n \"Enterprise Voice Platform (OVIP)\",\n \"Fine-Tuning Automation (OFTA)\",\n \"Consumer / Kids Products\",\n \"Investor Relations\",\n \"Partnership / Integration\",\n];\n\nexport const DEFAULT_FORM_DEFINITIONS: FormDefinition[] = [\n {\n id: \"book-demo\",\n title: \"Book a demo\",\n subtitle:\n \"The voice agent filled this from the call. Review and edit before sending.\",\n submit_url: \"/api/book-demo/\",\n submit_label: \"Confirm & send\",\n success_message:\n \"Thanks! Your demo request has been sent. We'll get back to you soon.\",\n topics: [\"book-demo.form\", \"book-demo.review\", \"form.book-demo\"],\n event_types: [\"book_demo_form\", \"book-demo-form\"],\n // Match the webapp's help-desk-call echo so the agent sees the same\n // event type from either surface. Default would have been\n // \"book-demo_submitted\" (hyphen).\n confirmation_type: \"book_demo_submitted\",\n fields: [\n { name: \"name\", label: \"Full name\", type: \"text\", required: true, placeholder: \"Your full name\" },\n { name: \"organization\", label: \"Organization\", type: \"text\", required: true, placeholder: \"Your company or organization\" },\n { name: \"email\", label: \"Email\", type: \"email\", required: true, placeholder: \"you@organization.com\" },\n {\n name: \"interested_in\",\n label: \"I'm interested in...\",\n type: \"select\",\n required: true,\n options: BOOK_DEMO_INTEREST_OPTIONS,\n default_value: BOOK_DEMO_INTEREST_OPTIONS[0],\n },\n {\n name: \"description\",\n label: \"Why do you want to book a demo?\",\n type: \"textarea\",\n required: true,\n rows: 4,\n placeholder: \"Describe your use case, goals, or questions...\",\n },\n ],\n },\n {\n id: \"reservation\",\n title: \"Make a reservation\",\n subtitle:\n \"The voice agent filled this from the call. Review and edit before sending.\",\n submit_url: \"/api/reservations/\",\n submit_label: \"Confirm reservation\",\n success_message: \"Reservation confirmed. We'll send a confirmation email shortly.\",\n topics: [\"reservation.form\", \"form.reservation\"],\n event_types: [\"reservation_form\", \"reservation-form\"],\n fields: [\n { name: \"name\", label: \"Name\", type: \"text\", required: true, placeholder: \"Your name\" },\n { name: \"email\", label: \"Email\", type: \"email\", required: true, placeholder: \"you@example.com\" },\n { name: \"phone\", label: \"Phone\", type: \"tel\", placeholder: \"+977 98XXXXXXXX\" },\n { name: \"party_size\", label: \"Party size\", type: \"text\", required: true, placeholder: \"e.g. 4\" },\n { name: \"date\", label: \"Date\", type: \"text\", required: true, placeholder: \"YYYY-MM-DD\" },\n { name: \"time\", label: \"Time\", type: \"text\", required: true, placeholder: \"HH:MM\" },\n { name: \"notes\", label: \"Notes\", type: \"textarea\", rows: 3, placeholder: \"Allergies, occasion, seating preference…\" },\n ],\n },\n];\n\n/**\n * Try to identify which form definition an incoming LiveKit data message is\n * asking the widget to open. Returns null if no definition matches.\n */\nexport function matchForm(\n topic: string | undefined,\n value: unknown,\n forms: FormDefinition[],\n): FormDefinition | null {\n if (!value || typeof value !== \"object\") return null;\n\n const normalize = (s: string) => s.trim().toLowerCase();\n\n if (topic) {\n const t = normalize(topic);\n for (const form of forms) {\n if (form.topics?.some((entry) => normalize(entry) === t)) return form;\n const id = normalize(form.id);\n if (\n t === `${id}.form` ||\n t === `${id}.review` ||\n t === `form.${id}` ||\n t === id\n ) {\n return form;\n }\n }\n }\n\n const candidate = value as Record<string, unknown>;\n\n const explicitId = stringField(candidate.form_id) ?? stringField(candidate.formId);\n if (explicitId) {\n const id = normalize(explicitId);\n const match = forms.find((f) => normalize(f.id) === id);\n if (match) return match;\n }\n\n const eventType = [candidate.type, candidate.event, candidate.kind, candidate.intent]\n .map(stringField)\n .find((v): v is string => Boolean(v))\n ?.toLowerCase();\n\n if (eventType) {\n for (const form of forms) {\n if (form.event_types?.some((entry) => normalize(entry) === eventType)) return form;\n const id = normalize(form.id);\n if (\n eventType === id ||\n eventType === `${id}_form` ||\n eventType === `${id}-form` ||\n eventType === `${id.replace(/-/g, \"_\")}_form`\n ) {\n return form;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Pull a partial field/value draft out of an incoming data message. Looks at\n * the payload root and common nested keys (`form`, `payload`, `data`,\n * `fields`, `values`). Returns null when nothing matches the form's fields.\n */\nexport function extractFormDraft(\n value: unknown,\n definition: FormDefinition,\n): Record<string, string> | null {\n if (!value || typeof value !== \"object\") return null;\n const candidate = value as Record<string, unknown>;\n\n const nested = [\"draft\", \"form\", \"payload\", \"data\", \"fields\", \"values\"]\n .map((key) => candidate[key])\n .find((entry) => entry && typeof entry === \"object\") as\n | Record<string, unknown>\n | undefined;\n\n const draft: Record<string, string> = {};\n\n for (const field of collectInputFields(definition)) {\n if (!field.name) continue;\n const raw =\n stringField(nested?.[field.name]) ?? stringField(candidate[field.name]);\n if (raw) draft[field.name] = raw;\n }\n\n return Object.keys(draft).length > 0 ? draft : null;\n}\n\nexport function initialFormValues(\n definition: FormDefinition,\n): Record<string, string> {\n const values: Record<string, string> = {};\n for (const field of collectInputFields(definition)) {\n if (!field.name) continue;\n values[field.name] = field.default_value ?? \"\";\n }\n return values;\n}\n\nexport function mergeFormDraft(\n current: Record<string, string>,\n draft: Record<string, string> | null | undefined,\n): Record<string, string> {\n if (!draft) return current;\n const next = { ...current };\n for (const [k, v] of Object.entries(draft)) {\n if (typeof v === \"string\" && v.trim()) next[k] = v.trim();\n }\n return next;\n}\n\n/** Plain-text summary of a submission — echoed back to the agent on confirm. */\nexport function buildSubmissionText(\n definition: FormDefinition,\n values: Record<string, string>,\n): string {\n const parts = collectInputFields(definition)\n .map((f) => `${f.label.toLowerCase()}: ${(values[f.name!] ?? \"\").trim()}`)\n .filter((entry) => !entry.endsWith(\": \"));\n return `I have confirmed and submitted the \"${definition.title}\" form: ${parts.join(\"; \")}.`;\n}\n\nexport interface SubmitFormArgs {\n definition: FormDefinition;\n values: Record<string, string>;\n apiUrl: string;\n /** Agent slug — required when submit_url is null (managed storage path). */\n slug: string;\n /** Active session ID to link the form response to its billing session. */\n sessionId?: string | null;\n /** Secret key sent as `x-api-key` on the managed-storage POST. */\n apiKey?: string;\n /** Custom fetch (Node <18 / testing). Defaults to globalThis.fetch. */\n fetch?: typeof fetch;\n}\n\n/** Collect all field definitions across steps and top-level fields. */\nfunction allFields(definition: FormDefinition): FormFieldDef[] {\n if (definition.steps?.length) {\n return definition.steps.flatMap((s) => s.fields);\n }\n return definition.fields;\n}\n\nexport async function submitForm({\n definition,\n values,\n apiUrl,\n slug,\n sessionId,\n apiKey,\n fetch: fetchImpl,\n}: SubmitFormArgs): Promise<unknown> {\n const base = apiUrl.replace(/\\/+$/, \"\");\n\n let url: string;\n let body: unknown;\n let method: string;\n let managed = false;\n\n if (!definition.submit_url) {\n // Managed storage path: POST to platform API with question+answer pairs.\n managed = true;\n url = `${base}/api/agents/${slug}/form-responses/`;\n method = \"POST\";\n const formData = allFields(definition)\n .filter((f) => f.name && f.type !== \"display\")\n .map((f) => ({\n name: f.name!,\n label: f.label,\n value: values[f.name!] ?? \"\",\n }));\n body = {\n form_id: definition.id,\n form_data: formData,\n ...(sessionId ? { session_id: sessionId } : {}),\n };\n } else {\n // External submit_url path (existing behaviour).\n url = /^https?:\\/\\//i.test(definition.submit_url)\n ? definition.submit_url\n : `${base}/${definition.submit_url.replace(/^\\/+/, \"\")}`;\n method = definition.submit_method ?? \"POST\";\n body = values;\n }\n\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n // Only attach the platform key on the managed-storage path; never leak it to\n // a third-party submit_url.\n if (managed && apiKey?.trim()) headers[\"x-api-key\"] = apiKey.trim();\n const doFetch =\n fetchImpl ??\n (typeof globalThis.fetch === \"function\"\n ? globalThis.fetch.bind(globalThis)\n : (() => {\n throw new Error(\"[voice-agent] No fetch available; pass config.fetch.\");\n })());\n\n const response = await doFetch(url, {\n method,\n headers,\n body: JSON.stringify(body),\n });\n\n const data = await response.json().catch(() => null);\n\n if (!response.ok) {\n throw new Error(\n data?.error ||\n data?.detail ||\n `We couldn't send your request right now (HTTP ${response.status}).`,\n );\n }\n\n return data;\n}\n\n/**\n * Accept a partial `forms` array from the appearance config (or anywhere\n * else) and turn it into a clean array of FormDefinitions, dropping entries\n * that don't have an id + at least one field.\n */\nexport function normalizeFormDefinitions(value: unknown): FormDefinition[] {\n if (!Array.isArray(value)) return [];\n const out: FormDefinition[] = [];\n\n for (const raw of value) {\n if (!raw || typeof raw !== \"object\") continue;\n const entry = raw as Record<string, unknown>;\n const id = stringField(entry.id);\n if (!id) continue;\n\n const steps = normalizeSteps(entry.steps);\n const fields = normalizeFields(entry.fields);\n // A form must have either at least one step (with fields) or a fields array.\n if (!steps.length && !fields.length) continue;\n\n out.push({\n id,\n title: stringField(entry.title) ?? id,\n subtitle: stringField(entry.subtitle),\n fields,\n steps: steps.length ? steps : undefined,\n layout: normalizeLayout(entry.layout),\n disabled: entry.disabled === true ? true : undefined,\n submit_url: stringField(entry.submit_url) ?? null,\n submit_method: enumField(\n [\"POST\", \"PUT\", \"PATCH\"] as const,\n entry.submit_method,\n ),\n submit_label: stringField(entry.submit_label),\n success_message: stringField(entry.success_message),\n topics: stringArray(entry.topics),\n event_types: stringArray(entry.event_types),\n confirmation_topic: stringField(entry.confirmation_topic),\n confirmation_type: stringField(entry.confirmation_type),\n });\n }\n return out;\n}\n\nfunction normalizeFields(value: unknown): FormFieldDef[] {\n if (!Array.isArray(value)) return [];\n const out: FormFieldDef[] = [];\n for (const raw of value) {\n if (!raw || typeof raw !== \"object\") continue;\n const entry = raw as Record<string, unknown>;\n const type =\n enumField(\n [\n \"text\",\n \"email\",\n \"tel\",\n \"textarea\",\n \"select\",\n \"number\",\n \"date\",\n \"time\",\n \"checkbox\",\n \"radio\",\n \"display\",\n ] as const,\n entry.type,\n ) ?? \"text\";\n const name = stringField(entry.name);\n // Inputs require a name; display blocks don't.\n if (type !== \"display\" && !name) continue;\n\n out.push({\n name,\n label: stringField(entry.label) ?? name ?? \"\",\n type,\n placeholder: stringField(entry.placeholder),\n required: entry.required === true,\n options: normalizeOptions(entry.options),\n rows:\n typeof entry.rows === \"number\" && Number.isFinite(entry.rows) && entry.rows > 0\n ? entry.rows\n : undefined,\n default_value: stringField(entry.default_value),\n help_text: stringField(entry.help_text),\n pattern: stringField(entry.pattern),\n min: finiteNumber(entry.min),\n max: finiteNumber(entry.max),\n width: enumField([\"full\", \"half\"] as const, entry.width),\n });\n }\n return out;\n}\n\nfunction normalizeSteps(value: unknown): FormStep[] {\n if (!Array.isArray(value)) return [];\n const out: FormStep[] = [];\n for (const [index, raw] of value.entries()) {\n if (!raw || typeof raw !== \"object\") continue;\n const entry = raw as Record<string, unknown>;\n const fields = normalizeFields(entry.fields);\n if (!fields.length) continue;\n out.push({\n id: stringField(entry.id) ?? `step-${index + 1}`,\n title: stringField(entry.title),\n subtitle: stringField(entry.subtitle),\n fields,\n next_label: stringField(entry.next_label),\n back_label: stringField(entry.back_label),\n });\n }\n return out;\n}\n\nfunction normalizeLayout(value: unknown): FormLayout | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const entry = value as Record<string, unknown>;\n const layout: FormLayout = {};\n const fieldLayout = enumField([\"stack\", \"grid\"] as const, entry.field_layout);\n if (fieldLayout) layout.field_layout = fieldLayout;\n const density = enumField([\"comfortable\", \"compact\"] as const, entry.density);\n if (density) layout.density = density;\n const labelPos = enumField([\"top\", \"inline\"] as const, entry.label_position);\n if (labelPos) layout.label_position = labelPos;\n return Object.keys(layout).length ? layout : undefined;\n}\n\nfunction normalizeOptions(value: unknown): FormFieldOption[] | undefined {\n if (!Array.isArray(value)) return undefined;\n const out: FormFieldOption[] = [];\n for (const raw of value) {\n if (typeof raw === \"string\") {\n const trimmed = raw.trim();\n if (trimmed) out.push(trimmed);\n continue;\n }\n if (raw && typeof raw === \"object\") {\n const entry = raw as Record<string, unknown>;\n const v = stringField(entry.value);\n if (!v) continue;\n const label = stringField(entry.label);\n out.push(label ? { value: v, label } : { value: v });\n }\n }\n return out.length ? out : undefined;\n}\n\nfunction finiteNumber(value: unknown): number | undefined {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const parsed = Number(value.trim());\n if (Number.isFinite(parsed)) return parsed;\n }\n return undefined;\n}\n\nfunction stringField(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction stringArray(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined;\n const out = value\n .map(stringField)\n .filter((v): v is string => Boolean(v));\n return out.length ? out : undefined;\n}\n\nfunction enumField<T extends string>(\n allowed: readonly T[],\n value: unknown,\n): T | undefined {\n return typeof value === \"string\" && allowed.includes(value as T)\n ? (value as T)\n : undefined;\n}\n","import {\n DEFAULT_FORM_DEFINITIONS,\n FormDefinition,\n normalizeFormDefinitions,\n} from \"./forms\";\n\n/**\n * Per-agent widget appearance configuration.\n *\n * The widget fetches this from `GET {apiUrl}/api/agents/{slug}/appearance/`\n * (override with `data-appearance-url`) on init, falling back to the defaults\n * below if the request fails or any field is missing.\n *\n * Every field is optional on the wire — partial responses are merged on top of\n * the defaults so the widget never breaks if the backend hasn't been updated.\n */\n\nexport type WidgetPosition =\n | \"bottom-right\"\n | \"bottom-left\"\n | \"top-right\"\n | \"top-left\";\n\nexport interface AppearanceTheme {\n primary_color: string;\n accent_color: string;\n background_color: string;\n text_color: string;\n user_bubble_color: string;\n user_bubble_text_color: string;\n agent_bubble_color: string;\n agent_bubble_text_color: string;\n}\n\nexport interface AppearanceDimensions {\n fab_size: number;\n panel_width: number;\n panel_height: number;\n border_radius: number;\n}\n\nexport interface AppearanceLayout {\n position: WidgetPosition;\n font_family: string;\n}\n\nexport interface AppearanceLabels {\n idle: string;\n connecting: string;\n listening: string;\n speaking: string;\n /** Shown while the agent is processing (running STT→LLM→tool→TTS). The\n * backend may override this with a per-tool contextual label\n * (\"Searching the knowledge base…\") via voice.agent_status events. */\n thinking: string;\n muted: string;\n call_ended: string;\n transcript_placeholder: string;\n language_label: string;\n}\n\nexport interface AppearanceLanguage {\n /** BCP-47-ish short code sent to the backend (e.g. \"en\", \"ne\"). */\n code: string;\n /** English label, e.g. \"English\", \"Nepali\". */\n label: string;\n /** Native script label, e.g. \"English\", \"नेपाली\". Falls back to label. */\n native_label: string;\n}\n\nexport interface AppearanceConfig {\n name: string;\n subtitle: string;\n logo_url: string;\n fab_label: string;\n fab_sublabel: string;\n start_button_text: string;\n powered_by_text: string;\n powered_by_url: string;\n theme: AppearanceTheme;\n dimensions: AppearanceDimensions;\n layout: AppearanceLayout;\n labels: AppearanceLabels;\n languages: AppearanceLanguage[];\n default_language: string;\n /** Hard cap on a single voice call, in seconds. 0 = no limit. */\n max_call_seconds: number;\n /** URL to the terms & conditions / privacy policy page. When set, a\n * consent line is shown beneath the Start button stating that starting\n * the call accepts these terms. Empty disables the consent UI. */\n terms_url: string;\n /** Visible label for the terms link (default \"Terms & Conditions\"). */\n terms_label: string;\n /** Prefix shown before the terms link (default\n * \"By starting the call, you agree to our\"). */\n consent_text: string;\n /**\n * Extra form definitions (book-a-demo, reservation, lead capture, …) the\n * agent can open mid-call by publishing a LiveKit data message. Built-in\n * defaults are always available; entries here override defaults by id.\n * See `forms.ts` for the wire protocol.\n */\n forms: FormDefinition[];\n /**\n * Whether to expose the in-widget audio settings drawer (mic/speaker\n * selection, NC/AEC/AGC toggles, headphone mode, mic-level meter,\n * diagnostics row). Defaults to true. Set false to hide the gear icon\n * entirely for a kiosk-style minimal UI.\n */\n show_audio_settings: boolean;\n}\n\nexport const DEFAULT_APPEARANCE: AppearanceConfig = {\n name: \"Voice Assistant\",\n subtitle: \"Tap below to start a voice conversation.\",\n logo_url: \"\",\n // Empty by default — only show FAB text when the backend explicitly\n // configures fab_label/fab_sublabel. Otherwise the FAB is icon-only.\n fab_label: \"\",\n fab_sublabel: \"\",\n start_button_text: \"Start Call\",\n powered_by_text: \"Powered by Oshara.ai\",\n powered_by_url: \"\",\n theme: {\n primary_color: \"#6366F1\",\n accent_color: \"#22D3EE\",\n background_color: \"#FFFFFF\",\n text_color: \"#0F172A\",\n user_bubble_color: \"#6366F1\",\n user_bubble_text_color: \"#FFFFFF\",\n agent_bubble_color: \"#F1F5F9\",\n agent_bubble_text_color: \"#0F172A\",\n },\n dimensions: {\n fab_size: 64,\n panel_width: 380,\n panel_height: 620,\n border_radius: 24,\n },\n layout: {\n position: \"bottom-right\",\n font_family:\n \"Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n },\n labels: {\n idle: \"Idle\",\n connecting: \"Connecting…\",\n listening: \"Listening\",\n speaking: \"Speaking…\",\n thinking: \"Thinking…\",\n muted: \"Microphone muted\",\n call_ended: \"Call ended\",\n transcript_placeholder: \"Your live transcript will appear here\",\n language_label: \"Language\",\n },\n languages: [\n { code: \"en\", label: \"English\", native_label: \"English\" },\n { code: \"ne\", label: \"Nepali\", native_label: \"नेपाली\" },\n ],\n default_language: \"en\",\n max_call_seconds: 0,\n terms_url: \"\",\n terms_label: \"Terms & Conditions\",\n consent_text: \"By starting the call, you agree to our\",\n forms: DEFAULT_FORM_DEFINITIONS,\n show_audio_settings: true,\n};\n\nconst POSITIONS: WidgetPosition[] = [\n \"bottom-right\",\n \"bottom-left\",\n \"top-right\",\n \"top-left\",\n];\n\n/**\n * Deep-merge a partial server response on top of DEFAULT_APPEARANCE.\n * Only known keys are accepted; unknown keys are ignored.\n */\nexport function mergeAppearance(\n partial: Partial<AppearanceConfig> | null | undefined,\n): AppearanceConfig {\n if (!partial || typeof partial !== \"object\") return DEFAULT_APPEARANCE;\n const safe = asRecord(partial);\n const theme = asRecord(safe.theme);\n const dimensions = asRecord(safe.dimensions);\n const layout = asRecord(safe.layout);\n const labels = asRecord(safe.labels);\n\n return {\n name: nonEmptyString(safe.name) ?? DEFAULT_APPEARANCE.name,\n subtitle: stringOrDefault(safe.subtitle, DEFAULT_APPEARANCE.subtitle),\n logo_url: stringOrDefault(safe.logo_url, DEFAULT_APPEARANCE.logo_url),\n fab_label: stringOrDefault(safe.fab_label, DEFAULT_APPEARANCE.fab_label),\n fab_sublabel: stringOrDefault(\n safe.fab_sublabel,\n DEFAULT_APPEARANCE.fab_sublabel,\n ),\n start_button_text:\n nonEmptyString(safe.start_button_text) ??\n DEFAULT_APPEARANCE.start_button_text,\n powered_by_text: stringOrDefault(\n safe.powered_by_text,\n DEFAULT_APPEARANCE.powered_by_text,\n ),\n powered_by_url: stringOrDefault(\n safe.powered_by_url,\n DEFAULT_APPEARANCE.powered_by_url,\n ),\n theme: {\n primary_color:\n nonEmptyString(theme.primary_color) ??\n DEFAULT_APPEARANCE.theme.primary_color,\n accent_color:\n nonEmptyString(theme.accent_color) ??\n DEFAULT_APPEARANCE.theme.accent_color,\n background_color:\n nonEmptyString(theme.background_color) ??\n DEFAULT_APPEARANCE.theme.background_color,\n text_color:\n nonEmptyString(theme.text_color) ?? DEFAULT_APPEARANCE.theme.text_color,\n user_bubble_color:\n nonEmptyString(theme.user_bubble_color) ??\n DEFAULT_APPEARANCE.theme.user_bubble_color,\n user_bubble_text_color:\n nonEmptyString(theme.user_bubble_text_color) ??\n DEFAULT_APPEARANCE.theme.user_bubble_text_color,\n agent_bubble_color:\n nonEmptyString(theme.agent_bubble_color) ??\n DEFAULT_APPEARANCE.theme.agent_bubble_color,\n agent_bubble_text_color:\n nonEmptyString(theme.agent_bubble_text_color) ??\n DEFAULT_APPEARANCE.theme.agent_bubble_text_color,\n },\n dimensions: {\n fab_size:\n pixelOrNumber(dimensions.fab_size) ??\n DEFAULT_APPEARANCE.dimensions.fab_size,\n panel_width:\n pixelOrNumber(dimensions.panel_width) ??\n DEFAULT_APPEARANCE.dimensions.panel_width,\n panel_height:\n pixelOrNumber(dimensions.panel_height) ??\n DEFAULT_APPEARANCE.dimensions.panel_height,\n border_radius:\n pixelOrNumber(dimensions.border_radius) ??\n DEFAULT_APPEARANCE.dimensions.border_radius,\n },\n layout: {\n position:\n enumOrUndefined(POSITIONS, layout.position) ??\n DEFAULT_APPEARANCE.layout.position,\n font_family: stringOrDefault(\n layout.font_family,\n DEFAULT_APPEARANCE.layout.font_family,\n ),\n },\n labels: {\n idle: nonEmptyString(labels.idle) ?? DEFAULT_APPEARANCE.labels.idle,\n connecting:\n nonEmptyString(labels.connecting) ??\n DEFAULT_APPEARANCE.labels.connecting,\n listening:\n nonEmptyString(labels.listening) ?? DEFAULT_APPEARANCE.labels.listening,\n speaking:\n nonEmptyString(labels.speaking) ?? DEFAULT_APPEARANCE.labels.speaking,\n thinking:\n nonEmptyString(labels.thinking) ?? DEFAULT_APPEARANCE.labels.thinking,\n muted: nonEmptyString(labels.muted) ?? DEFAULT_APPEARANCE.labels.muted,\n call_ended:\n nonEmptyString(labels.call_ended) ??\n DEFAULT_APPEARANCE.labels.call_ended,\n transcript_placeholder: stringOrDefault(\n labels.transcript_placeholder,\n DEFAULT_APPEARANCE.labels.transcript_placeholder,\n ),\n language_label:\n nonEmptyString(labels.language_label) ??\n DEFAULT_APPEARANCE.labels.language_label,\n },\n languages: normalizeLanguages(safe.languages),\n default_language:\n nonEmptyString(safe.default_language) ??\n DEFAULT_APPEARANCE.default_language,\n max_call_seconds:\n nonNegativeNumber(safe.max_call_seconds) ??\n DEFAULT_APPEARANCE.max_call_seconds,\n terms_url: stringOrDefault(safe.terms_url, DEFAULT_APPEARANCE.terms_url),\n terms_label:\n nonEmptyString(safe.terms_label) ?? DEFAULT_APPEARANCE.terms_label,\n consent_text: stringOrDefault(\n safe.consent_text,\n DEFAULT_APPEARANCE.consent_text,\n ),\n forms: mergeForms(safe.forms),\n show_audio_settings:\n typeof safe.show_audio_settings === \"boolean\"\n ? safe.show_audio_settings\n : DEFAULT_APPEARANCE.show_audio_settings,\n };\n}\n\nfunction mergeForms(value: unknown): FormDefinition[] {\n const custom = normalizeFormDefinitions(value);\n const map = new Map<string, FormDefinition>();\n for (const f of DEFAULT_FORM_DEFINITIONS) map.set(f.id, f);\n for (const f of custom) map.set(f.id, f);\n return Array.from(map.values());\n}\n\nfunction normalizeLanguages(value: unknown): AppearanceLanguage[] {\n if (!Array.isArray(value)) return DEFAULT_APPEARANCE.languages;\n const seen = new Set<string>();\n const out: AppearanceLanguage[] = [];\n for (const raw of value) {\n if (!isPlainObject(raw)) continue;\n const code = nonEmptyString(raw.code);\n if (!code || seen.has(code)) continue;\n const label = nonEmptyString(raw.label) ?? code.toUpperCase();\n const native = nonEmptyString(raw.native_label) ?? label;\n seen.add(code);\n out.push({ code, label, native_label: native });\n }\n return out.length ? out : DEFAULT_APPEARANCE.languages;\n}\n\nexport interface FetchAppearanceArgs {\n /** Resolved URL (already substituted) — takes precedence when present. */\n appearanceUrl: string;\n /** Falls back to `${apiUrl}/api/agents/${slug}/appearance/`. */\n apiUrl: string;\n slug: string;\n /** Secret key sent as `x-api-key` (server/trusted mode). */\n apiKey?: string;\n /** Custom fetch (Node <18 / testing). Defaults to globalThis.fetch. */\n fetch?: typeof fetch;\n}\n\nexport async function fetchAppearance(\n args: FetchAppearanceArgs,\n): Promise<AppearanceConfig> {\n const url =\n args.appearanceUrl ||\n `${args.apiUrl.replace(/\\/$/, \"\")}/api/agents/${encodeURIComponent(\n args.slug,\n )}/appearance/`;\n\n const headers: Record<string, string> = { Accept: \"application/json\" };\n if (args.apiKey?.trim()) headers[\"x-api-key\"] = args.apiKey.trim();\n const doFetch =\n args.fetch ??\n (typeof globalThis.fetch === \"function\"\n ? globalThis.fetch.bind(globalThis)\n : null);\n if (!doFetch) {\n // eslint-disable-next-line no-console\n console.warn(\"[voice-agent] no fetch available; using default appearance\");\n return DEFAULT_APPEARANCE;\n }\n\n // eslint-disable-next-line no-console\n console.info(\"[voice-agent] fetching appearance from\", url);\n\n try {\n const r = await doFetch(url, { method: \"GET\", headers });\n if (!r.ok) {\n // eslint-disable-next-line no-console\n console.warn(\n `[voice-agent] appearance fetch returned ${r.status}; using defaults`,\n );\n return DEFAULT_APPEARANCE;\n }\n const body: any = await r.json();\n // eslint-disable-next-line no-console\n console.info(\"[voice-agent] appearance response\", body);\n const payload = unwrapAppearanceResponse(body);\n const merged = mergeAppearance(payload);\n // eslint-disable-next-line no-console\n console.info(\"[voice-agent] applied appearance\", merged);\n return merged;\n } catch (err) {\n // eslint-disable-next-line no-console\n console.warn(\"[voice-agent] appearance fetch failed; using defaults:\", err);\n return DEFAULT_APPEARANCE;\n }\n}\n\nfunction unwrapAppearanceResponse(\n body: unknown,\n): Partial<AppearanceConfig> | null {\n if (!isPlainObject(body)) return null;\n const safe = body as Record<string, unknown>;\n\n const candidates: unknown[] = [\n safe.data,\n safe.appearance,\n safe.widget_appearance,\n safe,\n ];\n for (const candidate of candidates) {\n if (isPlainObject(candidate)) {\n return candidate as Partial<AppearanceConfig>;\n }\n }\n return null;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return isPlainObject(value) ? value : {};\n}\n\nfunction nonEmptyString(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction stringOrDefault(value: unknown, fallback: string): string {\n return typeof value === \"string\" ? value : fallback;\n}\n\nfunction finitePositiveNumber(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0\n ? value\n : undefined;\n}\n\nfunction nonNegativeNumber(value: unknown): number | undefined {\n if (typeof value === \"number\" && Number.isFinite(value) && value >= 0) {\n return value;\n }\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const parsed = Number(trimmed);\n if (Number.isFinite(parsed) && parsed >= 0) return parsed;\n }\n return undefined;\n}\n\nfunction pixelOrNumber(value: unknown): number | undefined {\n const direct = finitePositiveNumber(value);\n if (direct !== undefined) return direct;\n if (typeof value !== \"string\") return undefined;\n\n const match = value.trim().match(/^(\\d+(?:\\.\\d+)?)px$/i);\n if (!match) return undefined;\n\n const parsed = Number(match[1]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n}\n\nfunction enumOrUndefined<T extends string>(\n allowed: readonly T[],\n value: unknown,\n): T | undefined {\n return typeof value === \"string\" && allowed.includes(value as T)\n ? (value as T)\n : undefined;\n}\n"],"names":["buildFieldSchema","definition","_a","toSchema","f","step","entry","opt","out","s","idx","collectInputFields","handleFormAction","topic","value","target","normalized","formId","payload","openId","payloadFormId","stringField","eventType","direction","stepIndex","fieldsForStep","totalSteps","EMAIL_RE","TEL_ALLOWED_RE","validateFields","fields","values","errors","field","digits","num","BOOK_DEMO_INTEREST_OPTIONS","DEFAULT_FORM_DEFINITIONS","matchForm","forms","_b","_c","normalize","t","form","id","candidate","explicitId","match","v","extractFormDraft","nested","key","draft","raw","initialFormValues","mergeFormDraft","current","next","k","buildSubmissionText","parts","allFields","submitForm","apiUrl","slug","sessionId","apiKey","fetchImpl","base","url","body","method","managed","formData","headers","response","data","normalizeFormDefinitions","steps","normalizeSteps","normalizeFields","normalizeLayout","enumField","stringArray","type","name","normalizeOptions","finiteNumber","index","layout","fieldLayout","density","labelPos","trimmed","label","parsed","allowed","DEFAULT_APPEARANCE","POSITIONS","mergeAppearance","partial","safe","asRecord","theme","dimensions","labels","nonEmptyString","stringOrDefault","pixelOrNumber","enumOrUndefined","normalizeLanguages","nonNegativeNumber","mergeForms","custom","map","seen","isPlainObject","code","native","fetchAppearance","args","doFetch","unwrapAppearanceResponse","merged","err","candidates","fallback","finitePositiveNumber","direct"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ const u = "voiceAgent.consent";
2
+ function o(t) {
3
+ return `${u}.${t}`;
4
+ }
5
+ function s() {
6
+ try {
7
+ return window.localStorage;
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ function a(t) {
13
+ if (!t) return null;
14
+ const n = s();
15
+ if (!n) return null;
16
+ let r = null;
17
+ try {
18
+ r = n.getItem(o(t));
19
+ } catch {
20
+ return null;
21
+ }
22
+ if (!r) return null;
23
+ try {
24
+ const e = JSON.parse(r);
25
+ if (!e || typeof e != "object") return null;
26
+ const c = typeof e.acceptedAt == "number" ? e.acceptedAt : 0, l = typeof e.termsUrl == "string" ? e.termsUrl : "";
27
+ return c ? { acceptedAt: c, termsUrl: l } : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ function f(t, n) {
33
+ const r = a(t);
34
+ return r ? r.termsUrl === n : !1;
35
+ }
36
+ function i(t, n) {
37
+ if (!t) return;
38
+ const r = s();
39
+ if (!r) return;
40
+ const e = {
41
+ acceptedAt: Date.now(),
42
+ termsUrl: n
43
+ };
44
+ try {
45
+ r.setItem(o(t), JSON.stringify(e));
46
+ } catch {
47
+ }
48
+ }
49
+ export {
50
+ f as h,
51
+ a as l,
52
+ i as s
53
+ };
54
+ //# sourceMappingURL=consent-CK9VXNPa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent-CK9VXNPa.js","sources":["../src/core/consent.ts"],"sourcesContent":["/**\n * Per-agent localStorage record of the user's acceptance of the terms &\n * conditions presented in the welcome screen. Starting a call implicitly\n * accepts the terms; we persist that acceptance so we can show a quieter\n * \"accepted\" state on return visits and have an audit trail of which\n * terms_url version was accepted.\n */\n\nconst STORAGE_PREFIX = \"voiceAgent.consent\";\n\nexport interface ConsentRecord {\n /** ms-since-epoch when the user first accepted. */\n acceptedAt: number;\n /** The terms_url that was in effect at acceptance time. */\n termsUrl: string;\n}\n\nfunction storageKey(agentSlug: string): string {\n return `${STORAGE_PREFIX}.${agentSlug}`;\n}\n\nfunction safeLocalStorage(): Storage | null {\n try {\n return window.localStorage;\n } catch {\n return null;\n }\n}\n\nexport function loadConsent(agentSlug: string): ConsentRecord | null {\n if (!agentSlug) return null;\n const ls = safeLocalStorage();\n if (!ls) return null;\n\n let raw: string | null = null;\n try {\n raw = ls.getItem(storageKey(agentSlug));\n } catch {\n return null;\n }\n if (!raw) return null;\n\n try {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== \"object\") return null;\n const acceptedAt =\n typeof parsed.acceptedAt === \"number\" ? parsed.acceptedAt : 0;\n const termsUrl =\n typeof parsed.termsUrl === \"string\" ? parsed.termsUrl : \"\";\n if (!acceptedAt) return null;\n return { acceptedAt, termsUrl };\n } catch {\n return null;\n }\n}\n\n/** Returns true if the user has accepted the *current* terms_url before. */\nexport function hasAcceptedTerms(agentSlug: string, termsUrl: string): boolean {\n const record = loadConsent(agentSlug);\n if (!record) return false;\n // If the link has changed since acceptance, require fresh acknowledgement.\n return record.termsUrl === termsUrl;\n}\n\nexport function saveConsent(agentSlug: string, termsUrl: string): void {\n if (!agentSlug) return;\n const ls = safeLocalStorage();\n if (!ls) return;\n const record: ConsentRecord = {\n acceptedAt: Date.now(),\n termsUrl,\n };\n try {\n ls.setItem(storageKey(agentSlug), JSON.stringify(record));\n } catch {\n // Quota or privacy mode — silently drop.\n }\n}\n"],"names":["STORAGE_PREFIX","storageKey","agentSlug","safeLocalStorage","loadConsent","ls","raw","parsed","acceptedAt","termsUrl","hasAcceptedTerms","record","saveConsent"],"mappings":"AAQA,MAAMA,IAAiB;AASvB,SAASC,EAAWC,GAA2B;AAC7C,SAAO,GAAGF,CAAc,IAAIE,CAAS;AACvC;AAEA,SAASC,IAAmC;AAC1C,MAAI;AACF,WAAO,OAAO;AAAA,EAChB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAASC,EAAYF,GAAyC;AACnE,MAAI,CAACA,EAAW,QAAO;AACvB,QAAMG,IAAKF,EAAA;AACX,MAAI,CAACE,EAAI,QAAO;AAEhB,MAAIC,IAAqB;AACzB,MAAI;AACF,IAAAA,IAAMD,EAAG,QAAQJ,EAAWC,CAAS,CAAC;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAACI,EAAK,QAAO;AAEjB,MAAI;AACF,UAAMC,IAAS,KAAK,MAAMD,CAAG;AAC7B,QAAI,CAACC,KAAU,OAAOA,KAAW,SAAU,QAAO;AAClD,UAAMC,IACJ,OAAOD,EAAO,cAAe,WAAWA,EAAO,aAAa,GACxDE,IACJ,OAAOF,EAAO,YAAa,WAAWA,EAAO,WAAW;AAC1D,WAAKC,IACE,EAAE,YAAAA,GAAY,UAAAC,EAAA,IADG;AAAA,EAE1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAASC,EAAiBR,GAAmBO,GAA2B;AAC7E,QAAME,IAASP,EAAYF,CAAS;AACpC,SAAKS,IAEEA,EAAO,aAAaF,IAFP;AAGtB;AAEO,SAASG,EAAYV,GAAmBO,GAAwB;AACrE,MAAI,CAACP,EAAW;AAChB,QAAMG,IAAKF,EAAA;AACX,MAAI,CAACE,EAAI;AACT,QAAMM,IAAwB;AAAA,IAC5B,YAAY,KAAK,IAAA;AAAA,IACjB,UAAAF;AAAA,EAAA;AAEF,MAAI;AACF,IAAAJ,EAAG,QAAQJ,EAAWC,CAAS,GAAG,KAAK,UAAUS,CAAM,CAAC;AAAA,EAC1D,QAAQ;AAAA,EAER;AACF;"}
@@ -0,0 +1,2 @@
1
+ "use strict";const a="voiceAgent.consent";function s(t){return`${a}.${t}`}function o(){try{return window.localStorage}catch{return null}}function l(t){if(!t)return null;const n=o();if(!n)return null;let e=null;try{e=n.getItem(s(t))}catch{return null}if(!e)return null;try{const r=JSON.parse(e);if(!r||typeof r!="object")return null;const c=typeof r.acceptedAt=="number"?r.acceptedAt:0,u=typeof r.termsUrl=="string"?r.termsUrl:"";return c?{acceptedAt:c,termsUrl:u}:null}catch{return null}}function f(t,n){const e=l(t);return e?e.termsUrl===n:!1}function i(t,n){if(!t)return;const e=o();if(!e)return;const r={acceptedAt:Date.now(),termsUrl:n};try{e.setItem(s(t),JSON.stringify(r))}catch{}}exports.hasAcceptedTerms=f;exports.loadConsent=l;exports.saveConsent=i;
2
+ //# sourceMappingURL=consent-D7QNSkQD.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent-D7QNSkQD.cjs","sources":["../src/core/consent.ts"],"sourcesContent":["/**\n * Per-agent localStorage record of the user's acceptance of the terms &\n * conditions presented in the welcome screen. Starting a call implicitly\n * accepts the terms; we persist that acceptance so we can show a quieter\n * \"accepted\" state on return visits and have an audit trail of which\n * terms_url version was accepted.\n */\n\nconst STORAGE_PREFIX = \"voiceAgent.consent\";\n\nexport interface ConsentRecord {\n /** ms-since-epoch when the user first accepted. */\n acceptedAt: number;\n /** The terms_url that was in effect at acceptance time. */\n termsUrl: string;\n}\n\nfunction storageKey(agentSlug: string): string {\n return `${STORAGE_PREFIX}.${agentSlug}`;\n}\n\nfunction safeLocalStorage(): Storage | null {\n try {\n return window.localStorage;\n } catch {\n return null;\n }\n}\n\nexport function loadConsent(agentSlug: string): ConsentRecord | null {\n if (!agentSlug) return null;\n const ls = safeLocalStorage();\n if (!ls) return null;\n\n let raw: string | null = null;\n try {\n raw = ls.getItem(storageKey(agentSlug));\n } catch {\n return null;\n }\n if (!raw) return null;\n\n try {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== \"object\") return null;\n const acceptedAt =\n typeof parsed.acceptedAt === \"number\" ? parsed.acceptedAt : 0;\n const termsUrl =\n typeof parsed.termsUrl === \"string\" ? parsed.termsUrl : \"\";\n if (!acceptedAt) return null;\n return { acceptedAt, termsUrl };\n } catch {\n return null;\n }\n}\n\n/** Returns true if the user has accepted the *current* terms_url before. */\nexport function hasAcceptedTerms(agentSlug: string, termsUrl: string): boolean {\n const record = loadConsent(agentSlug);\n if (!record) return false;\n // If the link has changed since acceptance, require fresh acknowledgement.\n return record.termsUrl === termsUrl;\n}\n\nexport function saveConsent(agentSlug: string, termsUrl: string): void {\n if (!agentSlug) return;\n const ls = safeLocalStorage();\n if (!ls) return;\n const record: ConsentRecord = {\n acceptedAt: Date.now(),\n termsUrl,\n };\n try {\n ls.setItem(storageKey(agentSlug), JSON.stringify(record));\n } catch {\n // Quota or privacy mode — silently drop.\n }\n}\n"],"names":["STORAGE_PREFIX","storageKey","agentSlug","safeLocalStorage","loadConsent","ls","raw","parsed","acceptedAt","termsUrl","hasAcceptedTerms","record","saveConsent"],"mappings":"aAQA,MAAMA,EAAiB,qBASvB,SAASC,EAAWC,EAA2B,CAC7C,MAAO,GAAGF,CAAc,IAAIE,CAAS,EACvC,CAEA,SAASC,GAAmC,CAC1C,GAAI,CACF,OAAO,OAAO,YAChB,MAAQ,CACN,OAAO,IACT,CACF,CAEO,SAASC,EAAYF,EAAyC,CACnE,GAAI,CAACA,EAAW,OAAO,KACvB,MAAMG,EAAKF,EAAA,EACX,GAAI,CAACE,EAAI,OAAO,KAEhB,IAAIC,EAAqB,KACzB,GAAI,CACFA,EAAMD,EAAG,QAAQJ,EAAWC,CAAS,CAAC,CACxC,MAAQ,CACN,OAAO,IACT,CACA,GAAI,CAACI,EAAK,OAAO,KAEjB,GAAI,CACF,MAAMC,EAAS,KAAK,MAAMD,CAAG,EAC7B,GAAI,CAACC,GAAU,OAAOA,GAAW,SAAU,OAAO,KAClD,MAAMC,EACJ,OAAOD,EAAO,YAAe,SAAWA,EAAO,WAAa,EACxDE,EACJ,OAAOF,EAAO,UAAa,SAAWA,EAAO,SAAW,GAC1D,OAAKC,EACE,CAAE,WAAAA,EAAY,SAAAC,CAAA,EADG,IAE1B,MAAQ,CACN,OAAO,IACT,CACF,CAGO,SAASC,EAAiBR,EAAmBO,EAA2B,CAC7E,MAAME,EAASP,EAAYF,CAAS,EACpC,OAAKS,EAEEA,EAAO,WAAaF,EAFP,EAGtB,CAEO,SAASG,EAAYV,EAAmBO,EAAwB,CACrE,GAAI,CAACP,EAAW,OAChB,MAAMG,EAAKF,EAAA,EACX,GAAI,CAACE,EAAI,OACT,MAAMM,EAAwB,CAC5B,WAAY,KAAK,IAAA,EACjB,SAAAF,CAAA,EAEF,GAAI,CACFJ,EAAG,QAAQJ,EAAWC,CAAS,EAAG,KAAK,UAAUS,CAAM,CAAC,CAC1D,MAAQ,CAER,CACF"}
@@ -0,0 +1,30 @@
1
+ type WidgetEventType = "widget_loaded" | "bubble_clicked";
2
+ interface TrackConfig {
3
+ apiUrl: string;
4
+ agentSlug: string;
5
+ /** Secret key sent as `x-api-key` (server/trusted mode). */
6
+ apiKey?: string;
7
+ /** Custom fetch (Node <18 / testing). Defaults to globalThis.fetch. */
8
+ fetch?: typeof fetch;
9
+ /** When true, suppress all event reporting. */
10
+ disabled?: boolean;
11
+ }
12
+ /**
13
+ * Return a stable per-browser id, generating and persisting one on first use.
14
+ * Falls back to an ephemeral id if localStorage is unavailable (private mode,
15
+ * blocked storage) so callers always get a usable value.
16
+ */
17
+ export declare function getVisitorId(): string;
18
+ /**
19
+ * Report a widget engagement event. Best-effort and fire-and-forget.
20
+ *
21
+ * Uses `fetch` with `keepalive: true` and `Content-Type: application/json` —
22
+ * the same cross-origin transport the session and form-submit POSTs already
23
+ * use successfully (see fetchSession in widget.ts). We deliberately do NOT use
24
+ * navigator.sendBeacon: it returns true the moment it *queues* the request
25
+ * (so callers can't tell it failed), and a cross-origin beacon with a JSON
26
+ * content type needs a CORS preflight that can be silently dropped — events
27
+ * would vanish. Errors are swallowed so analytics never breaks the widget.
28
+ */
29
+ export declare function trackWidgetEvent(cfg: TrackConfig, eventType: WidgetEventType, metadata?: Record<string, unknown>): void;
30
+ export {};
@@ -0,0 +1,113 @@
1
+ import { FormDefinition } from './forms';
2
+ /**
3
+ * Per-agent widget appearance configuration.
4
+ *
5
+ * The widget fetches this from `GET {apiUrl}/api/agents/{slug}/appearance/`
6
+ * (override with `data-appearance-url`) on init, falling back to the defaults
7
+ * below if the request fails or any field is missing.
8
+ *
9
+ * Every field is optional on the wire — partial responses are merged on top of
10
+ * the defaults so the widget never breaks if the backend hasn't been updated.
11
+ */
12
+ export type WidgetPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
13
+ export interface AppearanceTheme {
14
+ primary_color: string;
15
+ accent_color: string;
16
+ background_color: string;
17
+ text_color: string;
18
+ user_bubble_color: string;
19
+ user_bubble_text_color: string;
20
+ agent_bubble_color: string;
21
+ agent_bubble_text_color: string;
22
+ }
23
+ export interface AppearanceDimensions {
24
+ fab_size: number;
25
+ panel_width: number;
26
+ panel_height: number;
27
+ border_radius: number;
28
+ }
29
+ export interface AppearanceLayout {
30
+ position: WidgetPosition;
31
+ font_family: string;
32
+ }
33
+ export interface AppearanceLabels {
34
+ idle: string;
35
+ connecting: string;
36
+ listening: string;
37
+ speaking: string;
38
+ /** Shown while the agent is processing (running STT→LLM→tool→TTS). The
39
+ * backend may override this with a per-tool contextual label
40
+ * ("Searching the knowledge base…") via voice.agent_status events. */
41
+ thinking: string;
42
+ muted: string;
43
+ call_ended: string;
44
+ transcript_placeholder: string;
45
+ language_label: string;
46
+ }
47
+ export interface AppearanceLanguage {
48
+ /** BCP-47-ish short code sent to the backend (e.g. "en", "ne"). */
49
+ code: string;
50
+ /** English label, e.g. "English", "Nepali". */
51
+ label: string;
52
+ /** Native script label, e.g. "English", "नेपाली". Falls back to label. */
53
+ native_label: string;
54
+ }
55
+ export interface AppearanceConfig {
56
+ name: string;
57
+ subtitle: string;
58
+ logo_url: string;
59
+ fab_label: string;
60
+ fab_sublabel: string;
61
+ start_button_text: string;
62
+ powered_by_text: string;
63
+ powered_by_url: string;
64
+ theme: AppearanceTheme;
65
+ dimensions: AppearanceDimensions;
66
+ layout: AppearanceLayout;
67
+ labels: AppearanceLabels;
68
+ languages: AppearanceLanguage[];
69
+ default_language: string;
70
+ /** Hard cap on a single voice call, in seconds. 0 = no limit. */
71
+ max_call_seconds: number;
72
+ /** URL to the terms & conditions / privacy policy page. When set, a
73
+ * consent line is shown beneath the Start button stating that starting
74
+ * the call accepts these terms. Empty disables the consent UI. */
75
+ terms_url: string;
76
+ /** Visible label for the terms link (default "Terms & Conditions"). */
77
+ terms_label: string;
78
+ /** Prefix shown before the terms link (default
79
+ * "By starting the call, you agree to our"). */
80
+ consent_text: string;
81
+ /**
82
+ * Extra form definitions (book-a-demo, reservation, lead capture, …) the
83
+ * agent can open mid-call by publishing a LiveKit data message. Built-in
84
+ * defaults are always available; entries here override defaults by id.
85
+ * See `forms.ts` for the wire protocol.
86
+ */
87
+ forms: FormDefinition[];
88
+ /**
89
+ * Whether to expose the in-widget audio settings drawer (mic/speaker
90
+ * selection, NC/AEC/AGC toggles, headphone mode, mic-level meter,
91
+ * diagnostics row). Defaults to true. Set false to hide the gear icon
92
+ * entirely for a kiosk-style minimal UI.
93
+ */
94
+ show_audio_settings: boolean;
95
+ }
96
+ export declare const DEFAULT_APPEARANCE: AppearanceConfig;
97
+ /**
98
+ * Deep-merge a partial server response on top of DEFAULT_APPEARANCE.
99
+ * Only known keys are accepted; unknown keys are ignored.
100
+ */
101
+ export declare function mergeAppearance(partial: Partial<AppearanceConfig> | null | undefined): AppearanceConfig;
102
+ export interface FetchAppearanceArgs {
103
+ /** Resolved URL (already substituted) — takes precedence when present. */
104
+ appearanceUrl: string;
105
+ /** Falls back to `${apiUrl}/api/agents/${slug}/appearance/`. */
106
+ apiUrl: string;
107
+ slug: string;
108
+ /** Secret key sent as `x-api-key` (server/trusted mode). */
109
+ apiKey?: string;
110
+ /** Custom fetch (Node <18 / testing). Defaults to globalThis.fetch. */
111
+ fetch?: typeof fetch;
112
+ }
113
+ export declare function fetchAppearance(args: FetchAppearanceArgs): Promise<AppearanceConfig>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Per-agent audio preferences for the voice widget.
3
+ *
4
+ * Persists the user's chosen mic/speaker devices, processor toggles, and
5
+ * speaker volume so subsequent calls open with the same setup. Reads &
6
+ * writes are namespaced by agent slug so different agents can hold
7
+ * different preferences in the same browser.
8
+ */
9
+ /**
10
+ * Choice of deep-learning noise-suppression engine layered on top of the
11
+ * browser's native NS. Picked per-user via the audio settings drawer.
12
+ *
13
+ * - `off` — rely on browser NS + voiceIsolation only.
14
+ * - `krisp` — @livekit/krisp-noise-filter (closed-source, free for LK).
15
+ * - `deepfilter` — deepfilternet3-noise-filter (mezonai/mezon-noise-suppression),
16
+ * open-source DeepFilterNet3 with a 0-100 strength knob.
17
+ */
18
+ export type NoiseFilterEngine = "off" | "krisp" | "deepfilter";
19
+ export interface AudioPrefs {
20
+ /** Browser-native AEC. Standard MediaTrackSettings.echoCancellation. */
21
+ echoCancellation: boolean;
22
+ /** Browser-native noise suppression. Standard MediaTrackSettings. */
23
+ noiseSuppression: boolean;
24
+ /** Browser-native automatic gain control. */
25
+ autoGainControl: boolean;
26
+ /** Microsoft / Chrome non-standard hardware voice isolation. */
27
+ voiceIsolation: boolean;
28
+ /** Which deep-learning noise-suppression processor to attach. */
29
+ noiseFilter: NoiseFilterEngine;
30
+ /** DeepFilterNet3 strength 0-100 (ignored unless noiseFilter === "deepfilter"). */
31
+ deepFilterStrength: number;
32
+ /** Selected mic deviceId (empty string = system default). */
33
+ micDeviceId: string;
34
+ /** Selected speaker deviceId for setSinkId (empty = default). */
35
+ speakerDeviceId: string;
36
+ /** Speaker volume 0-100, applied to HTMLAudioElement.volume / 100. */
37
+ outputVolume: number;
38
+ /**
39
+ * When true, disables half-duplex ducking. The user wears headphones so
40
+ * the mic can't physically pick up the speaker — no need to mute them
41
+ * while the agent talks.
42
+ */
43
+ headphonesMode: boolean;
44
+ /** Whether to show live transcription in the UI. */
45
+ transcriptionEnabled: boolean;
46
+ /** Whether to show the text input box during a call. */
47
+ textInputEnabled: boolean;
48
+ }
49
+ export declare const DEFAULT_AUDIO_PREFS: AudioPrefs;
50
+ export declare function loadAudioPrefs(agentSlug: string): AudioPrefs;
51
+ export declare function saveAudioPrefs(agentSlug: string, prefs: AudioPrefs): void;
52
+ export interface AudioDevices {
53
+ inputs: MediaDeviceInfo[];
54
+ outputs: MediaDeviceInfo[];
55
+ }
56
+ /**
57
+ * Enumerate audio I/O devices. Device labels are only populated after the
58
+ * user has granted mic permission for the page; before that the labels
59
+ * come back empty and the caller should show a placeholder.
60
+ */
61
+ export declare function enumerateAudioDevices(): Promise<AudioDevices>;
62
+ /** Browser-feature probes used to decide which UI controls make sense. */
63
+ export interface AudioCapabilities {
64
+ /** setSinkId is unsupported on Safari and Firefox at the time of writing. */
65
+ setSinkIdSupported: boolean;
66
+ /** voiceIsolation is honored on Chromium/Edge with system support only. */
67
+ voiceIsolationSupported: boolean;
68
+ }
69
+ export declare function probeAudioCapabilities(): AudioCapabilities;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Per-agent localStorage record of the user's acceptance of the terms &
3
+ * conditions presented in the welcome screen. Starting a call implicitly
4
+ * accepts the terms; we persist that acceptance so we can show a quieter
5
+ * "accepted" state on return visits and have an audit trail of which
6
+ * terms_url version was accepted.
7
+ */
8
+ export interface ConsentRecord {
9
+ /** ms-since-epoch when the user first accepted. */
10
+ acceptedAt: number;
11
+ /** The terms_url that was in effect at acceptance time. */
12
+ termsUrl: string;
13
+ }
14
+ export declare function loadConsent(agentSlug: string): ConsentRecord | null;
15
+ /** Returns true if the user has accepted the *current* terms_url before. */
16
+ export declare function hasAcceptedTerms(agentSlug: string, termsUrl: string): boolean;
17
+ export declare function saveConsent(agentSlug: string, termsUrl: string): void;
@@ -0,0 +1,79 @@
1
+ import { AppearanceConfig } from './appearance';
2
+ import { AudioPrefs, AudioCapabilities, AudioDevices } from './audioSettings';
3
+ import { EventHandler, EventName } from './events';
4
+ import { FormController } from './formController';
5
+ import { FormDefinition } from './forms';
6
+ import { DeepFilterUrls } from './types';
7
+ import { AudioStateSnapshot, AudioStats } from './voice';
8
+ export interface VoiceAgentConfig {
9
+ /** AICharacter.slug on the backend. Required. */
10
+ agentSlug: string;
11
+ /** Backend base URL. Default "https://api.oshara.ai". */
12
+ apiUrl?: string;
13
+ /**
14
+ * Secret API key (sk_…) sent as `x-api-key`, bypassing origin gating.
15
+ * OMIT for public browser embeds (rely on the origin allow-list). Intended
16
+ * for server-side / Node usage; warns if used in a browser.
17
+ */
18
+ apiKey?: string;
19
+ /** Override the appearance endpoint URL. */
20
+ appearanceUrl?: string;
21
+ /** UI language (BCP-47 short). Default "en". */
22
+ language?: string;
23
+ /** DeepFilterNet3 asset overrides (same defaults/precedence as the widget). */
24
+ deepFilter?: DeepFilterUrls;
25
+ /** Auto-fetch appearance during init() (default true). */
26
+ fetchAppearanceOnInit?: boolean;
27
+ /** Disable analytics events (default false). */
28
+ disableAnalytics?: boolean;
29
+ /** Custom fetch (Node <18 / testing). Defaults to globalThis.fetch. */
30
+ fetch?: typeof fetch;
31
+ /**
32
+ * Seed audio preferences. Merged over DEFAULT_AUDIO_PREFS and (when
33
+ * persistAudioPrefs) the stored per-agent prefs.
34
+ */
35
+ audio?: Partial<AudioPrefs>;
36
+ /** Read/write audio prefs to localStorage (default true). */
37
+ persistAudioPrefs?: boolean;
38
+ }
39
+ export interface VoiceAgentClient {
40
+ /** The AICharacter slug this client targets. */
41
+ readonly agentSlug: string;
42
+ /** Fetch appearance (if enabled) and prepare the agent. Idempotent. */
43
+ init: () => Promise<{
44
+ appearance: AppearanceConfig;
45
+ }>;
46
+ /** Start a call. */
47
+ start: () => Promise<void>;
48
+ /** End the call. */
49
+ end: () => Promise<void>;
50
+ /** Tear down listeners + any active call. */
51
+ destroy: () => void;
52
+ toggleMute: () => Promise<boolean>;
53
+ isActive: () => boolean;
54
+ sessionId: () => string | null;
55
+ /** Send a typed text message to the agent (also emits a transcript line). */
56
+ sendText: (text: string) => Promise<void>;
57
+ publishData: (payload: unknown, topic: string) => Promise<void>;
58
+ updateAudioSettings: (delta: Partial<AudioPrefs>) => Promise<AudioStateSnapshot>;
59
+ getAudioState: () => AudioStateSnapshot;
60
+ getAudioStats: () => Promise<AudioStats | null>;
61
+ enumerateAudioDevices: () => Promise<AudioDevices>;
62
+ getAudioCapabilities: () => AudioCapabilities;
63
+ getActiveForm: () => ReturnType<FormController["getActive"]>;
64
+ /** Push on-screen form edits into the model (custom UIs call this on input). */
65
+ updateFormValues: (values: Record<string, string>) => void;
66
+ stepForm: (direction: "next" | "back" | number) => void;
67
+ submitForm: () => void;
68
+ closeForm: () => void;
69
+ /** Programmatically open a form. */
70
+ openForm: (definition: FormDefinition, draft?: Record<string, string>) => void;
71
+ getAppearance: () => AppearanceConfig;
72
+ setLanguage: (code: string) => void;
73
+ getLanguage: () => string;
74
+ /** Report an engagement event (widget_loaded / bubble_clicked). */
75
+ trackEvent: (type: "widget_loaded" | "bubble_clicked", metadata?: Record<string, unknown>) => void;
76
+ on: <K extends EventName>(event: K, handler: EventHandler<K>) => () => void;
77
+ off: <K extends EventName>(event: K, handler: EventHandler<K>) => void;
78
+ }
79
+ export declare function createVoiceAgent(config: VoiceAgentConfig): VoiceAgentClient;