@heybello/bello-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.
package/dist/react.cjs ADDED
@@ -0,0 +1,807 @@
1
+ "use client";
2
+ const import_meta = { env: {} };
3
+ 'use strict';
4
+
5
+ var React = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+ var client = require('react-dom/client');
8
+ var framerMotion = require('framer-motion');
9
+ var componentsReact = require('@livekit/components-react');
10
+ var reactAiOrb = require('react-ai-orb');
11
+ var lucideReact = require('lucide-react');
12
+ var livekitClient = require('livekit-client');
13
+
14
+ function _interopNamespaceDefault(e) {
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
32
+
33
+ function ensureContainer(id = "bello-widget-root") {
34
+ let el = document.getElementById(id);
35
+ if (!el) {
36
+ el = document.createElement("div");
37
+ el.id = id;
38
+ el.style.position = "relative";
39
+ el.style.zIndex = "2147483646";
40
+ document.body.appendChild(el);
41
+ }
42
+ return el;
43
+ }
44
+ function createShadowHost(container) {
45
+ let root = container._shadowRoot;
46
+ if (!root) {
47
+ root = container.attachShadow({ mode: "open" });
48
+ container._shadowRoot = root;
49
+ }
50
+ return root;
51
+ }
52
+
53
+ function normalizeVars(v) {
54
+ if (!v || typeof v !== "object") return void 0;
55
+ const out = {};
56
+ for (const [k, val] of Object.entries(v)) {
57
+ if (val == null) continue;
58
+ const key = k.startsWith("--") ? k : `--${k}`;
59
+ out[key] = String(val);
60
+ }
61
+ return Object.keys(out).length ? out : void 0;
62
+ }
63
+ async function fetchPublicConfig(opts) {
64
+ const base = opts.apiBaseUrl ?? "";
65
+ const res = await fetch(
66
+ `${base}/api/projects/${encodeURIComponent(opts.projectId)}`,
67
+ {
68
+ method: "GET",
69
+ mode: "cors",
70
+ credentials: "omit",
71
+ headers: { Accept: "application/json" }
72
+ }
73
+ );
74
+ if (!res.ok) throw new Error(`Config fetch failed: ${res.status}`);
75
+ const project = await res.json();
76
+ const wc = project?.project?.widget_config ?? project?.widget_config ?? {};
77
+ const themeVars = normalizeVars(wc.theme_vars) || normalizeVars(wc.themeVars) || normalizeVars(wc.css_vars) || void 0;
78
+ return {
79
+ widgetTitle: wc.widget_title ?? opts.widgetTitle ?? "Chat with AI",
80
+ widgetButtonTitle: wc.widget_button_title ?? opts.widgetButtonTitle ?? "Start chat",
81
+ orbStyle: wc.orb_style ?? opts.orbStyle ?? "galaxy",
82
+ theme: wc.theme ?? opts.theme ?? "dark",
83
+ position: wc.position ?? opts.position ?? "bottom-right",
84
+ themeVars
85
+ };
86
+ }
87
+ async function fetchConnectionDetails(opts) {
88
+ const base = opts.apiBaseUrl ?? "";
89
+ const res = await fetch(
90
+ `${base}/api/livekit/token/${encodeURIComponent(opts.projectId)}`,
91
+ {
92
+ method: "POST",
93
+ mode: "cors",
94
+ credentials: "omit",
95
+ headers: { "Content-Type": "application/json" }
96
+ }
97
+ );
98
+ if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
99
+ const j = await res.json();
100
+ return {
101
+ serverUrl: j.serverUrl ?? j.livekit_url,
102
+ participantToken: j.participantToken ?? j.token
103
+ };
104
+ }
105
+
106
+ function useLiveKit(opts, enabled) {
107
+ const room = React.useMemo(() => new livekitClient.Room(), []);
108
+ const [error, setError] = React.useState(null);
109
+ const [connecting, setConnecting] = React.useState(false);
110
+ const [connected, setConnected] = React.useState(false);
111
+ React.useEffect(() => {
112
+ if (!enabled) return;
113
+ let cancelled = false;
114
+ const enableAudioContext = async () => {
115
+ try {
116
+ const Ctx = window.AudioContext || window.webkitAudioContext;
117
+ if (Ctx) {
118
+ const ctx = new Ctx();
119
+ if (ctx.state === "suspended") await ctx.resume();
120
+ }
121
+ } catch {
122
+ }
123
+ };
124
+ const onMediaDevicesError = (err) => {
125
+ if (!cancelled) setError(`${err.name}: ${err.message}`);
126
+ };
127
+ const onDisconnected = () => {
128
+ if (!cancelled) setConnected(false);
129
+ };
130
+ room.on(livekitClient.RoomEvent.MediaDevicesError, onMediaDevicesError);
131
+ room.on(livekitClient.RoomEvent.Disconnected, onDisconnected);
132
+ (async () => {
133
+ try {
134
+ setConnecting(true);
135
+ await enableAudioContext();
136
+ const { serverUrl, participantToken } = await fetchConnectionDetails(opts);
137
+ if (cancelled) return;
138
+ await Promise.all([
139
+ room.localParticipant.setMicrophoneEnabled(
140
+ true,
141
+ void 0,
142
+ { preConnectBuffer: true }
143
+ ),
144
+ room.connect(serverUrl, participantToken)
145
+ ]);
146
+ if (!cancelled) setConnected(true);
147
+ } catch (e) {
148
+ if (!cancelled) {
149
+ setError(`${e?.name ?? "Error"}: ${e?.message ?? "unknown"}`);
150
+ }
151
+ } finally {
152
+ if (!cancelled) setConnecting(false);
153
+ }
154
+ })();
155
+ return () => {
156
+ cancelled = true;
157
+ room.off(livekitClient.RoomEvent.MediaDevicesError, onMediaDevicesError);
158
+ room.off(livekitClient.RoomEvent.Disconnected, onDisconnected);
159
+ try {
160
+ room.disconnect();
161
+ } catch {
162
+ }
163
+ setConnected(false);
164
+ setConnecting(false);
165
+ };
166
+ }, [enabled, room, opts.apiBaseUrl, opts.projectId]);
167
+ return { room, error, setError, connecting, connected };
168
+ }
169
+
170
+ function ChatInput({
171
+ onSend,
172
+ disabled
173
+ }) {
174
+ const [message, setMessage] = React__namespace.useState("");
175
+ const inputRef = React__namespace.useRef(null);
176
+ const submit = (e) => {
177
+ e.preventDefault();
178
+ const txt = message.trim();
179
+ if (!txt) return;
180
+ onSend(txt);
181
+ setMessage("");
182
+ };
183
+ React__namespace.useEffect(() => {
184
+ if (!disabled) inputRef.current?.focus();
185
+ }, [disabled]);
186
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "chat-input-row", onSubmit: submit, children: [
187
+ /* @__PURE__ */ jsxRuntime.jsx(
188
+ "input",
189
+ {
190
+ ref: inputRef,
191
+ type: "text",
192
+ className: "chat-input",
193
+ value: message,
194
+ autoComplete: "off",
195
+ placeholder: disabled ? "Connecting\u2026" : "Type a message\u2026",
196
+ onChange: (e) => setMessage(e.target.value),
197
+ "aria-label": "Message"
198
+ }
199
+ ),
200
+ /* @__PURE__ */ jsxRuntime.jsx(
201
+ "button",
202
+ {
203
+ type: "submit",
204
+ className: "bello-trigger small",
205
+ disabled: disabled || message.trim().length === 0,
206
+ "aria-label": "Send",
207
+ children: "Send"
208
+ }
209
+ )
210
+ ] });
211
+ }
212
+
213
+ const stickySide = /* @__PURE__ */ new Map();
214
+ function getAttr(t, k) {
215
+ return t?.streamInfo?.attributes?.[k] ?? t?.attributes?.[k];
216
+ }
217
+ function getIdentity(t) {
218
+ return t?.participantInfo?.identity ?? t?.participant?.identity ?? t?.participantIdentity ?? t?.participantSid;
219
+ }
220
+ function getTimestamp(t) {
221
+ const ts = t?.streamInfo?.timestamp ?? t?.timestamp ?? t?.time;
222
+ return typeof ts === "number" ? ts : Date.now();
223
+ }
224
+ function extractText(t) {
225
+ return t?.text ?? t?.alternatives?.[0]?.text ?? t?.result?.text ?? "";
226
+ }
227
+ function isFinalUtterance(t) {
228
+ const f = getAttr(t, "lk.transcription_final");
229
+ if (typeof f === "string") return f.toLowerCase() === "true";
230
+ return Boolean(
231
+ t?.isFinal || t?.final || t?.result?.is_final || t?.result?.final || t?.alternatives?.[0]?.isFinal
232
+ );
233
+ }
234
+ function utteranceKeyOf(t) {
235
+ return getAttr(t, "lk.segment_id") || t?.streamInfo?.id || `${getIdentity(t) || "p"}:${getTimestamp(t)}`;
236
+ }
237
+ function rememberSide(key, v) {
238
+ stickySide.set(key, v);
239
+ return v;
240
+ }
241
+ function guessLocalByIdentity(id) {
242
+ if (!id) return void 0;
243
+ if (id.startsWith("user-")) return true;
244
+ if (id.startsWith("agent-")) return false;
245
+ return void 0;
246
+ }
247
+ function decideIsLocalOnce(key, t, room) {
248
+ if (stickySide.has(key)) return stickySide.get(key);
249
+ const id = getIdentity(t);
250
+ const byPrefix = guessLocalByIdentity(id);
251
+ if (typeof byPrefix === "boolean")
252
+ return rememberSide(key, byPrefix);
253
+ const p = t?.participant || {};
254
+ if (typeof p?.isLocal === "boolean")
255
+ return rememberSide(key, p.isLocal);
256
+ const lp = room?.localParticipant || {};
257
+ const localId = lp.identity ?? lp.sid;
258
+ if (localId && id)
259
+ return rememberSide(key, String(localId) === String(id));
260
+ return rememberSide(key, false);
261
+ }
262
+ function transcriptionToChatMessage(t, room) {
263
+ const anyT = t;
264
+ const key = utteranceKeyOf(anyT);
265
+ const ts = getTimestamp(anyT);
266
+ const text = extractText(anyT);
267
+ const identity = getIdentity(anyT);
268
+ const isLocal = decideIsLocalOnce(key, anyT, room);
269
+ return {
270
+ id: `tx-${key}`,
271
+ // one id per utterance (collapses partials into a single bubble)
272
+ from: {
273
+ // Compatible with ChatEntry expectations
274
+ isLocal,
275
+ name: isLocal ? "You" : "Agent",
276
+ identity: identity ?? (isLocal ? "local" : "agent")
277
+ },
278
+ message: text,
279
+ timestamp: ts
280
+ };
281
+ }
282
+
283
+ function useChatAndTranscription() {
284
+ const transcriptions = componentsReact.useTranscriptions();
285
+ const chat = componentsReact.useChat();
286
+ const room = componentsReact.useRoomContext();
287
+ const storeRef = React.useRef(/* @__PURE__ */ new Map());
288
+ const orderRef = React.useRef([]);
289
+ const [, force] = React.useState(0);
290
+ const last = transcriptions[transcriptions.length - 1];
291
+ const txSig = transcriptions.length === 0 ? "0" : `${transcriptions.length}:${utteranceKeyOf(
292
+ last
293
+ )}:${extractText(last)}:${isFinalUtterance(last) ? 1 : 0}`;
294
+ React.useEffect(() => {
295
+ if (!transcriptions?.length) return;
296
+ let changed = false;
297
+ for (const t of transcriptions) {
298
+ const anyT = t;
299
+ const k = `tx-${utteranceKeyOf(anyT)}`;
300
+ const next = transcriptionToChatMessage(t, room);
301
+ const txt = extractText(anyT);
302
+ const fin = isFinalUtterance(anyT);
303
+ const prev = storeRef.current.get(k);
304
+ if (!prev) {
305
+ storeRef.current.set(k, {
306
+ msg: next,
307
+ final: Boolean(fin),
308
+ lastText: txt
309
+ });
310
+ orderRef.current.push(k);
311
+ changed = true;
312
+ continue;
313
+ }
314
+ const needTextUpdate = txt && txt !== prev.lastText;
315
+ const becameFinal = !prev.final && fin;
316
+ if (needTextUpdate || becameFinal) {
317
+ storeRef.current.set(k, {
318
+ msg: {
319
+ ...prev.msg,
320
+ message: needTextUpdate ? txt : prev.msg.message,
321
+ timestamp: Math.max(
322
+ prev.msg.timestamp || 0,
323
+ next.timestamp || 0
324
+ ),
325
+ from: {
326
+ ...prev.msg.from,
327
+ isLocal: prev.msg.from?.isLocal
328
+ }
329
+ },
330
+ final: prev.final || Boolean(fin),
331
+ lastText: needTextUpdate ? txt : prev.lastText
332
+ });
333
+ changed = true;
334
+ }
335
+ }
336
+ if (changed) force((v) => v + 1);
337
+ }, [txSig, room]);
338
+ const messages = React.useMemo(() => {
339
+ const txMsgs = orderRef.current.map((k) => storeRef.current.get(k)?.msg).filter(Boolean);
340
+ const all = [...txMsgs, ...chat.chatMessages];
341
+ all.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
342
+ return all;
343
+ }, [chat.chatMessages, txSig]);
344
+ const revision = React.useMemo(() => txSig, [txSig]);
345
+ return { messages, send: chat.send, revision };
346
+ }
347
+
348
+ function ChatEntry({
349
+ entry
350
+ }) {
351
+ const room = componentsReact.useRoomContext();
352
+ const lp = room?.localParticipant || {};
353
+ const localId = lp.identity ?? lp.sid;
354
+ const from = entry.from || {};
355
+ const pid = from?.identity ?? from?.sid;
356
+ const isUser = from?.isLocal === true || localId && pid && String(localId) === String(pid) || typeof pid === "string" && pid.startsWith("user-");
357
+ const who = isUser ? "You" : from?.name || "Agent";
358
+ const time = typeof entry.timestamp === "number" ? new Date(entry.timestamp) : /* @__PURE__ */ new Date();
359
+ const timeStr = time.toLocaleTimeString(void 0, {
360
+ timeStyle: "short"
361
+ });
362
+ return /* @__PURE__ */ jsxRuntime.jsxs(
363
+ "li",
364
+ {
365
+ className: `chat-entry ${isUser ? "me" : "them"}`,
366
+ title: time.toString(),
367
+ children: [
368
+ !isUser && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chat-name", children: who }),
369
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `chat-bubble ${isUser ? "me" : "them"}`, children: [
370
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chat-text", children: entry.message }),
371
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chat-time", children: timeStr })
372
+ ] })
373
+ ]
374
+ }
375
+ );
376
+ }
377
+
378
+ function preset(style) {
379
+ switch (style) {
380
+ case "ocean-depths":
381
+ return reactAiOrb.oceanDepthsPreset;
382
+ case "caribbean":
383
+ return reactAiOrb.caribeanPreset;
384
+ case "cherry-blossom":
385
+ return reactAiOrb.cherryBlossomPreset;
386
+ case "emerald":
387
+ return reactAiOrb.emeraldPreset;
388
+ case "multi-color":
389
+ return reactAiOrb.multiColorPreset;
390
+ case "golden-glow":
391
+ return reactAiOrb.goldenGlowPreset;
392
+ case "volcanic":
393
+ return reactAiOrb.volcanicPreset;
394
+ case "galaxy":
395
+ default:
396
+ return reactAiOrb.galaxyPreset;
397
+ }
398
+ }
399
+ function BelloWidget$1({
400
+ opts,
401
+ theme,
402
+ onClose
403
+ }) {
404
+ const [open, setOpen] = React.useState(false);
405
+ const agentOn = opts.agentEnabled !== false;
406
+ const voiceOn = opts.voiceEnabled !== false;
407
+ const { room, error, setError, connecting, connected } = useLiveKit(
408
+ opts,
409
+ open && agentOn
410
+ );
411
+ React.useEffect(() => {
412
+ const handler = (e) => {
413
+ const detail = e.detail;
414
+ const t = detail?.type;
415
+ if (t === "open") setOpen(true);
416
+ if (t === "close") {
417
+ setOpen(false);
418
+ onClose();
419
+ }
420
+ };
421
+ window.addEventListener("bello:event", handler);
422
+ return () => window.removeEventListener("bello:event", handler);
423
+ }, [onClose]);
424
+ const cfg = React.useMemo(
425
+ () => ({
426
+ title: opts.widgetTitle ?? "Need support?",
427
+ cta: opts.widgetButtonTitle ?? "Chat with AI",
428
+ pos: opts.position ?? "bottom-right",
429
+ orb: opts.orbStyle ?? "galaxy"
430
+ }),
431
+ [opts]
432
+ );
433
+ const cls = theme === "dark" ? "theme-dark" : theme === "glass" ? "theme-glass" : "theme-light";
434
+ const collapse = () => {
435
+ setOpen(false);
436
+ onClose();
437
+ };
438
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cls, children: [
439
+ !open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bello-container bello-row", children: [
440
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3", children: /* @__PURE__ */ jsxRuntime.jsx(
441
+ reactAiOrb.Orb,
442
+ {
443
+ ...preset(cfg.orb),
444
+ size: 0.7,
445
+ animationSpeedBase: 1,
446
+ animationSpeedHue: 1,
447
+ mainOrbHueAnimation: true
448
+ }
449
+ ) }),
450
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bello-title-container", children: [
451
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bello-hero", children: cfg.title }),
452
+ /* @__PURE__ */ jsxRuntime.jsx(
453
+ "button",
454
+ {
455
+ className: "bello-trigger",
456
+ onClick: () => setOpen(true),
457
+ children: cfg.cta
458
+ }
459
+ )
460
+ ] })
461
+ ] }),
462
+ /* @__PURE__ */ jsxRuntime.jsx(
463
+ framerMotion.motion.div,
464
+ {
465
+ className: "bello-pop",
466
+ "data-pos": cfg.pos,
467
+ initial: false,
468
+ animate: {
469
+ opacity: open ? 1 : 0,
470
+ scale: open ? 1 : 0.98,
471
+ y: open ? 0 : 6,
472
+ pointerEvents: open ? "auto" : "none"
473
+ },
474
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bello-card bello-body", children: [
475
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bello-header", children: [
476
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bello-row", children: [
477
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pl-2", children: /* @__PURE__ */ jsxRuntime.jsx(
478
+ reactAiOrb.Orb,
479
+ {
480
+ ...preset(cfg.orb),
481
+ size: 0.5,
482
+ animationSpeedBase: 1,
483
+ animationSpeedHue: 1,
484
+ mainOrbHueAnimation: true
485
+ }
486
+ ) }),
487
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
488
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bello-title", children: cfg.title }),
489
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bello-subtitle", children: agentOn ? "Voice & text assistant" : "UI preview (agent off)" })
490
+ ] })
491
+ ] }),
492
+ /* @__PURE__ */ jsxRuntime.jsxs(
493
+ "button",
494
+ {
495
+ className: "bello-trigger-danger small",
496
+ onClick: collapse,
497
+ "aria-label": "End session",
498
+ title: "End",
499
+ children: [
500
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Phone, { size: 14 }),
501
+ "End"
502
+ ]
503
+ }
504
+ )
505
+ ] }),
506
+ error ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bello-main", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bello-error", children: [
507
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bello-error-title", children: "Error" }),
508
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bello-error-text", children: error }),
509
+ /* @__PURE__ */ jsxRuntime.jsx(
510
+ "button",
511
+ {
512
+ className: "bello-trigger small mt",
513
+ onClick: () => setError(null),
514
+ children: "Dismiss"
515
+ }
516
+ )
517
+ ] }) }) : agentOn ? /* @__PURE__ */ jsxRuntime.jsxs(componentsReact.RoomContext.Provider, { value: room, children: [
518
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bello-main", children: [
519
+ voiceOn && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
520
+ /* @__PURE__ */ jsxRuntime.jsx(componentsReact.RoomAudioRenderer, {}),
521
+ /* @__PURE__ */ jsxRuntime.jsx(componentsReact.StartAudio, { label: "Start Audio" }),
522
+ /* @__PURE__ */ jsxRuntime.jsx(AgentAudioEnsurePlay, {})
523
+ ] }),
524
+ /* @__PURE__ */ jsxRuntime.jsx(
525
+ TopStatus,
526
+ {
527
+ connecting,
528
+ connected,
529
+ voiceOn
530
+ }
531
+ ),
532
+ /* @__PURE__ */ jsxRuntime.jsx(ChatRegion, {})
533
+ ] }),
534
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bello-footer", children: /* @__PURE__ */ jsxRuntime.jsx(FooterInput, { connected }) })
535
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
536
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bello-main", children: [
537
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "status", children: "UI preview \xB7 Agent disabled" }) }),
538
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chat-wrap", children: /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "chat-list", "aria-live": "polite", children: /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "chat-entry them", children: [
539
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chat-name", children: "Agent" }),
540
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "chat-bubble them", children: [
541
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chat-text", children: "Hi! This is UI-only mode." }),
542
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "chat-time", children: "\u2014" })
543
+ ] })
544
+ ] }) }) })
545
+ ] }),
546
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bello-footer", children: /* @__PURE__ */ jsxRuntime.jsx(ChatInput, { disabled: true, onSend: () => {
547
+ } }) })
548
+ ] })
549
+ ] })
550
+ }
551
+ ),
552
+ open && /* @__PURE__ */ jsxRuntime.jsx(
553
+ "button",
554
+ {
555
+ className: "bello-fab",
556
+ onClick: collapse,
557
+ "aria-label": "Collapse widget",
558
+ title: "Collapse",
559
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { size: 20 })
560
+ }
561
+ )
562
+ ] });
563
+ }
564
+ function TopStatus({
565
+ connecting,
566
+ connected,
567
+ voiceOn = true
568
+ }) {
569
+ const { state: agentState, audioTrack } = componentsReact.useVoiceAssistant();
570
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "center", children: [
571
+ voiceOn && /* @__PURE__ */ jsxRuntime.jsx(
572
+ componentsReact.BarVisualizer,
573
+ {
574
+ barCount: 5,
575
+ options: { minHeight: 6 },
576
+ className: "bars",
577
+ trackRef: audioTrack,
578
+ state: agentState,
579
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bar" })
580
+ }
581
+ ),
582
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "status", children: [
583
+ connecting && "Connecting\u2026",
584
+ !connecting && connected && (agentState === "speaking" ? "Agent speaking\u2026" : agentState === "listening" ? "Agent listening\u2026" : agentState === "thinking" ? "Agent thinking\u2026" : "Connected"),
585
+ !connecting && !connected && "Ready"
586
+ ] })
587
+ ] });
588
+ }
589
+ function AgentAudioEnsurePlay() {
590
+ const { audioTrack } = componentsReact.useVoiceAssistant();
591
+ React.useEffect(() => {
592
+ if (!audioTrack) return;
593
+ const el = audioTrack?.attachedElements?.[0];
594
+ const kick = (a) => {
595
+ try {
596
+ a.muted = false;
597
+ a.playsInline = true;
598
+ const p = a.play();
599
+ if (p && typeof p.then === "function") p.catch(() => {
600
+ });
601
+ } catch {
602
+ }
603
+ };
604
+ if (el) {
605
+ kick(el);
606
+ return;
607
+ }
608
+ try {
609
+ const audio = new Audio();
610
+ audio.autoplay = true;
611
+ audio.playsInline = true;
612
+ audio.muted = false;
613
+ if (typeof audioTrack.attach === "function") {
614
+ audioTrack.attach(audio);
615
+ } else if (audioTrack.mediaStream) {
616
+ audio.srcObject = audioTrack.mediaStream;
617
+ }
618
+ kick(audio);
619
+ } catch {
620
+ }
621
+ }, [audioTrack]);
622
+ return null;
623
+ }
624
+ function ChatRegion() {
625
+ const { messages, revision } = useChatAndTranscription();
626
+ const listRef = React.useRef(null);
627
+ const endRef = React.useRef(null);
628
+ const stickRef = React.useRef(true);
629
+ React.useEffect(() => {
630
+ const listEl = listRef.current;
631
+ if (!listEl) return;
632
+ const scroller = listEl.closest(
633
+ ".chat-wrap, .bello-main, .scroll-container"
634
+ ) || listEl.parentElement || listEl;
635
+ const onScroll = () => {
636
+ const nearBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < 80;
637
+ stickRef.current = nearBottom;
638
+ };
639
+ scroller.addEventListener("scroll", onScroll, { passive: true });
640
+ onScroll();
641
+ return () => scroller.removeEventListener("scroll", onScroll);
642
+ }, []);
643
+ React.useLayoutEffect(() => {
644
+ if (!stickRef.current) return;
645
+ requestAnimationFrame(() => {
646
+ endRef.current?.scrollIntoView({
647
+ block: "end",
648
+ inline: "nearest",
649
+ behavior: "auto"
650
+ });
651
+ });
652
+ }, [
653
+ revision,
654
+ messages.length,
655
+ messages[messages.length - 1]?.id,
656
+ messages[messages.length - 1]?.message
657
+ ]);
658
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "chat-wrap", children: /* @__PURE__ */ jsxRuntime.jsxs("ol", { ref: listRef, className: "chat-list", "aria-live": "polite", children: [
659
+ messages.map((m) => /* @__PURE__ */ jsxRuntime.jsx(ChatEntry, { entry: m }, m.id)),
660
+ /* @__PURE__ */ jsxRuntime.jsx("li", { "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: endRef }) })
661
+ ] }) });
662
+ }
663
+ function FooterInput({ connected }) {
664
+ const { send } = useChatAndTranscription();
665
+ return /* @__PURE__ */ jsxRuntime.jsx(
666
+ ChatInput,
667
+ {
668
+ disabled: !connected,
669
+ onSend: (txt) => {
670
+ const t = txt.trim();
671
+ if (!t) return;
672
+ send(t);
673
+ }
674
+ }
675
+ );
676
+ }
677
+
678
+ var cssText = ":host {\n all: initial;\n font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI,\n Roboto, Arial, Noto Sans, 'Apple Color Emoji', 'Segoe UI Emoji';\n\n /* defaults (can be overridden via themeVars) */\n --bello-bg: #111;\n --bello-fg: #fff;\n --bello-surface: #111;\n --bello-border: rgba(255, 255, 255, 0.12);\n --bello-cta-bg: #fff;\n --bello-cta-fg: #111;\n --bello-subtle: rgba(255, 255, 255, 0.72);\n --bello-danger: #ff6b6b;\n --bello-shadow: 0 10px 24px rgba(0, 0, 0, 0.18);\n\n --bello-radius: 12px;\n --bello-radius-pill: 999px;\n --bello-gap: 12px;\n --bello-pop-offset: 50px;\n\n --bello-width: 420px;\n --bello-height: 520px;\n --bello-fab-size-width: 80px;\n --bello-fab-size-height: 52px;\n --bello-fab-gap: 12px;\n}\n\n/* Themes only set alternate defaults; API variables still win */\n.theme-light :host,\n.theme-light {\n --bello-bg: #fff;\n --bello-fg: #111;\n --bello-surface: #fff;\n --bello-border: rgba(0, 0, 0, 0.08);\n --bello-cta-bg: #111;\n --bello-cta-fg: #fff;\n --bello-subtle: rgba(17, 17, 17, 0.72);\n}\n.theme-glass :host,\n.theme-glass {\n --bello-surface: rgba(255, 255, 255, 0.1);\n --bello-fg: #111;\n --bello-border: rgba(255, 255, 255, 0.2);\n --bello-cta-bg: #111;\n --bello-cta-fg: #fff;\n backdrop-filter: blur(10px);\n}\n\n/* containers */\n.bello-container {\n border-radius: var(--bello-radius);\n border: 1px solid var(--bello-border);\n box-shadow: var(--bello-shadow);\n padding: var(--bello-gap);\n background: var(--bello-surface);\n color: var(--bello-fg);\n}\n\n.bello-title-container {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.bello-card {\n border-radius: var(--bello-radius);\n border: 1px solid var(--bello-border);\n background: var(--bello-surface);\n color: var(--bello-fg);\n}\n\n/* buttons */\n.bello-trigger {\n border-radius: var(--bello-radius-pill);\n padding: 10px 18px;\n font-weight: 600;\n border: none;\n cursor: pointer;\n background: var(--bello-cta-bg);\n color: var(--bello-cta-fg);\n}\n.bello-trigger.small {\n padding: 8px 12px;\n font-size: 12px;\n}\n.bello-trigger:hover {\n opacity: 0.9;\n}\n\n.bello-trigger-danger {\n display: flex;\n align-items: center;\n gap: 6px;\n border-radius: var(--bello-radius-pill);\n padding: 10px 18px;\n font-weight: 600;\n border: none;\n cursor: pointer;\n background: var(--bello-danger);\n color: #fff;\n}\n.bello-trigger-danger.small {\n padding: 8px 12px;\n font-size: 12px;\n}\n.bello-trigger-danger:hover {\n opacity: 0.9;\n}\n\n/* header */\n.bello-header {\n display: flex;\n align-items: center;\n gap: var(--bello-gap);\n padding: var(--bello-gap);\n border-bottom: 1px solid var(--bello-border);\n justify-content: space-between;\n}\n.bello-title {\n font-size: 14px;\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.bello-subtitle {\n font-size: 12px;\n opacity: 0.8;\n}\n\n/* body/sizing */\n.bello-body {\n position: relative;\n display: flex;\n flex-direction: column;\n height: var(--bello-height);\n width: var(--bello-width);\n max-height: 80vh;\n}\n.bello-main {\n flex: 1;\n overflow: auto;\n padding: var(--bello-gap);\n}\n/* The FAB is a sibling of .bello-pop inside the fixed anchor. */\n.bello-fab {\n position: absolute; /* relative to the fixed anchor created by embed.tsx */\n z-index: 2; /* above the card, but below any global launchers */\n width: var(--bello-fab-size-width);\n height: var(--bello-fab-size-height);\n border-radius: 999px;\n border: 1px solid var(--bello-border);\n background: var(--bello-cta-bg);\n color: var(--bello-cta-fg);\n box-shadow: var(--bello-shadow);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n /* default hidden positioning; specific corners below set the final spot */\n top: auto;\n right: auto;\n bottom: auto;\n left: auto;\n}\n.bello-fab:hover {\n opacity: 0.9;\n}\n\n/* Anchor the FAB to the SAME corner as the widget, but OUTSIDE the card. */\n.bello-pop[data-pos='bottom-right'] ~ .bello-fab {\n right: calc(-1 * var(--bello-fab-gap));\n bottom: calc(-1 * var(--bello-fab-gap));\n}\n.bello-pop[data-pos='bottom-left'] ~ .bello-fab {\n left: calc(-1 * var(--bello-fab-gap));\n bottom: calc(-1 * var(--bello-fab-gap));\n}\n.bello-pop[data-pos='top-right'] ~ .bello-fab {\n right: calc(-1 * var(--bello-fab-gap));\n top: calc(-1 * var(--bello-fab-gap));\n}\n.bello-pop[data-pos='top-left'] ~ .bello-fab {\n left: calc(-1 * var(--bello-fab-gap));\n top: calc(-1 * var(--bello-fab-gap));\n}\n\n.bello-footer {\n height: 60px;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 0 var(--bello-gap);\n}\n\n.bello-footer .chat-input-row {\n width: 100%;\n}\n/* popover positioning */\n.bello-pop {\n position: absolute;\n transform-origin: bottom right;\n}\n.bello-pop[data-pos='bottom-right'] {\n bottom: var(--bello-pop-offset);\n right: 0;\n}\n.bello-pop[data-pos='bottom-left'] {\n bottom: var(--bello-pop-offset);\n left: 0;\n transform-origin: bottom left;\n}\n.bello-pop[data-pos='top-right'] {\n top: var(--bello-pop-offset);\n right: 0;\n transform-origin: top right;\n}\n.bello-pop[data-pos='top-left'] {\n top: var(--bello-pop-offset);\n left: 0;\n transform-origin: top left;\n}\n\n/* helpers */\n.bello-row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.bello-hero {\n font-weight: 700;\n}\n\n.bello-error {\n border: 1px solid var(--bello-danger);\n padding: var(--bello-gap);\n border-radius: 8px;\n}\n.bello-error-title {\n font-weight: 700;\n margin-bottom: 4px;\n}\n.bello-error-text {\n font-size: 12px;\n opacity: 0.9;\n}\n.center {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n gap: 8px;\n}\n.status {\n font-size: 12px;\n opacity: 0.8;\n}\n\n/* chat */\n.chat-wrap {\n display: flex;\n flex-direction: column;\n gap: 8px;\n height: 100%;\n}\n.chat-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.chat-entry {\n display: flex;\n flex-direction: column;\n}\n.chat-entry.me {\n align-items: flex-end;\n} /* YOU → right */\n.chat-entry.them {\n align-items: flex-start;\n} /* AGENT → left */\n\n.chat-entry .chat-name {\n font-size: 11px;\n opacity: 0.7;\n margin-bottom: 2px;\n}\n.chat-entry.me .chat-name {\n align-self: flex-end;\n}\n.chat-entry.them .chat-name {\n align-self: flex-start;\n}\n\n.chat-bubble {\n max-width: 75%;\n padding: 8px 10px;\n border-radius: 10px;\n border: 1px solid var(--bello-border);\n}\n.chat-bubble.me {\n background: rgba(0, 0, 0, 0.2);\n} /* you (right) */\n.chat-bubble.them {\n background: rgba(255, 255, 255, 0.06);\n} /* agent (left) */\n\n.chat-text {\n display: inline-block;\n}\n.chat-time {\n font-size: 10px;\n opacity: 0.6;\n margin-left: 8px;\n}\n\n.chat-input-row {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n.chat-input {\n flex: 1;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--bello-border);\n background: transparent;\n color: var(--bello-fg);\n}\n.chat-input::placeholder {\n color: var(--bello-subtle);\n}\n";
679
+
680
+ if (typeof window !== "undefined" && window.process == null) {
681
+ window.process = {
682
+ env: {
683
+ NODE_ENV: undefined?.MODE || "development"
684
+ }
685
+ };
686
+ }
687
+
688
+ let currentOpts = null;
689
+ let root = null;
690
+ let shadowRootRef = null;
691
+ let hostEl = null;
692
+ function mirrorHeadStylesInto(shadow) {
693
+ const seen = /* @__PURE__ */ new WeakSet();
694
+ const copy = (node) => {
695
+ if (!(node instanceof HTMLElement)) return;
696
+ if (node.tagName === "STYLE" && !seen.has(node)) {
697
+ const style = document.createElement("style");
698
+ style.textContent = node.textContent ?? "";
699
+ shadow.appendChild(style);
700
+ seen.add(node);
701
+ }
702
+ };
703
+ document.head.querySelectorAll("style").forEach((n) => copy(n));
704
+ const mo = new MutationObserver((mutations) => {
705
+ for (const m of mutations) m.addedNodes.forEach(copy);
706
+ });
707
+ mo.observe(document.head, { childList: true });
708
+ }
709
+ function applyThemeVars(vars) {
710
+ if (!hostEl || !vars) return;
711
+ for (const [k, v] of Object.entries(vars))
712
+ hostEl.style.setProperty(k, v);
713
+ }
714
+ async function mount(opts) {
715
+ const publicCfg = await fetchPublicConfig(opts);
716
+ currentOpts = {
717
+ agentEnabled: opts.agentEnabled ?? true,
718
+ voiceEnabled: opts.voiceEnabled ?? true,
719
+ ...opts,
720
+ theme: opts.theme ?? publicCfg.theme,
721
+ orbStyle: opts.orbStyle ?? publicCfg.orbStyle,
722
+ position: opts.position ?? publicCfg.position,
723
+ widgetTitle: opts.widgetTitle ?? publicCfg.widgetTitle,
724
+ widgetButtonTitle: opts.widgetButtonTitle ?? publicCfg.widgetButtonTitle,
725
+ themeVars: {
726
+ ...publicCfg.themeVars ?? {},
727
+ ...opts.themeVars ?? {}
728
+ }
729
+ };
730
+ const theme = currentOpts.theme ?? "dark";
731
+ const container = ensureContainer("bello-widget-root");
732
+ hostEl = container;
733
+ const shadow = createShadowHost(container);
734
+ shadowRootRef = shadow;
735
+ const style = document.createElement("style");
736
+ style.textContent = cssText;
737
+ shadow.appendChild(style);
738
+ mirrorHeadStylesInto(shadow);
739
+ applyThemeVars(currentOpts.themeVars);
740
+ const anchor = document.createElement("div");
741
+ anchor.style.position = "fixed";
742
+ anchor.style.zIndex = "2147483647";
743
+ const pos = currentOpts.position ?? "bottom-right";
744
+ const map = {
745
+ "bottom-right": { bottom: "16px", right: "16px" },
746
+ "bottom-left": { bottom: "16px", left: "16px" },
747
+ "top-right": { top: "16px", right: "16px" },
748
+ "top-left": { top: "16px", left: "16px" }
749
+ };
750
+ Object.assign(anchor.style, map[pos]);
751
+ shadow.appendChild(anchor);
752
+ root = client.createRoot(anchor);
753
+ root.render(
754
+ /* @__PURE__ */ jsxRuntime.jsx(
755
+ BelloWidget$1,
756
+ {
757
+ opts: currentOpts,
758
+ theme,
759
+ onClose: () => {
760
+ }
761
+ }
762
+ )
763
+ );
764
+ }
765
+ function rerender() {
766
+ if (!root || !shadowRootRef || !currentOpts) return;
767
+ applyThemeVars(currentOpts.themeVars);
768
+ root.render(
769
+ /* @__PURE__ */ jsxRuntime.jsx(
770
+ BelloWidget$1,
771
+ {
772
+ opts: currentOpts,
773
+ theme: currentOpts.theme ?? "dark",
774
+ onClose: () => {
775
+ }
776
+ }
777
+ )
778
+ );
779
+ }
780
+ async function initWidget(opts) {
781
+ if (typeof window === "undefined") return;
782
+ await mount(opts);
783
+ }
784
+ function updateWidget(opts) {
785
+ if (typeof window === "undefined") return;
786
+ currentOpts = {
787
+ ...currentOpts ?? {},
788
+ ...opts ?? {}
789
+ };
790
+ rerender();
791
+ }
792
+
793
+ function BelloWidget(props) {
794
+ const inited = React.useRef(false);
795
+ React.useEffect(() => {
796
+ if (!inited.current) {
797
+ initWidget(props);
798
+ inited.current = true;
799
+ } else {
800
+ updateWidget(props);
801
+ }
802
+ }, [props]);
803
+ return null;
804
+ }
805
+
806
+ exports.BelloWidget = BelloWidget;
807
+ //# sourceMappingURL=react.cjs.map