@wallavi/widget 1.12.7 → 1.13.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/index.js CHANGED
@@ -92,6 +92,83 @@ async function getFreshClerkToken() {
92
92
  }
93
93
  }
94
94
 
95
+ // src/lib/page-context.ts
96
+ function candidatesFrom(actions) {
97
+ const out = [];
98
+ for (const action of actions) {
99
+ const title = action.displayName ?? action.name;
100
+ const target = action.steps?.find((s) => s.action === "navigate")?.value;
101
+ if (!title || typeof target !== "string" || !target.startsWith("/")) {
102
+ continue;
103
+ }
104
+ const [path = "", search = ""] = target.split("?");
105
+ out.push({
106
+ title,
107
+ segments: path.split("/").filter(Boolean),
108
+ query: new URLSearchParams(search)
109
+ });
110
+ }
111
+ return out;
112
+ }
113
+ var PARAM = /^\[(\w+)\]$/;
114
+ var LOCALE = /^[a-z]{2}(?:-[a-z]{2,4})?$/i;
115
+ function offsetsFor(pathSegments, routeLength) {
116
+ const offsets = [];
117
+ if (pathSegments.length === routeLength) offsets.push(0);
118
+ if (pathSegments.length === routeLength + 1 && LOCALE.test(pathSegments[0] ?? "")) {
119
+ offsets.push(1);
120
+ }
121
+ return offsets;
122
+ }
123
+ function safeDecode(segment) {
124
+ try {
125
+ return decodeURIComponent(segment);
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ function score(candidate, pathSegments, search) {
131
+ const { segments, query } = candidate;
132
+ if (segments.length === 0) return null;
133
+ for (const [key, value] of query) {
134
+ if (search.get(key) !== value) return null;
135
+ }
136
+ for (const offset of offsetsFor(pathSegments, segments.length)) {
137
+ const params = {};
138
+ let points = Array.from(query.keys()).length;
139
+ let matched = true;
140
+ for (let i = 0; i < segments.length && matched; i++) {
141
+ const pattern = segments[i];
142
+ const actual = safeDecode(pathSegments[offset + i]);
143
+ const param = PARAM.exec(pattern);
144
+ if (actual === null) matched = false;
145
+ else if (param) params[param[1]] = actual;
146
+ else if (pattern === actual) points += 2;
147
+ else matched = false;
148
+ }
149
+ if (matched) return { score: points, params };
150
+ }
151
+ return null;
152
+ }
153
+ function derivePageContext(location, actions) {
154
+ if (!actions?.length) return void 0;
155
+ const pathSegments = location.pathname.split("/").filter(Boolean);
156
+ const search = new URLSearchParams(location.search);
157
+ let best = null;
158
+ for (const candidate of candidatesFrom(actions)) {
159
+ const result = score(candidate, pathSegments, search);
160
+ if (result && (!best || result.score > best.score)) {
161
+ best = { title: candidate.title, ...result };
162
+ }
163
+ }
164
+ if (!best) return void 0;
165
+ return {
166
+ url: `${location.pathname}${location.search}`,
167
+ title: best.title,
168
+ ...Object.keys(best.params).length > 0 ? { params: best.params } : {}
169
+ };
170
+ }
171
+
95
172
  // src/lib/types.ts
96
173
  function getContrastColor(hex) {
97
174
  const clean = hex.replace("#", "");
@@ -105,7 +182,7 @@ function formatToolName(name) {
105
182
  return name.replace(/([A-Z])/g, " $1").replace(/_/g, " ").trim();
106
183
  }
107
184
 
108
- // src/core/stream-protocol.ts
185
+ // ../protocol/src/index.ts
109
186
  var STREAM_DELIMITER = "\u03B6\u236E";
110
187
  async function consumeStream(body, handler) {
111
188
  const reader = body.getReader();
@@ -121,9 +198,9 @@ async function consumeStream(body, handler) {
121
198
  if (!raw) continue;
122
199
  try {
123
200
  const parsed = JSON.parse(raw);
124
- const proto = parsed.data?.uiMessageProtocol;
125
- if (proto) {
201
+ if (parsed.data?.uiMessageProtocol) {
126
202
  eventCount++;
203
+ const proto = parsed.data.uiMessageProtocol;
127
204
  if (proto.type === "text-delta") {
128
205
  textAccumulator += proto.delta ?? "";
129
206
  }
@@ -273,12 +350,321 @@ function applyUiEventToMessages(prev, proto, msgId) {
273
350
  copy[idx] = msg;
274
351
  return copy;
275
352
  }
276
- var API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
353
+
354
+ // src/core/declarative-actions.ts
355
+ function sanitizeSelector(selector) {
356
+ if (!selector || typeof selector !== "string") return null;
357
+ const lower = selector.toLowerCase();
358
+ if (lower.includes("<") || lower.includes(">") || lower.includes("script") || lower.includes("iframe") || lower.includes("onload") || lower.includes("onerror") || lower.includes("javascript:")) {
359
+ return null;
360
+ }
361
+ const cleanRegex = /^[a-zA-Z0-9\s\.\-#_\[\]="':]+$/;
362
+ if (!cleanRegex.test(selector)) {
363
+ return null;
364
+ }
365
+ return selector;
366
+ }
367
+ function sanitizeEventName(name) {
368
+ if (!name || typeof name !== "string") return null;
369
+ const nameRegex = /^[a-zA-Z0-9\-_:]+$/;
370
+ if (!nameRegex.test(name)) return null;
371
+ const nativeEvents = /* @__PURE__ */ new Set([
372
+ "click",
373
+ "dblclick",
374
+ "mouseup",
375
+ "mousedown",
376
+ "mouseover",
377
+ "mouseout",
378
+ "submit",
379
+ "reset",
380
+ "change",
381
+ "select",
382
+ "keydown",
383
+ "keypress",
384
+ "keyup",
385
+ "load",
386
+ "unload",
387
+ "abort",
388
+ "error",
389
+ "resize",
390
+ "scroll",
391
+ "contextmenu"
392
+ ]);
393
+ if (nativeEvents.has(name.toLowerCase())) return null;
394
+ return name;
395
+ }
396
+ function sanitizeUrl(url) {
397
+ if (!url || typeof url !== "string") return null;
398
+ const trimmed = url.trim();
399
+ const lower = trimmed.toLowerCase();
400
+ if (trimmed.startsWith("//")) {
401
+ return null;
402
+ }
403
+ if (lower.startsWith("javascript:") || lower.startsWith("data:")) {
404
+ return null;
405
+ }
406
+ if (trimmed.startsWith("/")) {
407
+ return trimmed;
408
+ }
409
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
410
+ return trimmed;
411
+ }
412
+ return null;
413
+ }
414
+ async function waitForElement(selector, retries = 5, delayMs = 100) {
415
+ const cleanId = selector.startsWith("#") ? selector.substring(1) : selector;
416
+ for (let i = 0; i < retries; i++) {
417
+ try {
418
+ const el = document.querySelector(selector);
419
+ if (el) return el;
420
+ } catch (e) {
421
+ console.error(e);
422
+ }
423
+ try {
424
+ const byId = document.getElementById(cleanId);
425
+ if (byId) return byId;
426
+ } catch (e) {
427
+ console.error(e);
428
+ }
429
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[")) {
430
+ try {
431
+ const byPrependedId = document.querySelector(`#${selector}`);
432
+ if (byPrependedId) return byPrependedId;
433
+ } catch (e) {
434
+ console.error(e);
435
+ }
436
+ }
437
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
438
+ }
439
+ return null;
440
+ }
441
+ var HEX_COLOR_REGEX = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;
442
+ function sanitizeColor(color, fallback = "#6366f1") {
443
+ if (!color || typeof color !== "string") return fallback;
444
+ const trimmed = color.trim();
445
+ return HEX_COLOR_REGEX.test(trimmed) ? trimmed : fallback;
446
+ }
447
+ function injectCopilotStyles(color = "#6366f1") {
448
+ if (typeof document === "undefined") return;
449
+ const safeColor = sanitizeColor(color, "#6366f1");
450
+ let style = document.getElementById(
451
+ "wallavi-copilot-styles"
452
+ );
453
+ if (!style) {
454
+ style = document.createElement("style");
455
+ style.id = "wallavi-copilot-styles";
456
+ document.head.appendChild(style);
457
+ }
458
+ if (style.dataset.color === safeColor) return;
459
+ style.dataset.color = safeColor;
460
+ style.textContent = `
461
+ @keyframes wallavi-pulse-beacon {
462
+ 0% {
463
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7);
464
+ outline: 2px solid ${safeColor};
465
+ }
466
+ 70% {
467
+ box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
468
+ outline: 2px solid rgba(99, 102, 241, 0.3);
469
+ }
470
+ 100% {
471
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
472
+ outline: 2px solid transparent;
473
+ }
474
+ }
475
+ .wallavi-element-beacon {
476
+ animation: wallavi-pulse-beacon 1.8s ease-out infinite !important;
477
+ outline-offset: 3px !important;
478
+ border-radius: inherit;
479
+ transition: outline 0.2s ease, box-shadow 0.2s ease !important;
480
+ }
481
+ @keyframes wallavi-flash-touch-anim {
482
+ 0% {
483
+ outline: 3px solid ${safeColor};
484
+ outline-offset: 2px;
485
+ filter: brightness(1.08);
486
+ }
487
+ 100% {
488
+ outline: 2px solid transparent;
489
+ outline-offset: 6px;
490
+ filter: none;
491
+ }
492
+ }
493
+ .wallavi-flash-touch {
494
+ animation: wallavi-flash-touch-anim 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards !important;
495
+ }
496
+ `;
497
+ document.head.appendChild(style);
498
+ }
499
+ function flashElement(el, color) {
500
+ injectCopilotStyles(color);
501
+ el.classList.remove("wallavi-flash-touch");
502
+ void el.offsetWidth;
503
+ el.classList.add("wallavi-flash-touch");
504
+ setTimeout(() => {
505
+ el.classList.remove("wallavi-flash-touch");
506
+ }, 600);
507
+ }
508
+ function highlightElement(el, color, durationMs = 3e3) {
509
+ injectCopilotStyles(color);
510
+ el.classList.add("wallavi-element-beacon");
511
+ setTimeout(() => {
512
+ el.classList.remove("wallavi-element-beacon");
513
+ }, durationMs);
514
+ }
515
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
516
+ async function executeDeclarativeSteps(steps, onNavigate, copilotColor) {
517
+ if (typeof window === "undefined" || !Array.isArray(steps)) return;
518
+ const safeCopilotColor = sanitizeColor(copilotColor, "#6366f1");
519
+ for (const step of steps) {
520
+ try {
521
+ const { action } = step;
522
+ switch (action) {
523
+ case "scroll_to":
524
+ case "scroll_into_view": {
525
+ const selector = sanitizeSelector(step.selector);
526
+ if (!selector) break;
527
+ const el = await waitForElement(selector);
528
+ if (el) {
529
+ flashElement(el, safeCopilotColor);
530
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
531
+ }
532
+ break;
533
+ }
534
+ case "click": {
535
+ const selector = sanitizeSelector(step.selector);
536
+ if (!selector) break;
537
+ const el = await waitForElement(selector);
538
+ if (el) {
539
+ flashElement(el, safeCopilotColor);
540
+ await delay(80);
541
+ el.click();
542
+ }
543
+ break;
544
+ }
545
+ case "focus": {
546
+ const selector = sanitizeSelector(step.selector);
547
+ if (!selector) break;
548
+ const el = await waitForElement(selector);
549
+ if (el) {
550
+ flashElement(el, safeCopilotColor);
551
+ el.focus();
552
+ }
553
+ break;
554
+ }
555
+ case "highlight": {
556
+ const selector = sanitizeSelector(step.selector);
557
+ if (!selector) break;
558
+ const el = await waitForElement(selector);
559
+ if (el) {
560
+ highlightElement(
561
+ el,
562
+ sanitizeColor(step.color, safeCopilotColor),
563
+ step.durationMs ?? 3e3
564
+ );
565
+ }
566
+ break;
567
+ }
568
+ case "wait": {
569
+ const parsedVal = typeof step.value === "number" ? step.value : parseInt(String(step.value ?? "500"), 10);
570
+ const ms = step.delayMs ?? (Number.isNaN(parsedVal) ? 500 : parsedVal);
571
+ await delay(ms);
572
+ break;
573
+ }
574
+ case "add_class": {
575
+ const selector = sanitizeSelector(step.selector);
576
+ const value = step.value;
577
+ if (!selector || !value) break;
578
+ const el = await waitForElement(selector);
579
+ if (el) el.classList.add(...value.split(/\s+/).filter(Boolean));
580
+ break;
581
+ }
582
+ case "remove_class": {
583
+ const selector = sanitizeSelector(step.selector);
584
+ const value = step.value;
585
+ if (!selector || !value) break;
586
+ const el = await waitForElement(selector);
587
+ if (el) el.classList.remove(...value.split(/\s+/).filter(Boolean));
588
+ break;
589
+ }
590
+ case "fill_value": {
591
+ const selector = sanitizeSelector(step.selector);
592
+ const value = step.value;
593
+ if (!selector) break;
594
+ const el = await waitForElement(selector);
595
+ if (el && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
596
+ if (el instanceof HTMLInputElement) {
597
+ const type = el.type.toLowerCase();
598
+ if (type === "password" || type === "hidden") {
599
+ console.warn(
600
+ `[Wallavi Widget] Blocked fill_value action on sensitive field: ${type}`
601
+ );
602
+ break;
603
+ }
604
+ }
605
+ const isReadOnly = (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && el.readOnly;
606
+ if (el.disabled || isReadOnly) {
607
+ console.warn(
608
+ "[Wallavi Widget] Blocked fill_value action on disabled/readOnly element"
609
+ );
610
+ break;
611
+ }
612
+ flashElement(el, safeCopilotColor);
613
+ el.value = value ?? "";
614
+ el.dispatchEvent(new Event("input", { bubbles: true }));
615
+ el.dispatchEvent(new Event("change", { bubbles: true }));
616
+ }
617
+ break;
618
+ }
619
+ case "dispatch_event": {
620
+ const eventName = sanitizeEventName(step.value);
621
+ if (!eventName) break;
622
+ const detail = step.payload ?? {};
623
+ window.dispatchEvent(new CustomEvent(eventName, { detail }));
624
+ break;
625
+ }
626
+ case "navigate": {
627
+ const url = sanitizeUrl(step.value);
628
+ if (url) {
629
+ if (onNavigate) {
630
+ onNavigate(url);
631
+ } else {
632
+ window.location.href = url;
633
+ }
634
+ }
635
+ break;
636
+ }
637
+ default: {
638
+ const unknownAction = step.action;
639
+ console.warn(
640
+ `[Wallavi Widget] Unknown declarative action: ${unknownAction}`
641
+ );
642
+ }
643
+ }
644
+ await delay(120);
645
+ } catch (err) {
646
+ console.error(
647
+ "[Wallavi Widget] Failed to execute declarative step:",
648
+ step,
649
+ err
650
+ );
651
+ }
652
+ }
653
+ }
654
+
655
+ // src/lib/api-url.ts
656
+ var WALLAVI_PRODUCTION_API_URL = "https://wallavi-production.up.railway.app";
657
+ function resolveApiUrl(explicit) {
658
+ return explicit ?? process.env.NEXT_PUBLIC_API_URL ?? WALLAVI_PRODUCTION_API_URL;
659
+ }
660
+
661
+ // src/hooks/use-voice-call.ts
277
662
  function useVoiceCall({
278
663
  agentId,
279
664
  threadId,
280
665
  workspaceId,
281
- customBackend
666
+ customBackend,
667
+ apiUrl
282
668
  }) {
283
669
  const [active, setActive] = react.useState(false);
284
670
  const [token, setToken] = react.useState(null);
@@ -291,7 +677,7 @@ function useVoiceCall({
291
677
  setError(null);
292
678
  try {
293
679
  const isPrivate = Boolean(workspaceId);
294
- 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)}`;
680
+ 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)}`;
295
681
  const token2 = isPrivate ? await getFreshClerkToken() : null;
296
682
  const res = await fetch(url, {
297
683
  headers: {
@@ -307,7 +693,7 @@ function useVoiceCall({
307
693
  setError(err.message);
308
694
  }
309
695
  setLoading(false);
310
- }, [agentId, threadId, workspaceId, customBackend]);
696
+ }, [agentId, threadId, workspaceId, customBackend, apiUrl]);
311
697
  const stop = react.useCallback(() => {
312
698
  setActive(false);
313
699
  setToken(null);
@@ -324,13 +710,33 @@ function useVoiceCall({
324
710
  };
325
711
  }
326
712
 
713
+ // src/lib/user-headers.ts
714
+ var warnedHeadersFailure = false;
715
+ async function resolveUserHeaders(userContext) {
716
+ const source = userContext?.headers;
717
+ if (!source) return {};
718
+ if (typeof source !== "function") return source;
719
+ try {
720
+ return await source() ?? {};
721
+ } catch (err) {
722
+ if (!warnedHeadersFailure) {
723
+ warnedHeadersFailure = true;
724
+ console.warn(
725
+ "[wallavi-widget] userContext.headers threw; sending without host headers",
726
+ err
727
+ );
728
+ }
729
+ return {};
730
+ }
731
+ }
732
+
327
733
  // src/hooks/use-chat.ts
328
- var API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
329
734
  function newId() {
330
735
  return Math.random().toString(36).slice(2, 10);
331
736
  }
332
737
  function useChat({
333
738
  agentId,
739
+ apiUrl,
334
740
  workspaceId = "",
335
741
  envId,
336
742
  source = "playground",
@@ -338,13 +744,20 @@ function useChat({
338
744
  persist = false,
339
745
  onNavigate,
340
746
  playgroundOverrides,
341
- customBackend
747
+ customBackend,
748
+ copilotColor,
749
+ resolvePageContext
342
750
  }) {
751
+ const baseUrl = resolveApiUrl(apiUrl);
343
752
  const userId = userContext?.userId;
344
753
  const persistKey = persist ? userId ? `wallavi_${agentId}_${userId}` : `wallavi_${agentId}` : null;
345
754
  const onNavigateRef = react.useRef(onNavigate);
755
+ const resolvePageContextRef = react.useRef(resolvePageContext);
756
+ const copilotColorRef = react.useRef(copilotColor);
346
757
  react.useEffect(() => {
347
758
  onNavigateRef.current = onNavigate;
759
+ resolvePageContextRef.current = resolvePageContext;
760
+ copilotColorRef.current = copilotColor;
348
761
  });
349
762
  const [messages, setMessages] = react.useState(() => {
350
763
  if (!persistKey || typeof window === "undefined") return [];
@@ -438,7 +851,11 @@ function useChat({
438
851
  return;
439
852
  }
440
853
  if (proto.type === "client-action") {
441
- executeDeclarativeSteps(proto.steps, onNavigateRef.current);
854
+ executeDeclarativeSteps(
855
+ proto.steps,
856
+ onNavigateRef.current,
857
+ copilotColorRef.current
858
+ );
442
859
  return;
443
860
  }
444
861
  if (proto.type === "debug-trace") {
@@ -457,7 +874,9 @@ function useChat({
457
874
  const { input: userInput, msgId, extraMetadata, attachments } = opts;
458
875
  const isPrivate = Boolean(workspaceId);
459
876
  const token = isPrivate ? await getFreshClerkToken() : null;
460
- const url = isPrivate ? `${API_URL2}/api/threads/${threadId}/stream` : `${API_URL2}/api/chat/stream`;
877
+ const hostHeaders = await resolveUserHeaders(userContext);
878
+ const pageContext = userContext?.pageContext ?? resolvePageContextRef.current?.();
879
+ const url = isPrivate ? `${baseUrl}/api/threads/${threadId}/stream` : `${baseUrl}/api/chat/stream`;
461
880
  const res = await fetch(url, {
462
881
  method: "POST",
463
882
  headers: {
@@ -478,10 +897,10 @@ function useChat({
478
897
  ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
479
898
  userMetadata: {
480
899
  ...userContext?.metadata ?? {},
481
- ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
900
+ ...pageContext ? { pageContext } : {},
482
901
  headers: {
483
902
  ...token ? { Authorization: `Bearer ${token}` } : {},
484
- ...userContext?.headers ?? {},
903
+ ...hostHeaders,
485
904
  ...userContext?.metadata?.headers ?? {}
486
905
  },
487
906
  ...extraMetadata ?? {}
@@ -526,6 +945,7 @@ function useChat({
526
945
  }
527
946
  },
528
947
  [
948
+ baseUrl,
529
949
  agentId,
530
950
  workspaceId,
531
951
  envId,
@@ -540,7 +960,8 @@ function useChat({
540
960
  agentId,
541
961
  threadId,
542
962
  workspaceId,
543
- customBackend
963
+ customBackend,
964
+ apiUrl: baseUrl
544
965
  });
545
966
  react.useEffect(() => {
546
967
  if (customBackend || !persistKey) return;
@@ -589,7 +1010,7 @@ function useChat({
589
1010
  try {
590
1011
  const isPrivate = Boolean(workspaceId);
591
1012
  const token = isPrivate ? await getFreshClerkToken() : null;
592
- const url = isPrivate ? `${API_URL2}/api/threads/${threadId}/messages` : `${API_URL2}/api/chat/messages?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
1013
+ const url = isPrivate ? `${baseUrl}/api/threads/${threadId}/messages` : `${baseUrl}/api/chat/messages?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
593
1014
  const res = await fetch(url, {
594
1015
  headers: {
595
1016
  ...token ? { Authorization: `Bearer ${token}` } : {}
@@ -653,7 +1074,15 @@ function useChat({
653
1074
  }
654
1075
  }
655
1076
  }
656
- }, [threadId, agentId, workspaceId, envId, persistKey, fetchAndStream]);
1077
+ }, [
1078
+ baseUrl,
1079
+ threadId,
1080
+ agentId,
1081
+ workspaceId,
1082
+ envId,
1083
+ persistKey,
1084
+ fetchAndStream
1085
+ ]);
657
1086
  const reset = react.useCallback(() => {
658
1087
  setMessages([]);
659
1088
  setInput("");
@@ -837,106 +1266,6 @@ function useChat({
837
1266
  setSelectedContext
838
1267
  };
839
1268
  }
840
- async function waitForElement(selector, retries = 5, delayMs = 100) {
841
- const cleanId = selector.startsWith("#") ? selector.substring(1) : selector;
842
- for (let i = 0; i < retries; i++) {
843
- try {
844
- const el = document.querySelector(selector);
845
- if (el) return el;
846
- } catch (e) {
847
- }
848
- try {
849
- const byId = document.getElementById(cleanId);
850
- if (byId) return byId;
851
- } catch (e) {
852
- }
853
- if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[")) {
854
- try {
855
- const byPrependedId = document.querySelector(`#${selector}`);
856
- if (byPrependedId) return byPrependedId;
857
- } catch (e) {
858
- }
859
- }
860
- await new Promise((resolve) => setTimeout(resolve, delayMs));
861
- }
862
- return null;
863
- }
864
- async function executeDeclarativeSteps(steps, onNavigate) {
865
- if (typeof window === "undefined" || !Array.isArray(steps)) return;
866
- for (const step of steps) {
867
- try {
868
- const { action, selector, value, payload } = step;
869
- switch (action) {
870
- case "scroll_into_view": {
871
- if (!selector) break;
872
- const el = await waitForElement(selector);
873
- el?.scrollIntoView({ behavior: "smooth", block: "center" });
874
- break;
875
- }
876
- case "click": {
877
- if (!selector) break;
878
- const el = await waitForElement(selector);
879
- el?.click();
880
- break;
881
- }
882
- case "focus": {
883
- if (!selector) break;
884
- const el = await waitForElement(selector);
885
- el?.focus();
886
- break;
887
- }
888
- case "add_class": {
889
- if (!selector || !value) break;
890
- const el = await waitForElement(selector);
891
- if (el) el.classList.add(...value.split(/\s+/).filter(Boolean));
892
- break;
893
- }
894
- case "remove_class": {
895
- if (!selector || !value) break;
896
- const el = await waitForElement(selector);
897
- if (el) el.classList.remove(...value.split(/\s+/).filter(Boolean));
898
- break;
899
- }
900
- case "fill_value": {
901
- if (!selector) break;
902
- const el = await waitForElement(selector);
903
- if (el) {
904
- el.value = value ?? "";
905
- el.dispatchEvent(new Event("input", { bubbles: true }));
906
- el.dispatchEvent(new Event("change", { bubbles: true }));
907
- }
908
- break;
909
- }
910
- case "dispatch_event": {
911
- if (!value) break;
912
- const detail = payload ?? {};
913
- window.dispatchEvent(new CustomEvent(value, { detail }));
914
- break;
915
- }
916
- case "navigate": {
917
- if (value) {
918
- if (onNavigate) {
919
- onNavigate(value);
920
- } else {
921
- window.location.href = value;
922
- }
923
- }
924
- break;
925
- }
926
- default:
927
- console.warn(
928
- `[Wallavi Widget] Unknown declarative action: ${action}`
929
- );
930
- }
931
- } catch (err) {
932
- console.error(
933
- "[Wallavi Widget] Failed to execute declarative step:",
934
- step,
935
- err
936
- );
937
- }
938
- }
939
- }
940
1269
  function getPreferredMimeType() {
941
1270
  if (typeof MediaRecorder === "undefined") return "";
942
1271
  const candidates = [
@@ -953,7 +1282,6 @@ function mimeTypeToExtension(mimeType) {
953
1282
  if (mimeType.includes("mp4")) return "mp4";
954
1283
  return "webm";
955
1284
  }
956
- var DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
957
1285
  function useVoice({
958
1286
  agentId,
959
1287
  apiUrl,
@@ -966,7 +1294,7 @@ function useVoice({
966
1294
  const streamRef = react.useRef(null);
967
1295
  const errorTimerRef = react.useRef(null);
968
1296
  const isSupported = typeof window !== "undefined" && typeof MediaRecorder !== "undefined" && !!navigator?.mediaDevices?.getUserMedia;
969
- const base = apiUrl ?? DEFAULT_API_URL;
1297
+ const base = resolveApiUrl(apiUrl);
970
1298
  const transcribeBlob = react.useCallback(
971
1299
  async (blob, mimeType) => {
972
1300
  setVoiceState("transcribing");
@@ -1045,7 +1373,6 @@ function useVoice({
1045
1373
  }, []);
1046
1374
  return { voiceState, isSupported, start, stop };
1047
1375
  }
1048
- var DEFAULT_API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
1049
1376
  function makeId() {
1050
1377
  return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
1051
1378
  }
@@ -1055,7 +1382,7 @@ function useAttachments({
1055
1382
  maxFiles = 5
1056
1383
  }) {
1057
1384
  const [attachments, setAttachments] = react.useState([]);
1058
- const base = apiUrl ?? DEFAULT_API_URL2;
1385
+ const base = resolveApiUrl(apiUrl);
1059
1386
  const uploadOne = react.useCallback(
1060
1387
  async (file, id) => {
1061
1388
  const form = new FormData();
@@ -2850,6 +3177,20 @@ function VoiceOverlay({
2850
3177
  }
2851
3178
  ) });
2852
3179
  }
3180
+
3181
+ // src/lib/unique-keys.ts
3182
+ function uniqueKeys(items, baseKey) {
3183
+ const seen = /* @__PURE__ */ new Map();
3184
+ return items.map((item) => {
3185
+ const base = baseKey(item);
3186
+ const count = seen.get(base) ?? 0;
3187
+ seen.set(base, count + 1);
3188
+ return count === 0 ? base : `${base}#${count}`;
3189
+ });
3190
+ }
3191
+ var actionKey = (action) => `${action.name}:${JSON.stringify(action.steps ?? [])}`;
3192
+ var docKey = (doc) => `${doc.name}:${doc.url ?? ""}`;
3193
+ var topicKey = (topic) => topic.name;
2853
3194
  function SvgIcon({
2854
3195
  className,
2855
3196
  strokeWidth = "1.8",
@@ -2923,11 +3264,11 @@ function urlRelevance(docUrl, pageUrl) {
2923
3264
  return 50;
2924
3265
  const docParts = docClean.split(/[/?#]/).filter(Boolean);
2925
3266
  const pageParts = pageClean.split(/[/?#]/).filter(Boolean);
2926
- let score = 0;
3267
+ let score2 = 0;
2927
3268
  for (const part of pageParts) {
2928
- if (docParts.includes(part)) score += 10;
3269
+ if (docParts.includes(part)) score2 += 10;
2929
3270
  }
2930
- return score;
3271
+ return score2;
2931
3272
  }
2932
3273
  function topicToQuestion(topic, locale) {
2933
3274
  const name = topic.name.trim();
@@ -3018,9 +3359,9 @@ function getContextKeywords(pageUrl, pageTitle, pageParams, pageVars, metadata)
3018
3359
  return Array.from(keywords);
3019
3360
  }
3020
3361
  function computeRelevanceScore(item, keywords, pageUrl) {
3021
- let score = 0;
3362
+ let score2 = 0;
3022
3363
  if (item.url && pageUrl) {
3023
- score += urlRelevance(item.url, pageUrl);
3364
+ score2 += urlRelevance(item.url, pageUrl);
3024
3365
  }
3025
3366
  const itemName = (item.name || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
3026
3367
  const itemDesc = (item.description || item.summary || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -3028,33 +3369,41 @@ function computeRelevanceScore(item, keywords, pageUrl) {
3028
3369
  const nameWords = itemName.split(/[^a-z0-9]/).filter(Boolean);
3029
3370
  for (const keyword of keywords) {
3030
3371
  if (itemName.includes(keyword)) {
3031
- score += 30;
3372
+ score2 += 30;
3032
3373
  if (nameWords.includes(keyword)) {
3033
- score += 20;
3374
+ score2 += 20;
3034
3375
  }
3035
3376
  }
3036
3377
  if (itemDesc.includes(keyword)) {
3037
- score += 15;
3378
+ score2 += 15;
3038
3379
  }
3039
3380
  if (itemUrl.includes(keyword)) {
3040
- score += 20;
3381
+ score2 += 20;
3041
3382
  }
3042
3383
  }
3043
- if (item.keywords) {
3044
- const allItemKeywords = [
3045
- ...item.keywords.en || [],
3046
- ...item.keywords.es || [],
3047
- ...item.keywords.fr || []
3048
- ].map(
3384
+ if (item.keywords && typeof item.keywords === "object") {
3385
+ const rawList = [];
3386
+ if (Array.isArray(item.keywords)) {
3387
+ rawList.push(...item.keywords);
3388
+ } else {
3389
+ const kwRecord = item.keywords;
3390
+ for (const lang of ["en", "es", "fr"]) {
3391
+ const langVal = kwRecord[lang];
3392
+ if (Array.isArray(langVal)) {
3393
+ rawList.push(...langVal);
3394
+ }
3395
+ }
3396
+ }
3397
+ const allItemKeywords = rawList.filter((k) => typeof k === "string" && k.trim().length > 0).map(
3049
3398
  (k) => k.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
3050
3399
  );
3051
3400
  for (const keyword of keywords) {
3052
3401
  if (allItemKeywords.includes(keyword)) {
3053
- score += 45;
3402
+ score2 += 45;
3054
3403
  }
3055
3404
  }
3056
3405
  }
3057
- return score;
3406
+ return score2;
3058
3407
  }
3059
3408
  var STAGGER_MS = 50;
3060
3409
  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";
@@ -3267,6 +3616,9 @@ function CommandPanel({
3267
3616
  }));
3268
3617
  return scored.sort((a, b) => b.score - a.score);
3269
3618
  }, [clientActions, pageUrl, pageTitle, pageParams, pageVars, metadata]);
3619
+ const actionKeys = uniqueKeys(sortedActions, actionKey);
3620
+ const docKeys = uniqueKeys(relevantDocs, docKey);
3621
+ const topicKeys = uniqueKeys(topicQuestions, topicKey);
3270
3622
  const hasActions = sortedActions.length > 0;
3271
3623
  const hasDocs = relevantDocs.length > 0;
3272
3624
  const hasTopics = topicQuestions.length > 0;
@@ -3288,7 +3640,7 @@ function CommandPanel({
3288
3640
  accentColor,
3289
3641
  onExecute: () => handleExecute(action.steps)
3290
3642
  },
3291
- action.name
3643
+ actionKeys[i]
3292
3644
  )) })
3293
3645
  ] }),
3294
3646
  hasDocs && /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
@@ -3301,7 +3653,7 @@ function CommandPanel({
3301
3653
  onSend,
3302
3654
  locale
3303
3655
  },
3304
- `${doc.name}-${doc.url ?? i}`
3656
+ docKeys[i]
3305
3657
  )) })
3306
3658
  ] }),
3307
3659
  hasTopics && /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
@@ -3313,7 +3665,7 @@ function CommandPanel({
3313
3665
  index: i,
3314
3666
  onSend
3315
3667
  },
3316
- topic.name
3668
+ topicKeys[i]
3317
3669
  )) })
3318
3670
  ] })
3319
3671
  ] }),
@@ -3360,7 +3712,7 @@ function CommandPanel({
3360
3712
  accentColor,
3361
3713
  onExecute: () => handleExecute(action.steps)
3362
3714
  },
3363
- action.name
3715
+ actionKeys[i]
3364
3716
  )) })
3365
3717
  ] }),
3366
3718
  hasDocs && /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
@@ -3373,7 +3725,7 @@ function CommandPanel({
3373
3725
  onSend,
3374
3726
  locale
3375
3727
  },
3376
- `${doc.name}-${doc.url ?? i}`
3728
+ docKeys[i]
3377
3729
  )) })
3378
3730
  ] }),
3379
3731
  hasTopics && /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
@@ -3386,13 +3738,14 @@ function CommandPanel({
3386
3738
  compact: true,
3387
3739
  onSend
3388
3740
  },
3389
- topic.name
3741
+ topicKeys[i]
3390
3742
  )) })
3391
3743
  ] })
3392
3744
  ] });
3393
3745
  }
3394
3746
  function ChatWidget({
3395
3747
  agentId,
3748
+ apiUrl,
3396
3749
  workspaceId,
3397
3750
  agentName,
3398
3751
  displayName,
@@ -3453,8 +3806,10 @@ function ChatWidget({
3453
3806
  window.addEventListener("message", handleMessage);
3454
3807
  return () => window.removeEventListener("message", handleMessage);
3455
3808
  }, []);
3809
+ const resolvePageContext = () => userContext?.pageContext ?? (typeof window === "undefined" ? void 0 : derivePageContext(window.location, clientActions));
3456
3810
  const chat = useChat({
3457
3811
  agentId,
3812
+ apiUrl,
3458
3813
  workspaceId,
3459
3814
  envId,
3460
3815
  source,
@@ -3462,10 +3817,13 @@ function ChatWidget({
3462
3817
  persist,
3463
3818
  onNavigate,
3464
3819
  playgroundOverrides,
3465
- customBackend
3820
+ customBackend,
3821
+ copilotColor: userMessageColor,
3822
+ resolvePageContext: () => resolvePageContext()
3466
3823
  });
3467
3824
  const voice = useVoice({
3468
3825
  agentId,
3826
+ apiUrl,
3469
3827
  onTranscript: (text) => {
3470
3828
  if (voiceAutoSend) {
3471
3829
  void chat.send(text);
@@ -3474,7 +3832,7 @@ function ChatWidget({
3474
3832
  }
3475
3833
  }
3476
3834
  });
3477
- const attachmentHook = useAttachments({ agentId });
3835
+ const attachmentHook = useAttachments({ agentId, apiUrl });
3478
3836
  const debugTraceLenRef = react.useRef(0);
3479
3837
  react.useEffect(() => {
3480
3838
  if (!onDebugTrace || chat.debugTraces.length <= debugTraceLenRef.current)
@@ -3574,9 +3932,9 @@ function ChatWidget({
3574
3932
  const showInlineCommandPanel = showCommandPanel && !showSidebar && chat.messages.length === 0 && (showQuickActions && clientActions.length > 0 || showRagTopics && ragTopics.length > 0 || showRagDocuments && ragDocuments.length > 0);
3575
3933
  const handleExecuteAction = react.useCallback(
3576
3934
  (steps) => {
3577
- executeDeclarativeSteps(steps, onNavigate);
3935
+ executeDeclarativeSteps(steps, onNavigate, userMessageColor);
3578
3936
  },
3579
- [onNavigate]
3937
+ [onNavigate, userMessageColor]
3580
3938
  );
3581
3939
  const [activeActionIndex, setActiveActionIndex] = react.useState(-1);
3582
3940
  const query = chat.input.toLowerCase().trim();
@@ -3633,11 +3991,12 @@ function ChatWidget({
3633
3991
  },
3634
3992
  [matchingActions, activeActionIndex, handleExecuteAction, chat]
3635
3993
  );
3994
+ const pageContext = resolvePageContext();
3636
3995
  const commandPanelProps = {
3637
- pageUrl: userContext?.pageContext?.url,
3638
- pageTitle: userContext?.pageContext?.title,
3639
- pageParams: userContext?.pageContext?.params,
3640
- pageVars: userContext?.pageContext?.vars,
3996
+ pageUrl: pageContext?.url,
3997
+ pageTitle: pageContext?.title,
3998
+ pageParams: pageContext?.params,
3999
+ pageVars: pageContext?.vars,
3641
4000
  metadata: userContext?.metadata,
3642
4001
  clientActions: showQuickActions ? clientActions : [],
3643
4002
  ragTopics: showRagTopics ? ragTopics : [],
@@ -3957,7 +4316,6 @@ function ChatWidget({
3957
4316
  }
3958
4317
  );
3959
4318
  }
3960
- var WALLAVI_PUBLIC_API = "https://wallavi-production.up.railway.app";
3961
4319
  var EMPTY = {
3962
4320
  remoteConfig: {},
3963
4321
  bubbleIconUrl: void 0,
@@ -3971,7 +4329,37 @@ var EMPTY = {
3971
4329
  panelHeight: 580,
3972
4330
  loading: false
3973
4331
  };
3974
- function useAutoConfig(agentId, enabled) {
4332
+ function toChatProps(cfg) {
4333
+ const remote = {};
4334
+ const set = (key, value) => {
4335
+ if (value !== null && value !== void 0) remote[key] = value;
4336
+ };
4337
+ set("agentName", cfg.agentName);
4338
+ set("profilePicture", cfg.profilePicture);
4339
+ set("displayName", cfg.displayName);
4340
+ set("theme", cfg.theme);
4341
+ set("userMessageColor", cfg.userMessageColor);
4342
+ if (cfg.initialMessages?.length) remote.initialMessages = cfg.initialMessages;
4343
+ if (cfg.suggestedMessages?.length)
4344
+ remote.suggestedMessages = cfg.suggestedMessages;
4345
+ set("messagePlaceholder", cfg.messagePlaceholder);
4346
+ set("watermark", cfg.watermark);
4347
+ set("footer", cfg.footer);
4348
+ set("showThinking", cfg.showThinking);
4349
+ set("regenerateMessage", cfg.regenerateMessage);
4350
+ set("widgetLayout", cfg.widgetLayout);
4351
+ set("enableVoice", cfg.enableVoice);
4352
+ set("showCommandPanel", cfg.showCommandPanel);
4353
+ set("showRagTopics", cfg.showRagTopics);
4354
+ set("showRagDocuments", cfg.showRagDocuments);
4355
+ set("showQuickActions", cfg.showQuickActions);
4356
+ set("clientActions", cfg.clientActions);
4357
+ set("ragTopics", cfg.ragTopics);
4358
+ set("ragDocuments", cfg.ragDocuments);
4359
+ set("locale", cfg.locale);
4360
+ return remote;
4361
+ }
4362
+ function useAutoConfig(agentId, enabled, apiUrl) {
3975
4363
  const [result, setResult] = react.useState(() => ({
3976
4364
  ...EMPTY,
3977
4365
  loading: enabled && Boolean(agentId)
@@ -3982,61 +4370,34 @@ function useAutoConfig(agentId, enabled) {
3982
4370
  return;
3983
4371
  }
3984
4372
  let cancelled = false;
3985
- fetch(`${WALLAVI_PUBLIC_API}/api/public/widget/${agentId}`).then((r) => r.json()).then((body) => {
4373
+ setResult({ ...EMPTY, loading: true });
4374
+ fetch(`${resolveApiUrl(apiUrl)}/api/public/widget/${agentId}`).then((r) => r.json()).then((body) => {
3986
4375
  if (cancelled) return;
3987
- const cfg = body?.data ?? {};
3988
- const remote = {};
3989
- if (cfg.profilePicture != null)
3990
- remote.profilePicture = cfg.profilePicture;
3991
- if (cfg.displayName != null)
3992
- remote.displayName = cfg.displayName;
3993
- if (cfg.theme) remote.theme = cfg.theme;
3994
- if (cfg.userMessageColor)
3995
- remote.userMessageColor = cfg.userMessageColor;
3996
- if (Array.isArray(cfg.initialMessages) && cfg.initialMessages.length > 0)
3997
- remote.initialMessages = cfg.initialMessages;
3998
- if (Array.isArray(cfg.suggestedMessages))
3999
- remote.suggestedMessages = cfg.suggestedMessages;
4000
- if (cfg.messagePlaceholder != null)
4001
- remote.messagePlaceholder = cfg.messagePlaceholder;
4002
- if (cfg.watermark != null) remote.watermark = cfg.watermark;
4003
- if (cfg.footer != null) remote.footer = cfg.footer;
4004
- if (cfg.showThinking != null)
4005
- remote.showThinking = cfg.showThinking;
4006
- if (cfg.regenerateMessage != null)
4007
- remote.regenerateMessage = cfg.regenerateMessage;
4008
- if (cfg.widgetLayout)
4009
- remote.widgetLayout = cfg.widgetLayout;
4010
- if (cfg.enableVoice != null)
4011
- remote.enableVoice = cfg.enableVoice;
4012
- if (cfg.clientActions)
4013
- remote.clientActions = cfg.clientActions;
4014
- if (Array.isArray(cfg.ragTopics))
4015
- remote.ragTopics = cfg.ragTopics;
4016
- if (Array.isArray(cfg.ragDocuments))
4017
- remote.ragDocuments = cfg.ragDocuments;
4018
- if (cfg.locale != null)
4019
- remote.locale = cfg.locale;
4376
+ const cfg = body?.data;
4377
+ if (!cfg) {
4378
+ setResult(EMPTY);
4379
+ return;
4380
+ }
4020
4381
  setResult({
4021
- remoteConfig: remote,
4382
+ remoteConfig: toChatProps(cfg),
4022
4383
  bubbleIconUrl: cfg.chatIcon || cfg.profilePicture || void 0,
4023
4384
  autoOpen: Boolean(cfg.autoOpen),
4024
4385
  keyboardShortcut: Boolean(cfg.keyboardShortcut),
4025
4386
  position: cfg.alignChatBubbleButton === "left" ? "bottom-left" : "bottom-right",
4026
4387
  widgetLayout: cfg.widgetLayout === "center" ? "center" : "bubble",
4027
- clientActions: Array.isArray(cfg.clientActions) ? cfg.clientActions : [],
4028
- bubbleSize: typeof cfg.bubbleSize === "number" ? cfg.bubbleSize : 52,
4029
- panelWidth: typeof cfg.panelWidth === "number" ? cfg.panelWidth : 360,
4030
- panelHeight: typeof cfg.panelHeight === "number" ? cfg.panelHeight : 580,
4388
+ clientActions: cfg.clientActions ?? [],
4389
+ bubbleSize: cfg.bubbleSize ?? 52,
4390
+ panelWidth: cfg.panelWidth ?? 360,
4391
+ panelHeight: cfg.panelHeight ?? 580,
4031
4392
  loading: false
4032
4393
  });
4033
4394
  }).catch(() => {
4034
- if (!cancelled) setResult((r) => ({ ...r, loading: false }));
4395
+ if (!cancelled) setResult(EMPTY);
4035
4396
  });
4036
4397
  return () => {
4037
4398
  cancelled = true;
4038
4399
  };
4039
- }, [agentId, enabled]);
4400
+ }, [agentId, enabled, apiUrl]);
4040
4401
  return result;
4041
4402
  }
4042
4403
  function toWidgetMsg(m) {
@@ -4117,6 +4478,38 @@ function useSupportChat({
4117
4478
  },
4118
4479
  [base]
4119
4480
  );
4481
+ function loadAblyScript() {
4482
+ return new Promise((resolve, reject) => {
4483
+ if (typeof window === "undefined") {
4484
+ resolve(null);
4485
+ return;
4486
+ }
4487
+ if (window.Ably) {
4488
+ resolve(window.Ably);
4489
+ return;
4490
+ }
4491
+ const existing = document.querySelector("script[data-ably-sdk]");
4492
+ if (existing) {
4493
+ existing.addEventListener("load", () => resolve(window.Ably));
4494
+ existing.addEventListener(
4495
+ "error",
4496
+ () => reject(new Error("Ably CDN load failed"))
4497
+ );
4498
+ return;
4499
+ }
4500
+ const script = document.createElement("script");
4501
+ script.src = "https://cdn.ably.com/lib/ably.min-2.js";
4502
+ script.crossOrigin = "anonymous";
4503
+ script.async = true;
4504
+ script.setAttribute("data-ably-sdk", "true");
4505
+ script.onload = () => resolve(window.Ably);
4506
+ script.onerror = () => {
4507
+ script.remove();
4508
+ reject(new Error("Ably CDN load failed"));
4509
+ };
4510
+ document.head.appendChild(script);
4511
+ });
4512
+ }
4120
4513
  react.useEffect(() => {
4121
4514
  if (!enabled || !session) return;
4122
4515
  void loadMessages(session);
@@ -4131,7 +4524,8 @@ function useSupportChat({
4131
4524
  );
4132
4525
  if (!res.ok) return;
4133
4526
  const { tokenRequest, channel: channelName } = await res.json();
4134
- const AblyLib = (await import('ably')).default;
4527
+ const AblyLib = await loadAblyScript();
4528
+ if (!AblyLib) return;
4135
4529
  ablyClient = new AblyLib.Realtime({
4136
4530
  authCallback: (_, cb) => cb(null, tokenRequest)
4137
4531
  });
@@ -4439,9 +4833,9 @@ function BubbleWidget({
4439
4833
  height: heightProp,
4440
4834
  expandedWidth = 640,
4441
4835
  expandedHeight = "calc(100vh - 100px)",
4442
- keyboardShortcut: keyboardShortcutProp = false,
4836
+ keyboardShortcut: keyboardShortcutProp,
4443
4837
  shortcutKey = "k",
4444
- autoOpen: autoOpenProp = false,
4838
+ autoOpen: autoOpenProp,
4445
4839
  bubbleIconUrl: bubbleIconUrlProp,
4446
4840
  bubbleSize: bubbleSizeProp,
4447
4841
  panelClassName,
@@ -4482,24 +4876,25 @@ function BubbleWidget({
4482
4876
  }, []);
4483
4877
  const remote = useAutoConfig(
4484
4878
  inboxToken ? "" : chatProps.agentId ?? "",
4485
- !inboxToken && autoConfig
4879
+ !inboxToken && autoConfig,
4880
+ chatProps.apiUrl
4486
4881
  );
4487
- const resolvedPosition = remote.position ?? positionProp;
4488
- const resolvedLayout = remote.widgetLayout ?? chatProps.widgetLayout ?? "bubble";
4489
- const resolvedBubbleIcon = remote.bubbleIconUrl ?? bubbleIconUrlProp;
4490
- const resolvedAutoOpen = remote.autoOpen || autoOpenProp;
4491
- const resolvedKeyboardShortcut = remote.keyboardShortcut || keyboardShortcutProp;
4492
- const resolvedBubbleSize = remote.bubbleSize ?? bubbleSizeProp;
4493
- const resolvedWidth = remote.panelWidth ?? widthProp;
4494
- const resolvedHeight = remote.panelHeight ?? heightProp;
4882
+ const resolvedPosition = positionProp ?? remote.position;
4883
+ const resolvedLayout = chatProps.widgetLayout ?? remote.widgetLayout;
4884
+ const resolvedBubbleIcon = bubbleIconUrlProp ?? remote.bubbleIconUrl;
4885
+ const resolvedAutoOpen = autoOpenProp ?? remote.autoOpen;
4886
+ const resolvedKeyboardShortcut = keyboardShortcutProp ?? remote.keyboardShortcut;
4887
+ const resolvedBubbleSize = bubbleSizeProp ?? remote.bubbleSize;
4888
+ const resolvedWidth = widthProp ?? remote.panelWidth;
4889
+ const resolvedHeight = heightProp ?? remote.panelHeight;
4495
4890
  const definedChatProps = Object.fromEntries(
4496
4891
  Object.entries(chatProps).filter(([, v]) => v !== void 0)
4497
4892
  );
4498
4893
  const mergedConfig = {
4499
- ...definedChatProps,
4500
4894
  ...remote.remoteConfig,
4895
+ ...definedChatProps,
4501
4896
  agentId: chatProps.agentId ?? "",
4502
- agentName: remote.remoteConfig.agentName ?? chatProps.agentName ?? "Asistente",
4897
+ agentName: chatProps.agentName ?? remote.remoteConfig.agentName ?? "Asistente",
4503
4898
  source: chatProps.source ?? remote.remoteConfig.source ?? "web"
4504
4899
  };
4505
4900
  const supportBackend = useSupportChat(
@@ -4515,6 +4910,10 @@ function BubbleWidget({
4515
4910
  react.useEffect(() => {
4516
4911
  setOpenRef.current = setOpen;
4517
4912
  });
4913
+ const openRef = react.useRef(open);
4914
+ react.useEffect(() => {
4915
+ openRef.current = open;
4916
+ });
4518
4917
  react.useEffect(() => {
4519
4918
  if (!resolvedAutoOpen || autoOpenedRef.current) return;
4520
4919
  const dismissedUntil = Number(localStorage.getItem(KEY_DISMISSED) ?? 0);
@@ -4524,15 +4923,29 @@ function BubbleWidget({
4524
4923
  }
4525
4924
  }, [resolvedAutoOpen]);
4526
4925
  react.useEffect(() => {
4527
- if (!resolvedKeyboardShortcut) return;
4926
+ const onToggle = () => setOpenRef.current((v) => !v);
4927
+ const onOpen = () => setOpenRef.current(true);
4928
+ const onClose = () => setOpenRef.current(false);
4929
+ window.addEventListener("wallavi:toggle-assistant", onToggle);
4930
+ window.addEventListener("wallavi:open-assistant", onOpen);
4931
+ window.addEventListener("wallavi:close-assistant", onClose);
4528
4932
  const onKey = (e) => {
4529
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey.toLowerCase()) {
4933
+ if (resolvedKeyboardShortcut && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey.toLowerCase()) {
4530
4934
  e.preventDefault();
4531
4935
  setOpenRef.current((v) => !v);
4936
+ } else if (e.key === "Escape" && openRef.current) {
4937
+ e.preventDefault();
4938
+ e.stopPropagation();
4939
+ setOpenRef.current(false);
4532
4940
  }
4533
4941
  };
4534
4942
  window.addEventListener("keydown", onKey);
4535
- return () => window.removeEventListener("keydown", onKey);
4943
+ return () => {
4944
+ window.removeEventListener("wallavi:toggle-assistant", onToggle);
4945
+ window.removeEventListener("wallavi:open-assistant", onOpen);
4946
+ window.removeEventListener("wallavi:close-assistant", onClose);
4947
+ window.removeEventListener("keydown", onKey);
4948
+ };
4536
4949
  }, [resolvedKeyboardShortcut, shortcutKey]);
4537
4950
  react.useEffect(() => {
4538
4951
  if (!open) return;
@@ -4587,7 +5000,8 @@ function BubbleWidget({
4587
5000
  display: "flex",
4588
5001
  flexDirection: "column",
4589
5002
  alignItems: isLeft ? "flex-start" : "flex-end",
4590
- gap: 12
5003
+ gap: 12,
5004
+ pointerEvents: !open && hideBubble ? "none" : void 0
4591
5005
  },
4592
5006
  children: [
4593
5007
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4604,12 +5018,14 @@ function BubbleWidget({
4604
5018
  zIndex: 9999,
4605
5019
  width: panelWidth,
4606
5020
  height: panelHeight,
4607
- transition: "width 0.3s ease, height 0.3s ease"
5021
+ transition: "width 0.3s ease, height 0.3s ease",
5022
+ pointerEvents: "auto"
4608
5023
  } : {
4609
5024
  display: open ? "block" : "none",
4610
5025
  width: panelWidth,
4611
5026
  height: panelHeight,
4612
- transition: "width 0.3s ease, height 0.3s ease"
5027
+ transition: "width 0.3s ease, height 0.3s ease",
5028
+ pointerEvents: "auto"
4613
5029
  },
4614
5030
  children: /* @__PURE__ */ jsxRuntime.jsx(
4615
5031
  ChatWidget,