@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
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @oshara/voice-sdk
2
+
3
+ Embed the Oshara voice agent in any web app. Add your **agent slug** (and optionally an API key) and everything initializes exactly like the widget: appearance is fetched, the voice call connects, and agent‑triggered **forms** surface automatically.
4
+
5
+ Three ways to use it, one package:
6
+
7
+ | Goal | Import | What you get |
8
+ |------|--------|--------------|
9
+ | **Custom UI** | `@oshara/voice-sdk` | Headless client — events + methods, zero DOM |
10
+ | **Prebuilt UI** | `@oshara/voice-sdk/ui` | `mountVoiceUI(client)` — the full widget UI |
11
+ | **React** | `@oshara/voice-sdk/react` | `useVoiceAgent()` hook + `<VoiceWidget>` |
12
+ | **Script tag** | `widget.js` | The drop‑in embeddable widget (built on this SDK) |
13
+
14
+ ---
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @oshara/voice-sdk livekit-client
20
+ ```
21
+
22
+ `livekit-client` and `@livekit/krisp-noise-filter` are runtime dependencies; in most apps your bundler dedupes a single copy. `react`/`react-dom` are optional peers, only needed for the `/react` entry.
23
+
24
+ ---
25
+
26
+ ## Quickstart
27
+
28
+ ### 1. Prebuilt UI (5 lines)
29
+
30
+ ```ts
31
+ import { createVoiceAgent } from "@oshara/voice-sdk";
32
+ import { mountVoiceUI } from "@oshara/voice-sdk/ui";
33
+
34
+ const client = createVoiceAgent({ agentSlug: "support-bot" });
35
+ await client.init(); // fetch appearance
36
+ mountVoiceUI(client); // FAB + panel + call/form screens
37
+ ```
38
+
39
+ ### 2. Script tag (no build step)
40
+
41
+ ```html
42
+ <script
43
+ src="https://cdn.oshara.ai/widget.js"
44
+ data-agent="support-bot"
45
+ data-api-url="https://api.oshara.ai"></script>
46
+ ```
47
+
48
+ ### 3. Headless (bring your own UI)
49
+
50
+ ```ts
51
+ import { createVoiceAgent } from "@oshara/voice-sdk";
52
+
53
+ const client = createVoiceAgent({ agentSlug: "support-bot" });
54
+ client.on("state", ({ orb }) => render(orb));
55
+ client.on("transcript", ({ role, text, isFinal }) => appendBubble(role, text, isFinal));
56
+ client.on("form:show", ({ definition }) => renderMyForm(definition));
57
+
58
+ await client.init();
59
+ startButton.onclick = () => client.start();
60
+ ```
61
+
62
+ ### 4. React
63
+
64
+ ```tsx
65
+ import { useVoiceAgent } from "@oshara/voice-sdk/react";
66
+
67
+ function CallButton() {
68
+ const { ready, connected, orb, transcript, start, end } = useVoiceAgent({
69
+ agentSlug: "support-bot",
70
+ });
71
+ return (
72
+ <button disabled={!ready} onClick={connected ? end : start}>
73
+ {connected ? `End (${orb})` : "Talk to us"}
74
+ </button>
75
+ );
76
+ }
77
+ ```
78
+
79
+ Or drop in the prebuilt UI: `<VoiceWidget config={{ agentSlug: "support-bot" }} />`.
80
+
81
+ ---
82
+
83
+ ## Authentication
84
+
85
+ Two modes (see [docs/authentication.md](docs/authentication.md)):
86
+
87
+ - **Public browser embed (default, recommended):** pass only `agentSlug`. The backend authorizes by matching the request origin against the agent's allow‑list — exactly how the widget works. No key in your bundle.
88
+ - **Server / trusted mode:** pass `apiKey: "sk_…"`, sent as `x-api-key`, bypassing origin gating. Intended for Node / server‑side usage. **A secret key in browser JS is world‑readable** — the SDK logs a warning if you do this.
89
+
90
+ ```ts
91
+ // Browser (public)
92
+ createVoiceAgent({ agentSlug: "support-bot" });
93
+
94
+ // Node / server
95
+ createVoiceAgent({ agentSlug: "support-bot", apiKey: process.env.OSHARA_KEY, fetch });
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Configuration
101
+
102
+ `createVoiceAgent(config)` accepts a complete superset of the widget's knobs — a bare `{ agentSlug }` initializes identically to the widget (same noise filtering, audio defaults, DeepFilter URLs).
103
+
104
+ ```ts
105
+ interface VoiceAgentConfig {
106
+ agentSlug: string; // required
107
+ apiUrl?: string; // default "https://api.oshara.ai"
108
+ apiKey?: string; // sk_… → x-api-key (server/trusted)
109
+ appearanceUrl?: string; // override appearance endpoint
110
+ language?: string; // default "en"
111
+ fetchAppearanceOnInit?: boolean; // default true
112
+ disableAnalytics?: boolean; // default false
113
+ fetch?: typeof fetch; // inject for Node <18
114
+
115
+ deepFilter?: { cdnUrl?; wasmUrl?; onnxUrl?; moduleUrl? };
116
+
117
+ // Seed audio prefs (merged over defaults + stored prefs). Same fields/defaults
118
+ // as the widget: noiseFilter "deepfilter", deepFilterStrength 40,
119
+ // voiceIsolation on, autoGainControl off, outputVolume 85, headphonesMode on…
120
+ audio?: Partial<AudioPrefs>;
121
+ persistAudioPrefs?: boolean; // default true (localStorage)
122
+ }
123
+ ```
124
+
125
+ UI‑only options (`openChat`, `inline`, `closeButtonHide`) live on `mountVoiceUI`, not here.
126
+
127
+ ---
128
+
129
+ ## Client API
130
+
131
+ ```ts
132
+ const client = createVoiceAgent(config);
133
+
134
+ await client.init(); // fetch appearance → { appearance }
135
+ await client.start(); // connect a call
136
+ await client.end(); // hang up
137
+ await client.toggleMute();
138
+ client.isActive(); client.sessionId();
139
+ await client.sendText("hello"); // typed message to the agent
140
+ await client.updateAudioSettings({ noiseFilter: "krisp" });
141
+ client.getAudioState(); await client.getAudioStats();
142
+ await client.enumerateAudioDevices(); client.getAudioCapabilities();
143
+
144
+ // forms
145
+ client.getActiveForm();
146
+ client.updateFormValues({ email: "a@b.com" }); // push on-screen edits
147
+ client.stepForm("next"); client.submitForm(); client.closeForm();
148
+
149
+ client.getAppearance(); client.setLanguage("ne");
150
+ client.on(event, handler); client.off(event, handler);
151
+ client.destroy();
152
+ ```
153
+
154
+ ### Events
155
+
156
+ `client.on(name, payload => …)`. Full reference in [docs/events.md](docs/events.md). Highlights:
157
+
158
+ | Event | Payload | Use |
159
+ |-------|---------|-----|
160
+ | `state` | `{ orb, statusLabel }` | Drive the orb / indicator |
161
+ | `call:status` | `{ status }` | Status line text |
162
+ | `connection` | `{ phase }` | connecting → connected → disconnected/failed |
163
+ | `transcript` | `{ role, segmentId, text, isFinal }` | Live transcript |
164
+ | `mute` | `{ muted }` | Mute button state |
165
+ | `audio` | `AudioStateSnapshot` | Audio settings drawer |
166
+ | `form:show` | `{ definition, draft, stepIndex, inCall }` | Render an agent‑triggered form |
167
+ | `form:update` / `form:validation` / `form:submitted` / `form:error` / `form:close` | — | Form lifecycle |
168
+ | `agent:handoff` | `{ agentName }` | Mid‑call agent swap |
169
+ | `error` | `{ scope, error }` | Non‑fatal errors |
170
+
171
+ ---
172
+
173
+ ## Building a custom UI
174
+
175
+ See [docs/custom-ui.md](docs/custom-ui.md) and [examples/headless-vanilla](examples/headless-vanilla). The contract:
176
+
177
+ 1. Subscribe to `state` / `transcript` / `connection` and render them.
178
+ 2. On `form:show`, render the `FormDefinition`'s fields.
179
+ 3. Call `client.updateFormValues(values)` on every keystroke (this replaces the prebuilt UI's DOM reads), then `client.submitForm()`.
180
+
181
+ The core owns the form `values` model and the agent round‑trip (validation, `form_submit_failed`, confirmation). You only render.
182
+
183
+ ---
184
+
185
+ ## Docs
186
+
187
+ - [Authentication & API keys](docs/authentication.md)
188
+ - [Events & types reference](docs/events.md)
189
+ - [Building a custom UI](docs/custom-ui.md)
190
+ - [Forms](docs/forms.md)
191
+ - [React](docs/react.md)
192
+
193
+ ## Examples
194
+
195
+ - [examples/script-tag.html](examples/script-tag.html)
196
+ - [examples/headless-vanilla](examples/headless-vanilla)
197
+ - [examples/prebuilt](examples/prebuilt)
198
+ - [examples/react](examples/react)
@@ -0,0 +1,2 @@
1
+ "use strict";function C(t){var o;const n=(e,i)=>{var s;const r={name:e.name,label:e.label,type:e.type,required:!!e.required,step:i};return(s=e.options)!=null&&s.length&&(r.options=e.options.map(f=>typeof f=="string"?{value:f,label:f}:{value:f.value,label:f.label??f.value})),e.pattern&&(r.pattern=e.pattern),e.min!==void 0&&(r.min=e.min),e.max!==void 0&&(r.max=e.max),r};if((o=t.steps)!=null&&o.length){const e=[];return t.steps.forEach((i,r)=>{for(const s of i.fields)s.type!=="display"&&s.name&&e.push(n(s,r))}),e}return x(t).map(e=>n(e,0))}function D(t,n,o){var _;if(!t)return!1;const e=t.trim().toLowerCase();if(!e.startsWith("form.")||!e.endsWith(".action"))return!1;const i=e.slice(5,-7);if(!i||!n||typeof n!="object")return!1;const r=n,s=o.current();if(!s||s.trim().toLowerCase()!==i)return!1;const f=l(r.form_id);if(f&&f.trim().toLowerCase()!==i)return!1;const d=(l(r.type)??l(r.event)??"").trim().toLowerCase();if(d==="form_step"){const c=(_=l(r.direction))==null?void 0:_.trim().toLowerCase();if(c==="next"||c==="back")return o.step(c),!0;const m=r.step_index;return typeof m=="number"&&Number.isFinite(m)&&o.step(m),!0}return d==="form_submit"?(o.submit(),!0):d==="form_close"?(o.close(),!0):!1}function x(t){var o;return((o=t.steps)!=null&&o.length?t.steps.flatMap(e=>e.fields):t.fields).filter(e=>e.type!=="display"&&!!e.name)}function L(t,n){var o;if((o=t.steps)!=null&&o.length){const e=t.steps[Math.max(0,Math.min(n,t.steps.length-1))];return(e==null?void 0:e.fields)??[]}return t.fields}function j(t){var n;return((n=t.steps)==null?void 0:n.length)||1}const q=/^[^\s@]+@[^\s@]+\.[^\s@]+$/,z=/^[+()\-.\s\d]+$/;function M(t,n){const o=[];for(const e of t){if(e.type==="display"||!e.name)continue;const r=(n[e.name]??"").trim();if(!r){e.required&&o.push({name:e.name,label:e.label,message:`${e.label} is required.`});continue}if(e.type==="email"&&!q.test(r)){o.push({name:e.name,label:e.label,message:"Enter a valid email address."});continue}if(e.type==="tel"){const s=r.replace(/\D/g,"");if(!z.test(r)||s.length<7){o.push({name:e.name,label:e.label,message:"Enter a valid phone number."});continue}}if(e.type==="number"){const s=Number(r);if(!Number.isFinite(s)){o.push({name:e.name,label:e.label,message:"Enter a valid number."});continue}if(e.min!==void 0&&s<e.min){o.push({name:e.name,label:e.label,message:`Must be at least ${e.min}.`});continue}if(e.max!==void 0&&s>e.max){o.push({name:e.name,label:e.label,message:`Must be at most ${e.max}.`});continue}}if(e.pattern)try{if(!new RegExp(e.pattern).test(r)){o.push({name:e.name,label:e.label,message:`Please enter a valid ${e.label.toLowerCase()}.`});continue}}catch{}}return o}const E=["Enterprise Voice Platform (OVIP)","Fine-Tuning Automation (OFTA)","Consumer / Kids Products","Investor Relations","Partnership / Integration"],k=[{id:"book-demo",title:"Book a demo",subtitle:"The voice agent filled this from the call. Review and edit before sending.",submit_url:"/api/book-demo/",submit_label:"Confirm & send",success_message:"Thanks! Your demo request has been sent. We'll get back to you soon.",topics:["book-demo.form","book-demo.review","form.book-demo"],event_types:["book_demo_form","book-demo-form"],confirmation_type:"book_demo_submitted",fields:[{name:"name",label:"Full name",type:"text",required:!0,placeholder:"Your full name"},{name:"organization",label:"Organization",type:"text",required:!0,placeholder:"Your company or organization"},{name:"email",label:"Email",type:"email",required:!0,placeholder:"you@organization.com"},{name:"interested_in",label:"I'm interested in...",type:"select",required:!0,options:E,default_value:E[0]},{name:"description",label:"Why do you want to book a demo?",type:"textarea",required:!0,rows:4,placeholder:"Describe your use case, goals, or questions..."}]},{id:"reservation",title:"Make a reservation",subtitle:"The voice agent filled this from the call. Review and edit before sending.",submit_url:"/api/reservations/",submit_label:"Confirm reservation",success_message:"Reservation confirmed. We'll send a confirmation email shortly.",topics:["reservation.form","form.reservation"],event_types:["reservation_form","reservation-form"],fields:[{name:"name",label:"Name",type:"text",required:!0,placeholder:"Your name"},{name:"email",label:"Email",type:"email",required:!0,placeholder:"you@example.com"},{name:"phone",label:"Phone",type:"tel",placeholder:"+977 98XXXXXXXX"},{name:"party_size",label:"Party size",type:"text",required:!0,placeholder:"e.g. 4"},{name:"date",label:"Date",type:"text",required:!0,placeholder:"YYYY-MM-DD"},{name:"time",label:"Time",type:"text",required:!0,placeholder:"HH:MM"},{name:"notes",label:"Notes",type:"textarea",rows:3,placeholder:"Allergies, occasion, seating preference…"}]}];function R(t,n,o){var f,d,_;if(!n||typeof n!="object")return null;const e=c=>c.trim().toLowerCase();if(t){const c=e(t);for(const m of o){if((f=m.topics)!=null&&f.some(T=>e(T)===c))return m;const b=e(m.id);if(c===`${b}.form`||c===`${b}.review`||c===`form.${b}`||c===b)return m}}const i=n,r=l(i.form_id)??l(i.formId);if(r){const c=e(r),m=o.find(b=>e(b.id)===c);if(m)return m}const s=(d=[i.type,i.event,i.kind,i.intent].map(l).find(c=>!!c))==null?void 0:d.toLowerCase();if(s)for(const c of o){if((_=c.event_types)!=null&&_.some(b=>e(b)===s))return c;const m=e(c.id);if(s===m||s===`${m}_form`||s===`${m}-form`||s===`${m.replace(/-/g,"_")}_form`)return c}return null}function U(t,n){if(!t||typeof t!="object")return null;const o=t,e=["draft","form","payload","data","fields","values"].map(r=>o[r]).find(r=>r&&typeof r=="object"),i={};for(const r of x(n)){if(!r.name)continue;const s=l(e==null?void 0:e[r.name])??l(o[r.name]);s&&(i[r.name]=s)}return Object.keys(i).length>0?i:null}function Y(t){const n={};for(const o of x(t))o.name&&(n[o.name]=o.default_value??"");return n}function B(t,n){if(!n)return t;const o={...t};for(const[e,i]of Object.entries(n))typeof i=="string"&&i.trim()&&(o[e]=i.trim());return o}function W(t,n){const o=x(t).map(e=>`${e.label.toLowerCase()}: ${(n[e.name]??"").trim()}`).filter(e=>!e.endsWith(": "));return`I have confirmed and submitted the "${t.title}" form: ${o.join("; ")}.`}function X(t){var n;return(n=t.steps)!=null&&n.length?t.steps.flatMap(o=>o.fields):t.fields}async function V({definition:t,values:n,apiUrl:o,slug:e,sessionId:i,apiKey:r,fetch:s}){const f=o.replace(/\/+$/,"");let d,_,c,m=!1;if(t.submit_url)d=/^https?:\/\//i.test(t.submit_url)?t.submit_url:`${f}/${t.submit_url.replace(/^\/+/,"")}`,c=t.submit_method??"POST",_=n;else{m=!0,d=`${f}/api/agents/${e}/form-responses/`,c="POST";const P=X(t).filter(h=>h.name&&h.type!=="display").map(h=>({name:h.name,label:h.label,value:n[h.name]??""}));_={form_id:t.id,form_data:P,...i?{session_id:i}:{}}}const b={"Content-Type":"application/json"};m&&(r!=null&&r.trim())&&(b["x-api-key"]=r.trim());const A=await(s??(typeof globalThis.fetch=="function"?globalThis.fetch.bind(globalThis):(()=>{throw new Error("[voice-agent] No fetch available; pass config.fetch.")})()))(d,{method:c,headers:b,body:JSON.stringify(_)}),g=await A.json().catch(()=>null);if(!A.ok)throw new Error((g==null?void 0:g.error)||(g==null?void 0:g.detail)||`We couldn't send your request right now (HTTP ${A.status}).`);return g}function $(t){if(!Array.isArray(t))return[];const n=[];for(const o of t){if(!o||typeof o!="object")continue;const e=o,i=l(e.id);if(!i)continue;const r=H(e.steps),s=I(e.fields);!r.length&&!s.length||n.push({id:i,title:l(e.title)??i,subtitle:l(e.subtitle),fields:s,steps:r.length?r:void 0,layout:G(e.layout),disabled:e.disabled===!0?!0:void 0,submit_url:l(e.submit_url)??null,submit_method:y(["POST","PUT","PATCH"],e.submit_method),submit_label:l(e.submit_label),success_message:l(e.success_message),topics:O(e.topics),event_types:O(e.event_types),confirmation_topic:l(e.confirmation_topic),confirmation_type:l(e.confirmation_type)})}return n}function I(t){if(!Array.isArray(t))return[];const n=[];for(const o of t){if(!o||typeof o!="object")continue;const e=o,i=y(["text","email","tel","textarea","select","number","date","time","checkbox","radio","display"],e.type)??"text",r=l(e.name);i!=="display"&&!r||n.push({name:r,label:l(e.label)??r??"",type:i,placeholder:l(e.placeholder),required:e.required===!0,options:J(e.options),rows:typeof e.rows=="number"&&Number.isFinite(e.rows)&&e.rows>0?e.rows:void 0,default_value:l(e.default_value),help_text:l(e.help_text),pattern:l(e.pattern),min:N(e.min),max:N(e.max),width:y(["full","half"],e.width)})}return n}function H(t){if(!Array.isArray(t))return[];const n=[];for(const[o,e]of t.entries()){if(!e||typeof e!="object")continue;const i=e,r=I(i.fields);r.length&&n.push({id:l(i.id)??`step-${o+1}`,title:l(i.title),subtitle:l(i.subtitle),fields:r,next_label:l(i.next_label),back_label:l(i.back_label)})}return n}function G(t){if(!t||typeof t!="object")return;const n=t,o={},e=y(["stack","grid"],n.field_layout);e&&(o.field_layout=e);const i=y(["comfortable","compact"],n.density);i&&(o.density=i);const r=y(["top","inline"],n.label_position);return r&&(o.label_position=r),Object.keys(o).length?o:void 0}function J(t){if(!Array.isArray(t))return;const n=[];for(const o of t){if(typeof o=="string"){const e=o.trim();e&&n.push(e);continue}if(o&&typeof o=="object"){const e=o,i=l(e.value);if(!i)continue;const r=l(e.label);n.push(r?{value:i,label:r}:{value:i})}}return n.length?n:void 0}function N(t){if(typeof t=="number"&&Number.isFinite(t))return t;if(typeof t=="string"){const n=Number(t.trim());if(Number.isFinite(n))return n}}function l(t){if(typeof t!="string")return;const n=t.trim();return n||void 0}function O(t){if(!Array.isArray(t))return;const n=t.map(l).filter(o=>!!o);return n.length?n:void 0}function y(t,n){return typeof n=="string"&&t.includes(n)?n:void 0}const a={name:"Voice Assistant",subtitle:"Tap below to start a voice conversation.",logo_url:"",fab_label:"",fab_sublabel:"",start_button_text:"Start Call",powered_by_text:"Powered by Oshara.ai",powered_by_url:"",theme:{primary_color:"#6366F1",accent_color:"#22D3EE",background_color:"#FFFFFF",text_color:"#0F172A",user_bubble_color:"#6366F1",user_bubble_text_color:"#FFFFFF",agent_bubble_color:"#F1F5F9",agent_bubble_text_color:"#0F172A"},dimensions:{fab_size:64,panel_width:380,panel_height:620,border_radius:24},layout:{position:"bottom-right",font_family:"Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"},labels:{idle:"Idle",connecting:"Connecting…",listening:"Listening",speaking:"Speaking…",thinking:"Thinking…",muted:"Microphone muted",call_ended:"Call ended",transcript_placeholder:"Your live transcript will appear here",language_label:"Language"},languages:[{code:"en",label:"English",native_label:"English"},{code:"ne",label:"Nepali",native_label:"नेपाली"}],default_language:"en",max_call_seconds:0,terms_url:"",terms_label:"Terms & Conditions",consent_text:"By starting the call, you agree to our",forms:k,show_audio_settings:!0},Q=["bottom-right","bottom-left","top-right","top-left"];function S(t){if(!t||typeof t!="object")return a;const n=F(t),o=F(n.theme),e=F(n.dimensions),i=F(n.layout),r=F(n.labels);return{name:u(n.name)??a.name,subtitle:p(n.subtitle,a.subtitle),logo_url:p(n.logo_url,a.logo_url),fab_label:p(n.fab_label,a.fab_label),fab_sublabel:p(n.fab_sublabel,a.fab_sublabel),start_button_text:u(n.start_button_text)??a.start_button_text,powered_by_text:p(n.powered_by_text,a.powered_by_text),powered_by_url:p(n.powered_by_url,a.powered_by_url),theme:{primary_color:u(o.primary_color)??a.theme.primary_color,accent_color:u(o.accent_color)??a.theme.accent_color,background_color:u(o.background_color)??a.theme.background_color,text_color:u(o.text_color)??a.theme.text_color,user_bubble_color:u(o.user_bubble_color)??a.theme.user_bubble_color,user_bubble_text_color:u(o.user_bubble_text_color)??a.theme.user_bubble_text_color,agent_bubble_color:u(o.agent_bubble_color)??a.theme.agent_bubble_color,agent_bubble_text_color:u(o.agent_bubble_text_color)??a.theme.agent_bubble_text_color},dimensions:{fab_size:w(e.fab_size)??a.dimensions.fab_size,panel_width:w(e.panel_width)??a.dimensions.panel_width,panel_height:w(e.panel_height)??a.dimensions.panel_height,border_radius:w(e.border_radius)??a.dimensions.border_radius},layout:{position:re(Q,i.position)??a.layout.position,font_family:p(i.font_family,a.layout.font_family)},labels:{idle:u(r.idle)??a.labels.idle,connecting:u(r.connecting)??a.labels.connecting,listening:u(r.listening)??a.labels.listening,speaking:u(r.speaking)??a.labels.speaking,thinking:u(r.thinking)??a.labels.thinking,muted:u(r.muted)??a.labels.muted,call_ended:u(r.call_ended)??a.labels.call_ended,transcript_placeholder:p(r.transcript_placeholder,a.labels.transcript_placeholder),language_label:u(r.language_label)??a.labels.language_label},languages:K(n.languages),default_language:u(n.default_language)??a.default_language,max_call_seconds:oe(n.max_call_seconds)??a.max_call_seconds,terms_url:p(n.terms_url,a.terms_url),terms_label:u(n.terms_label)??a.terms_label,consent_text:p(n.consent_text,a.consent_text),forms:Z(n.forms),show_audio_settings:typeof n.show_audio_settings=="boolean"?n.show_audio_settings:a.show_audio_settings}}function Z(t){const n=$(t),o=new Map;for(const e of k)o.set(e.id,e);for(const e of n)o.set(e.id,e);return Array.from(o.values())}function K(t){if(!Array.isArray(t))return a.languages;const n=new Set,o=[];for(const e of t){if(!v(e))continue;const i=u(e.code);if(!i||n.has(i))continue;const r=u(e.label)??i.toUpperCase(),s=u(e.native_label)??r;n.add(i),o.push({code:i,label:r,native_label:s})}return o.length?o:a.languages}async function ee(t){var i;const n=t.appearanceUrl||`${t.apiUrl.replace(/\/$/,"")}/api/agents/${encodeURIComponent(t.slug)}/appearance/`,o={Accept:"application/json"};(i=t.apiKey)!=null&&i.trim()&&(o["x-api-key"]=t.apiKey.trim());const e=t.fetch??(typeof globalThis.fetch=="function"?globalThis.fetch.bind(globalThis):null);if(!e)return console.warn("[voice-agent] no fetch available; using default appearance"),a;console.info("[voice-agent] fetching appearance from",n);try{const r=await e(n,{method:"GET",headers:o});if(!r.ok)return console.warn(`[voice-agent] appearance fetch returned ${r.status}; using defaults`),a;const s=await r.json();console.info("[voice-agent] appearance response",s);const f=te(s),d=S(f);return console.info("[voice-agent] applied appearance",d),d}catch(r){return console.warn("[voice-agent] appearance fetch failed; using defaults:",r),a}}function te(t){if(!v(t))return null;const n=t,o=[n.data,n.appearance,n.widget_appearance,n];for(const e of o)if(v(e))return e;return null}function v(t){return t!==null&&typeof t=="object"&&!Array.isArray(t)}function F(t){return v(t)?t:{}}function u(t){if(typeof t!="string")return;const n=t.trim();return n||void 0}function p(t,n){return typeof t=="string"?t:n}function ne(t){return typeof t=="number"&&Number.isFinite(t)&&t>0?t:void 0}function oe(t){if(typeof t=="number"&&Number.isFinite(t)&&t>=0)return t;if(typeof t=="string"){const n=t.trim();if(!n)return;const o=Number(n);if(Number.isFinite(o)&&o>=0)return o}}function w(t){const n=ne(t);if(n!==void 0)return n;if(typeof t!="string")return;const o=t.trim().match(/^(\d+(?:\.\d+)?)px$/i);if(!o)return;const e=Number(o[1]);return Number.isFinite(e)&&e>0?e:void 0}function re(t,n){return typeof n=="string"&&t.includes(n)?n:void 0}exports.DEFAULT_APPEARANCE=a;exports.DEFAULT_FORM_DEFINITIONS=k;exports.buildFieldSchema=C;exports.buildSubmissionText=W;exports.collectInputFields=x;exports.extractFormDraft=U;exports.fetchAppearance=ee;exports.fieldsForStep=L;exports.handleFormAction=D;exports.initialFormValues=Y;exports.matchForm=R;exports.mergeAppearance=S;exports.mergeFormDraft=B;exports.normalizeFormDefinitions=$;exports.submitForm=V;exports.totalSteps=j;exports.validateFields=M;
2
+ //# sourceMappingURL=appearance-CNWT8x1G.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appearance-CNWT8x1G.cjs","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","toSchema","f","step","entry","_a","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","normalize","t","form","id","candidate","explicitId","match","_b","v","_c","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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"}