@wallavi/widget 1.12.7 → 1.13.1

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/index.mjs CHANGED
@@ -66,6 +66,83 @@ async function getFreshClerkToken() {
66
66
  }
67
67
  }
68
68
 
69
+ // src/lib/page-context.ts
70
+ function candidatesFrom(actions) {
71
+ const out = [];
72
+ for (const action of actions) {
73
+ const title = action.displayName ?? action.name;
74
+ const target = action.steps?.find((s) => s.action === "navigate")?.value;
75
+ if (!title || typeof target !== "string" || !target.startsWith("/")) {
76
+ continue;
77
+ }
78
+ const [path = "", search = ""] = target.split("?");
79
+ out.push({
80
+ title,
81
+ segments: path.split("/").filter(Boolean),
82
+ query: new URLSearchParams(search)
83
+ });
84
+ }
85
+ return out;
86
+ }
87
+ var PARAM = /^\[(\w+)\]$/;
88
+ var LOCALE = /^[a-z]{2}(?:-[a-z]{2,4})?$/i;
89
+ function offsetsFor(pathSegments, routeLength) {
90
+ const offsets = [];
91
+ if (pathSegments.length === routeLength) offsets.push(0);
92
+ if (pathSegments.length === routeLength + 1 && LOCALE.test(pathSegments[0] ?? "")) {
93
+ offsets.push(1);
94
+ }
95
+ return offsets;
96
+ }
97
+ function safeDecode(segment) {
98
+ try {
99
+ return decodeURIComponent(segment);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ function score(candidate, pathSegments, search) {
105
+ const { segments, query } = candidate;
106
+ if (segments.length === 0) return null;
107
+ for (const [key, value] of query) {
108
+ if (search.get(key) !== value) return null;
109
+ }
110
+ for (const offset of offsetsFor(pathSegments, segments.length)) {
111
+ const params = {};
112
+ let points = Array.from(query.keys()).length;
113
+ let matched = true;
114
+ for (let i = 0; i < segments.length && matched; i++) {
115
+ const pattern = segments[i];
116
+ const actual = safeDecode(pathSegments[offset + i]);
117
+ const param = PARAM.exec(pattern);
118
+ if (actual === null) matched = false;
119
+ else if (param) params[param[1]] = actual;
120
+ else if (pattern === actual) points += 2;
121
+ else matched = false;
122
+ }
123
+ if (matched) return { score: points, params };
124
+ }
125
+ return null;
126
+ }
127
+ function derivePageContext(location, actions) {
128
+ if (!actions?.length) return void 0;
129
+ const pathSegments = location.pathname.split("/").filter(Boolean);
130
+ const search = new URLSearchParams(location.search);
131
+ let best = null;
132
+ for (const candidate of candidatesFrom(actions)) {
133
+ const result = score(candidate, pathSegments, search);
134
+ if (result && (!best || result.score > best.score)) {
135
+ best = { title: candidate.title, ...result };
136
+ }
137
+ }
138
+ if (!best) return void 0;
139
+ return {
140
+ url: `${location.pathname}${location.search}`,
141
+ title: best.title,
142
+ ...Object.keys(best.params).length > 0 ? { params: best.params } : {}
143
+ };
144
+ }
145
+
69
146
  // src/lib/types.ts
70
147
  function getContrastColor(hex) {
71
148
  const clean = hex.replace("#", "");
@@ -79,7 +156,7 @@ function formatToolName(name) {
79
156
  return name.replace(/([A-Z])/g, " $1").replace(/_/g, " ").trim();
80
157
  }
81
158
 
82
- // src/core/stream-protocol.ts
159
+ // ../protocol/src/index.ts
83
160
  var STREAM_DELIMITER = "\u03B6\u236E";
84
161
  async function consumeStream(body, handler) {
85
162
  const reader = body.getReader();
@@ -95,9 +172,9 @@ async function consumeStream(body, handler) {
95
172
  if (!raw) continue;
96
173
  try {
97
174
  const parsed = JSON.parse(raw);
98
- const proto = parsed.data?.uiMessageProtocol;
99
- if (proto) {
175
+ if (parsed.data?.uiMessageProtocol) {
100
176
  eventCount++;
177
+ const proto = parsed.data.uiMessageProtocol;
101
178
  if (proto.type === "text-delta") {
102
179
  textAccumulator += proto.delta ?? "";
103
180
  }
@@ -247,12 +324,321 @@ function applyUiEventToMessages(prev, proto, msgId) {
247
324
  copy[idx] = msg;
248
325
  return copy;
249
326
  }
250
- var API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
327
+
328
+ // src/core/declarative-actions.ts
329
+ function sanitizeSelector(selector) {
330
+ if (!selector || typeof selector !== "string") return null;
331
+ const lower = selector.toLowerCase();
332
+ if (lower.includes("<") || lower.includes(">") || lower.includes("script") || lower.includes("iframe") || lower.includes("onload") || lower.includes("onerror") || lower.includes("javascript:")) {
333
+ return null;
334
+ }
335
+ const cleanRegex = /^[a-zA-Z0-9\s\.\-#_\[\]="':]+$/;
336
+ if (!cleanRegex.test(selector)) {
337
+ return null;
338
+ }
339
+ return selector;
340
+ }
341
+ function sanitizeEventName(name) {
342
+ if (!name || typeof name !== "string") return null;
343
+ const nameRegex = /^[a-zA-Z0-9\-_:]+$/;
344
+ if (!nameRegex.test(name)) return null;
345
+ const nativeEvents = /* @__PURE__ */ new Set([
346
+ "click",
347
+ "dblclick",
348
+ "mouseup",
349
+ "mousedown",
350
+ "mouseover",
351
+ "mouseout",
352
+ "submit",
353
+ "reset",
354
+ "change",
355
+ "select",
356
+ "keydown",
357
+ "keypress",
358
+ "keyup",
359
+ "load",
360
+ "unload",
361
+ "abort",
362
+ "error",
363
+ "resize",
364
+ "scroll",
365
+ "contextmenu"
366
+ ]);
367
+ if (nativeEvents.has(name.toLowerCase())) return null;
368
+ return name;
369
+ }
370
+ function sanitizeUrl(url) {
371
+ if (!url || typeof url !== "string") return null;
372
+ const trimmed = url.trim();
373
+ const lower = trimmed.toLowerCase();
374
+ if (trimmed.startsWith("//")) {
375
+ return null;
376
+ }
377
+ if (lower.startsWith("javascript:") || lower.startsWith("data:")) {
378
+ return null;
379
+ }
380
+ if (trimmed.startsWith("/")) {
381
+ return trimmed;
382
+ }
383
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
384
+ return trimmed;
385
+ }
386
+ return null;
387
+ }
388
+ async function waitForElement(selector, retries = 5, delayMs = 100) {
389
+ const cleanId = selector.startsWith("#") ? selector.substring(1) : selector;
390
+ for (let i = 0; i < retries; i++) {
391
+ try {
392
+ const el = document.querySelector(selector);
393
+ if (el) return el;
394
+ } catch (e) {
395
+ console.error(e);
396
+ }
397
+ try {
398
+ const byId = document.getElementById(cleanId);
399
+ if (byId) return byId;
400
+ } catch (e) {
401
+ console.error(e);
402
+ }
403
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[")) {
404
+ try {
405
+ const byPrependedId = document.querySelector(`#${selector}`);
406
+ if (byPrependedId) return byPrependedId;
407
+ } catch (e) {
408
+ console.error(e);
409
+ }
410
+ }
411
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
412
+ }
413
+ return null;
414
+ }
415
+ var HEX_COLOR_REGEX = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;
416
+ function sanitizeColor(color, fallback = "#6366f1") {
417
+ if (!color || typeof color !== "string") return fallback;
418
+ const trimmed = color.trim();
419
+ return HEX_COLOR_REGEX.test(trimmed) ? trimmed : fallback;
420
+ }
421
+ function injectCopilotStyles(color = "#6366f1") {
422
+ if (typeof document === "undefined") return;
423
+ const safeColor = sanitizeColor(color, "#6366f1");
424
+ let style = document.getElementById(
425
+ "wallavi-copilot-styles"
426
+ );
427
+ if (!style) {
428
+ style = document.createElement("style");
429
+ style.id = "wallavi-copilot-styles";
430
+ document.head.appendChild(style);
431
+ }
432
+ if (style.dataset.color === safeColor) return;
433
+ style.dataset.color = safeColor;
434
+ style.textContent = `
435
+ @keyframes wallavi-pulse-beacon {
436
+ 0% {
437
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7);
438
+ outline: 2px solid ${safeColor};
439
+ }
440
+ 70% {
441
+ box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
442
+ outline: 2px solid rgba(99, 102, 241, 0.3);
443
+ }
444
+ 100% {
445
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
446
+ outline: 2px solid transparent;
447
+ }
448
+ }
449
+ .wallavi-element-beacon {
450
+ animation: wallavi-pulse-beacon 1.8s ease-out infinite !important;
451
+ outline-offset: 3px !important;
452
+ border-radius: inherit;
453
+ transition: outline 0.2s ease, box-shadow 0.2s ease !important;
454
+ }
455
+ @keyframes wallavi-flash-touch-anim {
456
+ 0% {
457
+ outline: 3px solid ${safeColor};
458
+ outline-offset: 2px;
459
+ filter: brightness(1.08);
460
+ }
461
+ 100% {
462
+ outline: 2px solid transparent;
463
+ outline-offset: 6px;
464
+ filter: none;
465
+ }
466
+ }
467
+ .wallavi-flash-touch {
468
+ animation: wallavi-flash-touch-anim 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards !important;
469
+ }
470
+ `;
471
+ document.head.appendChild(style);
472
+ }
473
+ function flashElement(el, color) {
474
+ injectCopilotStyles(color);
475
+ el.classList.remove("wallavi-flash-touch");
476
+ void el.offsetWidth;
477
+ el.classList.add("wallavi-flash-touch");
478
+ setTimeout(() => {
479
+ el.classList.remove("wallavi-flash-touch");
480
+ }, 600);
481
+ }
482
+ function highlightElement(el, color, durationMs = 3e3) {
483
+ injectCopilotStyles(color);
484
+ el.classList.add("wallavi-element-beacon");
485
+ setTimeout(() => {
486
+ el.classList.remove("wallavi-element-beacon");
487
+ }, durationMs);
488
+ }
489
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
490
+ async function executeDeclarativeSteps(steps, onNavigate, copilotColor) {
491
+ if (typeof window === "undefined" || !Array.isArray(steps)) return;
492
+ const safeCopilotColor = sanitizeColor(copilotColor, "#6366f1");
493
+ for (const step of steps) {
494
+ try {
495
+ const { action } = step;
496
+ switch (action) {
497
+ case "scroll_to":
498
+ case "scroll_into_view": {
499
+ const selector = sanitizeSelector(step.selector);
500
+ if (!selector) break;
501
+ const el = await waitForElement(selector);
502
+ if (el) {
503
+ flashElement(el, safeCopilotColor);
504
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
505
+ }
506
+ break;
507
+ }
508
+ case "click": {
509
+ const selector = sanitizeSelector(step.selector);
510
+ if (!selector) break;
511
+ const el = await waitForElement(selector);
512
+ if (el) {
513
+ flashElement(el, safeCopilotColor);
514
+ await delay(80);
515
+ el.click();
516
+ }
517
+ break;
518
+ }
519
+ case "focus": {
520
+ const selector = sanitizeSelector(step.selector);
521
+ if (!selector) break;
522
+ const el = await waitForElement(selector);
523
+ if (el) {
524
+ flashElement(el, safeCopilotColor);
525
+ el.focus();
526
+ }
527
+ break;
528
+ }
529
+ case "highlight": {
530
+ const selector = sanitizeSelector(step.selector);
531
+ if (!selector) break;
532
+ const el = await waitForElement(selector);
533
+ if (el) {
534
+ highlightElement(
535
+ el,
536
+ sanitizeColor(step.color, safeCopilotColor),
537
+ step.durationMs ?? 3e3
538
+ );
539
+ }
540
+ break;
541
+ }
542
+ case "wait": {
543
+ const parsedVal = typeof step.value === "number" ? step.value : parseInt(String(step.value ?? "500"), 10);
544
+ const ms = step.delayMs ?? (Number.isNaN(parsedVal) ? 500 : parsedVal);
545
+ await delay(ms);
546
+ break;
547
+ }
548
+ case "add_class": {
549
+ const selector = sanitizeSelector(step.selector);
550
+ const value = step.value;
551
+ if (!selector || !value) break;
552
+ const el = await waitForElement(selector);
553
+ if (el) el.classList.add(...value.split(/\s+/).filter(Boolean));
554
+ break;
555
+ }
556
+ case "remove_class": {
557
+ const selector = sanitizeSelector(step.selector);
558
+ const value = step.value;
559
+ if (!selector || !value) break;
560
+ const el = await waitForElement(selector);
561
+ if (el) el.classList.remove(...value.split(/\s+/).filter(Boolean));
562
+ break;
563
+ }
564
+ case "fill_value": {
565
+ const selector = sanitizeSelector(step.selector);
566
+ const value = step.value;
567
+ if (!selector) break;
568
+ const el = await waitForElement(selector);
569
+ if (el && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
570
+ if (el instanceof HTMLInputElement) {
571
+ const type = el.type.toLowerCase();
572
+ if (type === "password" || type === "hidden") {
573
+ console.warn(
574
+ `[Wallavi Widget] Blocked fill_value action on sensitive field: ${type}`
575
+ );
576
+ break;
577
+ }
578
+ }
579
+ const isReadOnly = (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && el.readOnly;
580
+ if (el.disabled || isReadOnly) {
581
+ console.warn(
582
+ "[Wallavi Widget] Blocked fill_value action on disabled/readOnly element"
583
+ );
584
+ break;
585
+ }
586
+ flashElement(el, safeCopilotColor);
587
+ el.value = value ?? "";
588
+ el.dispatchEvent(new Event("input", { bubbles: true }));
589
+ el.dispatchEvent(new Event("change", { bubbles: true }));
590
+ }
591
+ break;
592
+ }
593
+ case "dispatch_event": {
594
+ const eventName = sanitizeEventName(step.value);
595
+ if (!eventName) break;
596
+ const detail = step.payload ?? {};
597
+ window.dispatchEvent(new CustomEvent(eventName, { detail }));
598
+ break;
599
+ }
600
+ case "navigate": {
601
+ const url = sanitizeUrl(step.value);
602
+ if (url) {
603
+ if (onNavigate) {
604
+ onNavigate(url);
605
+ } else {
606
+ window.location.href = url;
607
+ }
608
+ }
609
+ break;
610
+ }
611
+ default: {
612
+ const unknownAction = step.action;
613
+ console.warn(
614
+ `[Wallavi Widget] Unknown declarative action: ${unknownAction}`
615
+ );
616
+ }
617
+ }
618
+ await delay(120);
619
+ } catch (err) {
620
+ console.error(
621
+ "[Wallavi Widget] Failed to execute declarative step:",
622
+ step,
623
+ err
624
+ );
625
+ }
626
+ }
627
+ }
628
+
629
+ // src/lib/api-url.ts
630
+ var WALLAVI_PRODUCTION_API_URL = "https://wallavi-production.up.railway.app";
631
+ function resolveApiUrl(explicit) {
632
+ return explicit ?? process.env.NEXT_PUBLIC_API_URL ?? WALLAVI_PRODUCTION_API_URL;
633
+ }
634
+
635
+ // src/hooks/use-voice-call.ts
251
636
  function useVoiceCall({
252
637
  agentId,
253
638
  threadId,
254
639
  workspaceId,
255
- customBackend
640
+ customBackend,
641
+ apiUrl
256
642
  }) {
257
643
  const [active, setActive] = useState(false);
258
644
  const [token, setToken] = useState(null);
@@ -265,7 +651,7 @@ function useVoiceCall({
265
651
  setError(null);
266
652
  try {
267
653
  const isPrivate = Boolean(workspaceId);
268
- const url = isPrivate ? `${API_URL}/api/threads/${threadId}/livekit-token?agentId=${encodeURIComponent(agentId)}` : `${API_URL}/api/chat/livekit-token?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
654
+ const url = isPrivate ? `${resolveApiUrl(apiUrl)}/api/threads/${threadId}/livekit-token?agentId=${encodeURIComponent(agentId)}` : `${resolveApiUrl(apiUrl)}/api/chat/livekit-token?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
269
655
  const token2 = isPrivate ? await getFreshClerkToken() : null;
270
656
  const res = await fetch(url, {
271
657
  headers: {
@@ -281,7 +667,7 @@ function useVoiceCall({
281
667
  setError(err.message);
282
668
  }
283
669
  setLoading(false);
284
- }, [agentId, threadId, workspaceId, customBackend]);
670
+ }, [agentId, threadId, workspaceId, customBackend, apiUrl]);
285
671
  const stop = useCallback(() => {
286
672
  setActive(false);
287
673
  setToken(null);
@@ -298,13 +684,33 @@ function useVoiceCall({
298
684
  };
299
685
  }
300
686
 
687
+ // src/lib/user-headers.ts
688
+ var warnedHeadersFailure = false;
689
+ async function resolveUserHeaders(userContext) {
690
+ const source = userContext?.headers;
691
+ if (!source) return {};
692
+ if (typeof source !== "function") return source;
693
+ try {
694
+ return await source() ?? {};
695
+ } catch (err) {
696
+ if (!warnedHeadersFailure) {
697
+ warnedHeadersFailure = true;
698
+ console.warn(
699
+ "[wallavi-widget] userContext.headers threw; sending without host headers",
700
+ err
701
+ );
702
+ }
703
+ return {};
704
+ }
705
+ }
706
+
301
707
  // src/hooks/use-chat.ts
302
- var API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
303
708
  function newId() {
304
709
  return Math.random().toString(36).slice(2, 10);
305
710
  }
306
711
  function useChat({
307
712
  agentId,
713
+ apiUrl,
308
714
  workspaceId = "",
309
715
  envId,
310
716
  source = "playground",
@@ -312,13 +718,20 @@ function useChat({
312
718
  persist = false,
313
719
  onNavigate,
314
720
  playgroundOverrides,
315
- customBackend
721
+ customBackend,
722
+ copilotColor,
723
+ resolvePageContext
316
724
  }) {
725
+ const baseUrl = resolveApiUrl(apiUrl);
317
726
  const userId = userContext?.userId;
318
727
  const persistKey = persist ? userId ? `wallavi_${agentId}_${userId}` : `wallavi_${agentId}` : null;
319
728
  const onNavigateRef = useRef(onNavigate);
729
+ const resolvePageContextRef = useRef(resolvePageContext);
730
+ const copilotColorRef = useRef(copilotColor);
320
731
  useEffect(() => {
321
732
  onNavigateRef.current = onNavigate;
733
+ resolvePageContextRef.current = resolvePageContext;
734
+ copilotColorRef.current = copilotColor;
322
735
  });
323
736
  const [messages, setMessages] = useState(() => {
324
737
  if (!persistKey || typeof window === "undefined") return [];
@@ -412,7 +825,11 @@ function useChat({
412
825
  return;
413
826
  }
414
827
  if (proto.type === "client-action") {
415
- executeDeclarativeSteps(proto.steps, onNavigateRef.current);
828
+ executeDeclarativeSteps(
829
+ proto.steps,
830
+ onNavigateRef.current,
831
+ copilotColorRef.current
832
+ );
416
833
  return;
417
834
  }
418
835
  if (proto.type === "debug-trace") {
@@ -428,10 +845,18 @@ function useChat({
428
845
  );
429
846
  const fetchAndStream = useCallback(
430
847
  async (opts) => {
431
- const { input: userInput, msgId, extraMetadata, attachments } = opts;
848
+ const {
849
+ input: userInput,
850
+ msgId,
851
+ extraMetadata,
852
+ attachments,
853
+ pickerSelection
854
+ } = opts;
432
855
  const isPrivate = Boolean(workspaceId);
433
856
  const token = isPrivate ? await getFreshClerkToken() : null;
434
- const url = isPrivate ? `${API_URL2}/api/threads/${threadId}/stream` : `${API_URL2}/api/chat/stream`;
857
+ const hostHeaders = await resolveUserHeaders(userContext);
858
+ const pageContext = userContext?.pageContext ?? resolvePageContextRef.current?.();
859
+ const url = isPrivate ? `${baseUrl}/api/threads/${threadId}/stream` : `${baseUrl}/api/chat/stream`;
435
860
  const res = await fetch(url, {
436
861
  method: "POST",
437
862
  headers: {
@@ -448,14 +873,17 @@ function useChat({
448
873
  } : { threadId },
449
874
  source,
450
875
  ...attachments?.length ? { attachments } : {},
876
+ // Its own field: userMetadata keys starting with __ belong to the
877
+ // engine and the API rejects them.
878
+ ...pickerSelection ? { pickerSelection } : {},
451
879
  ...userContext?.userName ? { userName: userContext.userName } : {},
452
880
  ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
453
881
  userMetadata: {
454
882
  ...userContext?.metadata ?? {},
455
- ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
883
+ ...pageContext ? { pageContext } : {},
456
884
  headers: {
457
885
  ...token ? { Authorization: `Bearer ${token}` } : {},
458
- ...userContext?.headers ?? {},
886
+ ...hostHeaders,
459
887
  ...userContext?.metadata?.headers ?? {}
460
888
  },
461
889
  ...extraMetadata ?? {}
@@ -500,6 +928,7 @@ function useChat({
500
928
  }
501
929
  },
502
930
  [
931
+ baseUrl,
503
932
  agentId,
504
933
  workspaceId,
505
934
  envId,
@@ -514,7 +943,8 @@ function useChat({
514
943
  agentId,
515
944
  threadId,
516
945
  workspaceId,
517
- customBackend
946
+ customBackend,
947
+ apiUrl: baseUrl
518
948
  });
519
949
  useEffect(() => {
520
950
  if (customBackend || !persistKey) return;
@@ -563,7 +993,7 @@ function useChat({
563
993
  try {
564
994
  const isPrivate = Boolean(workspaceId);
565
995
  const token = isPrivate ? await getFreshClerkToken() : null;
566
- const url = isPrivate ? `${API_URL2}/api/threads/${threadId}/messages` : `${API_URL2}/api/chat/messages?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
996
+ const url = isPrivate ? `${baseUrl}/api/threads/${threadId}/messages` : `${baseUrl}/api/chat/messages?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
567
997
  const res = await fetch(url, {
568
998
  headers: {
569
999
  ...token ? { Authorization: `Bearer ${token}` } : {}
@@ -627,7 +1057,15 @@ function useChat({
627
1057
  }
628
1058
  }
629
1059
  }
630
- }, [threadId, agentId, workspaceId, envId, persistKey, fetchAndStream]);
1060
+ }, [
1061
+ baseUrl,
1062
+ threadId,
1063
+ agentId,
1064
+ workspaceId,
1065
+ envId,
1066
+ persistKey,
1067
+ fetchAndStream
1068
+ ]);
631
1069
  const reset = useCallback(() => {
632
1070
  setMessages([]);
633
1071
  setInput("");
@@ -750,9 +1188,7 @@ function useChat({
750
1188
  await fetchAndStream({
751
1189
  input: label,
752
1190
  msgId: assistantMsgId,
753
- extraMetadata: {
754
- __pickerSelection: { pickerId, paramName, value, label }
755
- }
1191
+ pickerSelection: { pickerId, paramName, value, label }
756
1192
  });
757
1193
  } catch {
758
1194
  setMessages((prev) => {
@@ -811,106 +1247,6 @@ function useChat({
811
1247
  setSelectedContext
812
1248
  };
813
1249
  }
814
- async function waitForElement(selector, retries = 5, delayMs = 100) {
815
- const cleanId = selector.startsWith("#") ? selector.substring(1) : selector;
816
- for (let i = 0; i < retries; i++) {
817
- try {
818
- const el = document.querySelector(selector);
819
- if (el) return el;
820
- } catch (e) {
821
- }
822
- try {
823
- const byId = document.getElementById(cleanId);
824
- if (byId) return byId;
825
- } catch (e) {
826
- }
827
- if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[")) {
828
- try {
829
- const byPrependedId = document.querySelector(`#${selector}`);
830
- if (byPrependedId) return byPrependedId;
831
- } catch (e) {
832
- }
833
- }
834
- await new Promise((resolve) => setTimeout(resolve, delayMs));
835
- }
836
- return null;
837
- }
838
- async function executeDeclarativeSteps(steps, onNavigate) {
839
- if (typeof window === "undefined" || !Array.isArray(steps)) return;
840
- for (const step of steps) {
841
- try {
842
- const { action, selector, value, payload } = step;
843
- switch (action) {
844
- case "scroll_into_view": {
845
- if (!selector) break;
846
- const el = await waitForElement(selector);
847
- el?.scrollIntoView({ behavior: "smooth", block: "center" });
848
- break;
849
- }
850
- case "click": {
851
- if (!selector) break;
852
- const el = await waitForElement(selector);
853
- el?.click();
854
- break;
855
- }
856
- case "focus": {
857
- if (!selector) break;
858
- const el = await waitForElement(selector);
859
- el?.focus();
860
- break;
861
- }
862
- case "add_class": {
863
- if (!selector || !value) break;
864
- const el = await waitForElement(selector);
865
- if (el) el.classList.add(...value.split(/\s+/).filter(Boolean));
866
- break;
867
- }
868
- case "remove_class": {
869
- if (!selector || !value) break;
870
- const el = await waitForElement(selector);
871
- if (el) el.classList.remove(...value.split(/\s+/).filter(Boolean));
872
- break;
873
- }
874
- case "fill_value": {
875
- if (!selector) break;
876
- const el = await waitForElement(selector);
877
- if (el) {
878
- el.value = value ?? "";
879
- el.dispatchEvent(new Event("input", { bubbles: true }));
880
- el.dispatchEvent(new Event("change", { bubbles: true }));
881
- }
882
- break;
883
- }
884
- case "dispatch_event": {
885
- if (!value) break;
886
- const detail = payload ?? {};
887
- window.dispatchEvent(new CustomEvent(value, { detail }));
888
- break;
889
- }
890
- case "navigate": {
891
- if (value) {
892
- if (onNavigate) {
893
- onNavigate(value);
894
- } else {
895
- window.location.href = value;
896
- }
897
- }
898
- break;
899
- }
900
- default:
901
- console.warn(
902
- `[Wallavi Widget] Unknown declarative action: ${action}`
903
- );
904
- }
905
- } catch (err) {
906
- console.error(
907
- "[Wallavi Widget] Failed to execute declarative step:",
908
- step,
909
- err
910
- );
911
- }
912
- }
913
- }
914
1250
  function getPreferredMimeType() {
915
1251
  if (typeof MediaRecorder === "undefined") return "";
916
1252
  const candidates = [
@@ -927,7 +1263,6 @@ function mimeTypeToExtension(mimeType) {
927
1263
  if (mimeType.includes("mp4")) return "mp4";
928
1264
  return "webm";
929
1265
  }
930
- var DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
931
1266
  function useVoice({
932
1267
  agentId,
933
1268
  apiUrl,
@@ -940,7 +1275,7 @@ function useVoice({
940
1275
  const streamRef = useRef(null);
941
1276
  const errorTimerRef = useRef(null);
942
1277
  const isSupported = typeof window !== "undefined" && typeof MediaRecorder !== "undefined" && !!navigator?.mediaDevices?.getUserMedia;
943
- const base = apiUrl ?? DEFAULT_API_URL;
1278
+ const base = resolveApiUrl(apiUrl);
944
1279
  const transcribeBlob = useCallback(
945
1280
  async (blob, mimeType) => {
946
1281
  setVoiceState("transcribing");
@@ -1019,7 +1354,6 @@ function useVoice({
1019
1354
  }, []);
1020
1355
  return { voiceState, isSupported, start, stop };
1021
1356
  }
1022
- var DEFAULT_API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
1023
1357
  function makeId() {
1024
1358
  return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
1025
1359
  }
@@ -1029,7 +1363,7 @@ function useAttachments({
1029
1363
  maxFiles = 5
1030
1364
  }) {
1031
1365
  const [attachments, setAttachments] = useState([]);
1032
- const base = apiUrl ?? DEFAULT_API_URL2;
1366
+ const base = resolveApiUrl(apiUrl);
1033
1367
  const uploadOne = useCallback(
1034
1368
  async (file, id) => {
1035
1369
  const form = new FormData();
@@ -2824,6 +3158,20 @@ function VoiceOverlay({
2824
3158
  }
2825
3159
  ) });
2826
3160
  }
3161
+
3162
+ // src/lib/unique-keys.ts
3163
+ function uniqueKeys(items, baseKey) {
3164
+ const seen = /* @__PURE__ */ new Map();
3165
+ return items.map((item) => {
3166
+ const base = baseKey(item);
3167
+ const count = seen.get(base) ?? 0;
3168
+ seen.set(base, count + 1);
3169
+ return count === 0 ? base : `${base}#${count}`;
3170
+ });
3171
+ }
3172
+ var actionKey = (action) => `${action.name}:${JSON.stringify(action.steps ?? [])}`;
3173
+ var docKey = (doc) => `${doc.name}:${doc.url ?? ""}`;
3174
+ var topicKey = (topic) => topic.name;
2827
3175
  function SvgIcon({
2828
3176
  className,
2829
3177
  strokeWidth = "1.8",
@@ -2897,11 +3245,11 @@ function urlRelevance(docUrl, pageUrl) {
2897
3245
  return 50;
2898
3246
  const docParts = docClean.split(/[/?#]/).filter(Boolean);
2899
3247
  const pageParts = pageClean.split(/[/?#]/).filter(Boolean);
2900
- let score = 0;
3248
+ let score2 = 0;
2901
3249
  for (const part of pageParts) {
2902
- if (docParts.includes(part)) score += 10;
3250
+ if (docParts.includes(part)) score2 += 10;
2903
3251
  }
2904
- return score;
3252
+ return score2;
2905
3253
  }
2906
3254
  function topicToQuestion(topic, locale) {
2907
3255
  const name = topic.name.trim();
@@ -2992,9 +3340,9 @@ function getContextKeywords(pageUrl, pageTitle, pageParams, pageVars, metadata)
2992
3340
  return Array.from(keywords);
2993
3341
  }
2994
3342
  function computeRelevanceScore(item, keywords, pageUrl) {
2995
- let score = 0;
3343
+ let score2 = 0;
2996
3344
  if (item.url && pageUrl) {
2997
- score += urlRelevance(item.url, pageUrl);
3345
+ score2 += urlRelevance(item.url, pageUrl);
2998
3346
  }
2999
3347
  const itemName = (item.name || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
3000
3348
  const itemDesc = (item.description || item.summary || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -3002,33 +3350,41 @@ function computeRelevanceScore(item, keywords, pageUrl) {
3002
3350
  const nameWords = itemName.split(/[^a-z0-9]/).filter(Boolean);
3003
3351
  for (const keyword of keywords) {
3004
3352
  if (itemName.includes(keyword)) {
3005
- score += 30;
3353
+ score2 += 30;
3006
3354
  if (nameWords.includes(keyword)) {
3007
- score += 20;
3355
+ score2 += 20;
3008
3356
  }
3009
3357
  }
3010
3358
  if (itemDesc.includes(keyword)) {
3011
- score += 15;
3359
+ score2 += 15;
3012
3360
  }
3013
3361
  if (itemUrl.includes(keyword)) {
3014
- score += 20;
3362
+ score2 += 20;
3015
3363
  }
3016
3364
  }
3017
- if (item.keywords) {
3018
- const allItemKeywords = [
3019
- ...item.keywords.en || [],
3020
- ...item.keywords.es || [],
3021
- ...item.keywords.fr || []
3022
- ].map(
3365
+ if (item.keywords && typeof item.keywords === "object") {
3366
+ const rawList = [];
3367
+ if (Array.isArray(item.keywords)) {
3368
+ rawList.push(...item.keywords);
3369
+ } else {
3370
+ const kwRecord = item.keywords;
3371
+ for (const lang of ["en", "es", "fr"]) {
3372
+ const langVal = kwRecord[lang];
3373
+ if (Array.isArray(langVal)) {
3374
+ rawList.push(...langVal);
3375
+ }
3376
+ }
3377
+ }
3378
+ const allItemKeywords = rawList.filter((k) => typeof k === "string" && k.trim().length > 0).map(
3023
3379
  (k) => k.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
3024
3380
  );
3025
3381
  for (const keyword of keywords) {
3026
3382
  if (allItemKeywords.includes(keyword)) {
3027
- score += 45;
3383
+ score2 += 45;
3028
3384
  }
3029
3385
  }
3030
3386
  }
3031
- return score;
3387
+ return score2;
3032
3388
  }
3033
3389
  var STAGGER_MS = 50;
3034
3390
  var cardBase = "ww-group ww-w-full ww-rounded-xl ww-border ww-border-border ww-bg-background ww-text-left ww-shadow-[0_1px_2px_rgba(0,0,0,0.03)] hover:ww-bg-muted/20 hover:ww-border-border/80 hover:ww-shadow-md active:ww-scale-[0.98] ww-transition-all ww-duration-200 ww-animate-in ww-fade-in ww-slide-in-from-bottom-1 focus-visible:ww-ring-2 focus-visible:ww-ring-ring/40 focus-visible:ww-outline-none";
@@ -3241,6 +3597,9 @@ function CommandPanel({
3241
3597
  }));
3242
3598
  return scored.sort((a, b) => b.score - a.score);
3243
3599
  }, [clientActions, pageUrl, pageTitle, pageParams, pageVars, metadata]);
3600
+ const actionKeys = uniqueKeys(sortedActions, actionKey);
3601
+ const docKeys = uniqueKeys(relevantDocs, docKey);
3602
+ const topicKeys = uniqueKeys(topicQuestions, topicKey);
3244
3603
  const hasActions = sortedActions.length > 0;
3245
3604
  const hasDocs = relevantDocs.length > 0;
3246
3605
  const hasTopics = topicQuestions.length > 0;
@@ -3262,7 +3621,7 @@ function CommandPanel({
3262
3621
  accentColor,
3263
3622
  onExecute: () => handleExecute(action.steps)
3264
3623
  },
3265
- action.name
3624
+ actionKeys[i]
3266
3625
  )) })
3267
3626
  ] }),
3268
3627
  hasDocs && /* @__PURE__ */ jsxs("section", { children: [
@@ -3275,7 +3634,7 @@ function CommandPanel({
3275
3634
  onSend,
3276
3635
  locale
3277
3636
  },
3278
- `${doc.name}-${doc.url ?? i}`
3637
+ docKeys[i]
3279
3638
  )) })
3280
3639
  ] }),
3281
3640
  hasTopics && /* @__PURE__ */ jsxs("section", { children: [
@@ -3287,7 +3646,7 @@ function CommandPanel({
3287
3646
  index: i,
3288
3647
  onSend
3289
3648
  },
3290
- topic.name
3649
+ topicKeys[i]
3291
3650
  )) })
3292
3651
  ] })
3293
3652
  ] }),
@@ -3334,7 +3693,7 @@ function CommandPanel({
3334
3693
  accentColor,
3335
3694
  onExecute: () => handleExecute(action.steps)
3336
3695
  },
3337
- action.name
3696
+ actionKeys[i]
3338
3697
  )) })
3339
3698
  ] }),
3340
3699
  hasDocs && /* @__PURE__ */ jsxs("section", { children: [
@@ -3347,7 +3706,7 @@ function CommandPanel({
3347
3706
  onSend,
3348
3707
  locale
3349
3708
  },
3350
- `${doc.name}-${doc.url ?? i}`
3709
+ docKeys[i]
3351
3710
  )) })
3352
3711
  ] }),
3353
3712
  hasTopics && /* @__PURE__ */ jsxs("section", { children: [
@@ -3360,13 +3719,14 @@ function CommandPanel({
3360
3719
  compact: true,
3361
3720
  onSend
3362
3721
  },
3363
- topic.name
3722
+ topicKeys[i]
3364
3723
  )) })
3365
3724
  ] })
3366
3725
  ] });
3367
3726
  }
3368
3727
  function ChatWidget({
3369
3728
  agentId,
3729
+ apiUrl,
3370
3730
  workspaceId,
3371
3731
  agentName,
3372
3732
  displayName,
@@ -3427,8 +3787,10 @@ function ChatWidget({
3427
3787
  window.addEventListener("message", handleMessage);
3428
3788
  return () => window.removeEventListener("message", handleMessage);
3429
3789
  }, []);
3790
+ const resolvePageContext = () => userContext?.pageContext ?? (typeof window === "undefined" ? void 0 : derivePageContext(window.location, clientActions));
3430
3791
  const chat = useChat({
3431
3792
  agentId,
3793
+ apiUrl,
3432
3794
  workspaceId,
3433
3795
  envId,
3434
3796
  source,
@@ -3436,10 +3798,13 @@ function ChatWidget({
3436
3798
  persist,
3437
3799
  onNavigate,
3438
3800
  playgroundOverrides,
3439
- customBackend
3801
+ customBackend,
3802
+ copilotColor: userMessageColor,
3803
+ resolvePageContext: () => resolvePageContext()
3440
3804
  });
3441
3805
  const voice = useVoice({
3442
3806
  agentId,
3807
+ apiUrl,
3443
3808
  onTranscript: (text) => {
3444
3809
  if (voiceAutoSend) {
3445
3810
  void chat.send(text);
@@ -3448,7 +3813,7 @@ function ChatWidget({
3448
3813
  }
3449
3814
  }
3450
3815
  });
3451
- const attachmentHook = useAttachments({ agentId });
3816
+ const attachmentHook = useAttachments({ agentId, apiUrl });
3452
3817
  const debugTraceLenRef = useRef(0);
3453
3818
  useEffect(() => {
3454
3819
  if (!onDebugTrace || chat.debugTraces.length <= debugTraceLenRef.current)
@@ -3548,9 +3913,9 @@ function ChatWidget({
3548
3913
  const showInlineCommandPanel = showCommandPanel && !showSidebar && chat.messages.length === 0 && (showQuickActions && clientActions.length > 0 || showRagTopics && ragTopics.length > 0 || showRagDocuments && ragDocuments.length > 0);
3549
3914
  const handleExecuteAction = useCallback(
3550
3915
  (steps) => {
3551
- executeDeclarativeSteps(steps, onNavigate);
3916
+ executeDeclarativeSteps(steps, onNavigate, userMessageColor);
3552
3917
  },
3553
- [onNavigate]
3918
+ [onNavigate, userMessageColor]
3554
3919
  );
3555
3920
  const [activeActionIndex, setActiveActionIndex] = useState(-1);
3556
3921
  const query = chat.input.toLowerCase().trim();
@@ -3607,11 +3972,12 @@ function ChatWidget({
3607
3972
  },
3608
3973
  [matchingActions, activeActionIndex, handleExecuteAction, chat]
3609
3974
  );
3975
+ const pageContext = resolvePageContext();
3610
3976
  const commandPanelProps = {
3611
- pageUrl: userContext?.pageContext?.url,
3612
- pageTitle: userContext?.pageContext?.title,
3613
- pageParams: userContext?.pageContext?.params,
3614
- pageVars: userContext?.pageContext?.vars,
3977
+ pageUrl: pageContext?.url,
3978
+ pageTitle: pageContext?.title,
3979
+ pageParams: pageContext?.params,
3980
+ pageVars: pageContext?.vars,
3615
3981
  metadata: userContext?.metadata,
3616
3982
  clientActions: showQuickActions ? clientActions : [],
3617
3983
  ragTopics: showRagTopics ? ragTopics : [],
@@ -3931,7 +4297,6 @@ function ChatWidget({
3931
4297
  }
3932
4298
  );
3933
4299
  }
3934
- var WALLAVI_PUBLIC_API = "https://wallavi-production.up.railway.app";
3935
4300
  var EMPTY = {
3936
4301
  remoteConfig: {},
3937
4302
  bubbleIconUrl: void 0,
@@ -3945,7 +4310,37 @@ var EMPTY = {
3945
4310
  panelHeight: 580,
3946
4311
  loading: false
3947
4312
  };
3948
- function useAutoConfig(agentId, enabled) {
4313
+ function toChatProps(cfg) {
4314
+ const remote = {};
4315
+ const set = (key, value) => {
4316
+ if (value !== null && value !== void 0) remote[key] = value;
4317
+ };
4318
+ set("agentName", cfg.agentName);
4319
+ set("profilePicture", cfg.profilePicture);
4320
+ set("displayName", cfg.displayName);
4321
+ set("theme", cfg.theme);
4322
+ set("userMessageColor", cfg.userMessageColor);
4323
+ if (cfg.initialMessages?.length) remote.initialMessages = cfg.initialMessages;
4324
+ if (cfg.suggestedMessages?.length)
4325
+ remote.suggestedMessages = cfg.suggestedMessages;
4326
+ set("messagePlaceholder", cfg.messagePlaceholder);
4327
+ set("watermark", cfg.watermark);
4328
+ set("footer", cfg.footer);
4329
+ set("showThinking", cfg.showThinking);
4330
+ set("regenerateMessage", cfg.regenerateMessage);
4331
+ set("widgetLayout", cfg.widgetLayout);
4332
+ set("enableVoice", cfg.enableVoice);
4333
+ set("showCommandPanel", cfg.showCommandPanel);
4334
+ set("showRagTopics", cfg.showRagTopics);
4335
+ set("showRagDocuments", cfg.showRagDocuments);
4336
+ set("showQuickActions", cfg.showQuickActions);
4337
+ set("clientActions", cfg.clientActions);
4338
+ set("ragTopics", cfg.ragTopics);
4339
+ set("ragDocuments", cfg.ragDocuments);
4340
+ set("locale", cfg.locale);
4341
+ return remote;
4342
+ }
4343
+ function useAutoConfig(agentId, enabled, apiUrl) {
3949
4344
  const [result, setResult] = useState(() => ({
3950
4345
  ...EMPTY,
3951
4346
  loading: enabled && Boolean(agentId)
@@ -3956,61 +4351,34 @@ function useAutoConfig(agentId, enabled) {
3956
4351
  return;
3957
4352
  }
3958
4353
  let cancelled = false;
3959
- fetch(`${WALLAVI_PUBLIC_API}/api/public/widget/${agentId}`).then((r) => r.json()).then((body) => {
4354
+ setResult({ ...EMPTY, loading: true });
4355
+ fetch(`${resolveApiUrl(apiUrl)}/api/public/widget/${agentId}`).then((r) => r.json()).then((body) => {
3960
4356
  if (cancelled) return;
3961
- const cfg = body?.data ?? {};
3962
- const remote = {};
3963
- if (cfg.profilePicture != null)
3964
- remote.profilePicture = cfg.profilePicture;
3965
- if (cfg.displayName != null)
3966
- remote.displayName = cfg.displayName;
3967
- if (cfg.theme) remote.theme = cfg.theme;
3968
- if (cfg.userMessageColor)
3969
- remote.userMessageColor = cfg.userMessageColor;
3970
- if (Array.isArray(cfg.initialMessages) && cfg.initialMessages.length > 0)
3971
- remote.initialMessages = cfg.initialMessages;
3972
- if (Array.isArray(cfg.suggestedMessages))
3973
- remote.suggestedMessages = cfg.suggestedMessages;
3974
- if (cfg.messagePlaceholder != null)
3975
- remote.messagePlaceholder = cfg.messagePlaceholder;
3976
- if (cfg.watermark != null) remote.watermark = cfg.watermark;
3977
- if (cfg.footer != null) remote.footer = cfg.footer;
3978
- if (cfg.showThinking != null)
3979
- remote.showThinking = cfg.showThinking;
3980
- if (cfg.regenerateMessage != null)
3981
- remote.regenerateMessage = cfg.regenerateMessage;
3982
- if (cfg.widgetLayout)
3983
- remote.widgetLayout = cfg.widgetLayout;
3984
- if (cfg.enableVoice != null)
3985
- remote.enableVoice = cfg.enableVoice;
3986
- if (cfg.clientActions)
3987
- remote.clientActions = cfg.clientActions;
3988
- if (Array.isArray(cfg.ragTopics))
3989
- remote.ragTopics = cfg.ragTopics;
3990
- if (Array.isArray(cfg.ragDocuments))
3991
- remote.ragDocuments = cfg.ragDocuments;
3992
- if (cfg.locale != null)
3993
- remote.locale = cfg.locale;
4357
+ const cfg = body?.data;
4358
+ if (!cfg) {
4359
+ setResult(EMPTY);
4360
+ return;
4361
+ }
3994
4362
  setResult({
3995
- remoteConfig: remote,
4363
+ remoteConfig: toChatProps(cfg),
3996
4364
  bubbleIconUrl: cfg.chatIcon || cfg.profilePicture || void 0,
3997
4365
  autoOpen: Boolean(cfg.autoOpen),
3998
4366
  keyboardShortcut: Boolean(cfg.keyboardShortcut),
3999
4367
  position: cfg.alignChatBubbleButton === "left" ? "bottom-left" : "bottom-right",
4000
4368
  widgetLayout: cfg.widgetLayout === "center" ? "center" : "bubble",
4001
- clientActions: Array.isArray(cfg.clientActions) ? cfg.clientActions : [],
4002
- bubbleSize: typeof cfg.bubbleSize === "number" ? cfg.bubbleSize : 52,
4003
- panelWidth: typeof cfg.panelWidth === "number" ? cfg.panelWidth : 360,
4004
- panelHeight: typeof cfg.panelHeight === "number" ? cfg.panelHeight : 580,
4369
+ clientActions: cfg.clientActions ?? [],
4370
+ bubbleSize: cfg.bubbleSize ?? 52,
4371
+ panelWidth: cfg.panelWidth ?? 360,
4372
+ panelHeight: cfg.panelHeight ?? 580,
4005
4373
  loading: false
4006
4374
  });
4007
4375
  }).catch(() => {
4008
- if (!cancelled) setResult((r) => ({ ...r, loading: false }));
4376
+ if (!cancelled) setResult(EMPTY);
4009
4377
  });
4010
4378
  return () => {
4011
4379
  cancelled = true;
4012
4380
  };
4013
- }, [agentId, enabled]);
4381
+ }, [agentId, enabled, apiUrl]);
4014
4382
  return result;
4015
4383
  }
4016
4384
  function toWidgetMsg(m) {
@@ -4091,6 +4459,38 @@ function useSupportChat({
4091
4459
  },
4092
4460
  [base]
4093
4461
  );
4462
+ function loadAblyScript() {
4463
+ return new Promise((resolve, reject) => {
4464
+ if (typeof window === "undefined") {
4465
+ resolve(null);
4466
+ return;
4467
+ }
4468
+ if (window.Ably) {
4469
+ resolve(window.Ably);
4470
+ return;
4471
+ }
4472
+ const existing = document.querySelector("script[data-ably-sdk]");
4473
+ if (existing) {
4474
+ existing.addEventListener("load", () => resolve(window.Ably));
4475
+ existing.addEventListener(
4476
+ "error",
4477
+ () => reject(new Error("Ably CDN load failed"))
4478
+ );
4479
+ return;
4480
+ }
4481
+ const script = document.createElement("script");
4482
+ script.src = "https://cdn.ably.com/lib/ably.min-2.js";
4483
+ script.crossOrigin = "anonymous";
4484
+ script.async = true;
4485
+ script.setAttribute("data-ably-sdk", "true");
4486
+ script.onload = () => resolve(window.Ably);
4487
+ script.onerror = () => {
4488
+ script.remove();
4489
+ reject(new Error("Ably CDN load failed"));
4490
+ };
4491
+ document.head.appendChild(script);
4492
+ });
4493
+ }
4094
4494
  useEffect(() => {
4095
4495
  if (!enabled || !session) return;
4096
4496
  void loadMessages(session);
@@ -4105,7 +4505,8 @@ function useSupportChat({
4105
4505
  );
4106
4506
  if (!res.ok) return;
4107
4507
  const { tokenRequest, channel: channelName } = await res.json();
4108
- const AblyLib = (await import('ably')).default;
4508
+ const AblyLib = await loadAblyScript();
4509
+ if (!AblyLib) return;
4109
4510
  ablyClient = new AblyLib.Realtime({
4110
4511
  authCallback: (_, cb) => cb(null, tokenRequest)
4111
4512
  });
@@ -4413,9 +4814,9 @@ function BubbleWidget({
4413
4814
  height: heightProp,
4414
4815
  expandedWidth = 640,
4415
4816
  expandedHeight = "calc(100vh - 100px)",
4416
- keyboardShortcut: keyboardShortcutProp = false,
4817
+ keyboardShortcut: keyboardShortcutProp,
4417
4818
  shortcutKey = "k",
4418
- autoOpen: autoOpenProp = false,
4819
+ autoOpen: autoOpenProp,
4419
4820
  bubbleIconUrl: bubbleIconUrlProp,
4420
4821
  bubbleSize: bubbleSizeProp,
4421
4822
  panelClassName,
@@ -4456,24 +4857,25 @@ function BubbleWidget({
4456
4857
  }, []);
4457
4858
  const remote = useAutoConfig(
4458
4859
  inboxToken ? "" : chatProps.agentId ?? "",
4459
- !inboxToken && autoConfig
4860
+ !inboxToken && autoConfig,
4861
+ chatProps.apiUrl
4460
4862
  );
4461
- const resolvedPosition = remote.position ?? positionProp;
4462
- const resolvedLayout = remote.widgetLayout ?? chatProps.widgetLayout ?? "bubble";
4463
- const resolvedBubbleIcon = remote.bubbleIconUrl ?? bubbleIconUrlProp;
4464
- const resolvedAutoOpen = remote.autoOpen || autoOpenProp;
4465
- const resolvedKeyboardShortcut = remote.keyboardShortcut || keyboardShortcutProp;
4466
- const resolvedBubbleSize = remote.bubbleSize ?? bubbleSizeProp;
4467
- const resolvedWidth = remote.panelWidth ?? widthProp;
4468
- const resolvedHeight = remote.panelHeight ?? heightProp;
4863
+ const resolvedPosition = positionProp ?? remote.position;
4864
+ const resolvedLayout = chatProps.widgetLayout ?? remote.widgetLayout;
4865
+ const resolvedBubbleIcon = bubbleIconUrlProp ?? remote.bubbleIconUrl;
4866
+ const resolvedAutoOpen = autoOpenProp ?? remote.autoOpen;
4867
+ const resolvedKeyboardShortcut = keyboardShortcutProp ?? remote.keyboardShortcut;
4868
+ const resolvedBubbleSize = bubbleSizeProp ?? remote.bubbleSize;
4869
+ const resolvedWidth = widthProp ?? remote.panelWidth;
4870
+ const resolvedHeight = heightProp ?? remote.panelHeight;
4469
4871
  const definedChatProps = Object.fromEntries(
4470
4872
  Object.entries(chatProps).filter(([, v]) => v !== void 0)
4471
4873
  );
4472
4874
  const mergedConfig = {
4473
- ...definedChatProps,
4474
4875
  ...remote.remoteConfig,
4876
+ ...definedChatProps,
4475
4877
  agentId: chatProps.agentId ?? "",
4476
- agentName: remote.remoteConfig.agentName ?? chatProps.agentName ?? "Asistente",
4878
+ agentName: chatProps.agentName ?? remote.remoteConfig.agentName ?? "Asistente",
4477
4879
  source: chatProps.source ?? remote.remoteConfig.source ?? "web"
4478
4880
  };
4479
4881
  const supportBackend = useSupportChat(
@@ -4489,6 +4891,10 @@ function BubbleWidget({
4489
4891
  useEffect(() => {
4490
4892
  setOpenRef.current = setOpen;
4491
4893
  });
4894
+ const openRef = useRef(open);
4895
+ useEffect(() => {
4896
+ openRef.current = open;
4897
+ });
4492
4898
  useEffect(() => {
4493
4899
  if (!resolvedAutoOpen || autoOpenedRef.current) return;
4494
4900
  const dismissedUntil = Number(localStorage.getItem(KEY_DISMISSED) ?? 0);
@@ -4498,15 +4904,29 @@ function BubbleWidget({
4498
4904
  }
4499
4905
  }, [resolvedAutoOpen]);
4500
4906
  useEffect(() => {
4501
- if (!resolvedKeyboardShortcut) return;
4907
+ const onToggle = () => setOpenRef.current((v) => !v);
4908
+ const onOpen = () => setOpenRef.current(true);
4909
+ const onClose = () => setOpenRef.current(false);
4910
+ window.addEventListener("wallavi:toggle-assistant", onToggle);
4911
+ window.addEventListener("wallavi:open-assistant", onOpen);
4912
+ window.addEventListener("wallavi:close-assistant", onClose);
4502
4913
  const onKey = (e) => {
4503
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey.toLowerCase()) {
4914
+ if (resolvedKeyboardShortcut && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey.toLowerCase()) {
4504
4915
  e.preventDefault();
4505
4916
  setOpenRef.current((v) => !v);
4917
+ } else if (e.key === "Escape" && openRef.current) {
4918
+ e.preventDefault();
4919
+ e.stopPropagation();
4920
+ setOpenRef.current(false);
4506
4921
  }
4507
4922
  };
4508
4923
  window.addEventListener("keydown", onKey);
4509
- return () => window.removeEventListener("keydown", onKey);
4924
+ return () => {
4925
+ window.removeEventListener("wallavi:toggle-assistant", onToggle);
4926
+ window.removeEventListener("wallavi:open-assistant", onOpen);
4927
+ window.removeEventListener("wallavi:close-assistant", onClose);
4928
+ window.removeEventListener("keydown", onKey);
4929
+ };
4510
4930
  }, [resolvedKeyboardShortcut, shortcutKey]);
4511
4931
  useEffect(() => {
4512
4932
  if (!open) return;
@@ -4561,7 +4981,8 @@ function BubbleWidget({
4561
4981
  display: "flex",
4562
4982
  flexDirection: "column",
4563
4983
  alignItems: isLeft ? "flex-start" : "flex-end",
4564
- gap: 12
4984
+ gap: 12,
4985
+ pointerEvents: !open && hideBubble ? "none" : void 0
4565
4986
  },
4566
4987
  children: [
4567
4988
  /* @__PURE__ */ jsx(
@@ -4578,12 +4999,14 @@ function BubbleWidget({
4578
4999
  zIndex: 9999,
4579
5000
  width: panelWidth,
4580
5001
  height: panelHeight,
4581
- transition: "width 0.3s ease, height 0.3s ease"
5002
+ transition: "width 0.3s ease, height 0.3s ease",
5003
+ pointerEvents: "auto"
4582
5004
  } : {
4583
5005
  display: open ? "block" : "none",
4584
5006
  width: panelWidth,
4585
5007
  height: panelHeight,
4586
- transition: "width 0.3s ease, height 0.3s ease"
5008
+ transition: "width 0.3s ease, height 0.3s ease",
5009
+ pointerEvents: "auto"
4587
5010
  },
4588
5011
  children: /* @__PURE__ */ jsx(
4589
5012
  ChatWidget,