@wallavi/widget 1.13.1 → 1.13.3

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.d.mts CHANGED
@@ -174,8 +174,10 @@ interface ChatWidgetConfig {
174
174
  showThinking?: boolean;
175
175
  regenerateMessage?: boolean;
176
176
  /**
177
- * Persist messages and threadId in sessionStorage so the conversation
178
- * survives page reloads and navigations. Uses agentId as the storage key.
177
+ * Persist the conversation so it survives page reloads and navigations.
178
+ * The storage key is `wallavi_<agentId>_<userId>` when `userContext.userId`
179
+ * is set, otherwise `wallavi_<agentId>`. Messages go in sessionStorage under
180
+ * `<key>_msgs` and the threadId in localStorage under `<key>_tid`.
179
181
  */
180
182
  persist?: boolean;
181
183
  onNavigate?: (path: string) => void;
@@ -356,7 +358,10 @@ interface CommandPanelProps {
356
358
  ragTopics?: RagTopicInfo[];
357
359
  ragDocuments?: RagDocumentInfo[];
358
360
  onSend: (text: string) => void;
359
- onExecuteAction?: (steps: any[]) => void;
361
+ onExecuteAction?: (action: {
362
+ name: string;
363
+ steps: any[];
364
+ }) => void;
360
365
  accentColor?: string;
361
366
  isDark?: boolean;
362
367
  watermark?: boolean;
package/dist/index.d.ts CHANGED
@@ -174,8 +174,10 @@ interface ChatWidgetConfig {
174
174
  showThinking?: boolean;
175
175
  regenerateMessage?: boolean;
176
176
  /**
177
- * Persist messages and threadId in sessionStorage so the conversation
178
- * survives page reloads and navigations. Uses agentId as the storage key.
177
+ * Persist the conversation so it survives page reloads and navigations.
178
+ * The storage key is `wallavi_<agentId>_<userId>` when `userContext.userId`
179
+ * is set, otherwise `wallavi_<agentId>`. Messages go in sessionStorage under
180
+ * `<key>_msgs` and the threadId in localStorage under `<key>_tid`.
179
181
  */
180
182
  persist?: boolean;
181
183
  onNavigate?: (path: string) => void;
@@ -356,7 +358,10 @@ interface CommandPanelProps {
356
358
  ragTopics?: RagTopicInfo[];
357
359
  ragDocuments?: RagDocumentInfo[];
358
360
  onSend: (text: string) => void;
359
- onExecuteAction?: (steps: any[]) => void;
361
+ onExecuteAction?: (action: {
362
+ name: string;
363
+ steps: any[];
364
+ }) => void;
360
365
  accentColor?: string;
361
366
  isDark?: boolean;
362
367
  watermark?: boolean;
package/dist/index.js CHANGED
@@ -351,6 +351,55 @@ function applyUiEventToMessages(prev, proto, msgId) {
351
351
  return copy;
352
352
  }
353
353
 
354
+ // src/core/quick-action.ts
355
+ var BRACES = /\{\{(?:toolInput\.)?([a-zA-Z0-9_]+)\}\}|\{([a-zA-Z0-9_]+)\}/g;
356
+ var ROUTE = /\{\{(?:toolInput\.)?([a-zA-Z0-9_]+)\}\}|\{([a-zA-Z0-9_]+)\}|\[([a-zA-Z0-9_]+)\]|(?<=^|\/):([a-zA-Z0-9_]+)(?=[/?#]|$)/g;
357
+ function fill(text, pattern, sources, missing) {
358
+ return text.replace(pattern, (match, ...groups) => {
359
+ const key = groups.find((g) => typeof g === "string");
360
+ if (!key) return match;
361
+ for (const source of sources) {
362
+ const value = source?.[key];
363
+ if (typeof value === "string" && value !== "") return value;
364
+ if (typeof value === "number") return String(value);
365
+ }
366
+ missing?.add(key);
367
+ return match;
368
+ });
369
+ }
370
+ function fillPayload(value, sources) {
371
+ if (typeof value === "string") return fill(value, BRACES, sources);
372
+ if (Array.isArray(value)) return value.map((v) => fillPayload(v, sources));
373
+ if (value && typeof value === "object") {
374
+ return Object.fromEntries(
375
+ Object.entries(value).map(([k, v]) => [k, fillPayload(v, sources)])
376
+ );
377
+ }
378
+ return value;
379
+ }
380
+ function planQuickAction(steps, pageParams, metadata) {
381
+ const sources = [pageParams, metadata];
382
+ const missing = /* @__PURE__ */ new Set();
383
+ const filled = steps.map((original) => {
384
+ const step = original;
385
+ const out = { ...step };
386
+ if (typeof step.value === "string") {
387
+ out.value = step.action === "navigate" ? fill(step.value, ROUTE, sources, missing) : fill(step.value, BRACES, sources);
388
+ }
389
+ if (typeof step.selector === "string") {
390
+ out.selector = fill(step.selector, BRACES, sources);
391
+ }
392
+ if (step.payload !== void 0) {
393
+ out.payload = fillPayload(step.payload, sources);
394
+ }
395
+ return out;
396
+ });
397
+ return missing.size > 0 ? { kind: "ask_agent", missing: [...missing] } : { kind: "run", steps: filled };
398
+ }
399
+ function hasRoutePlaceholder(url) {
400
+ return new RegExp(ROUTE.source).test(url);
401
+ }
402
+
354
403
  // src/core/declarative-actions.ts
355
404
  function sanitizeSelector(selector) {
356
405
  if (!selector || typeof selector !== "string") return null;
@@ -625,6 +674,12 @@ async function executeDeclarativeSteps(steps, onNavigate, copilotColor) {
625
674
  }
626
675
  case "navigate": {
627
676
  const url = sanitizeUrl(step.value);
677
+ if (url && hasRoutePlaceholder(url)) {
678
+ console.warn(
679
+ "[Wallavi Widget] Blocked navigate with unfilled route params"
680
+ );
681
+ break;
682
+ }
628
683
  if (url) {
629
684
  if (onNavigate) {
630
685
  onNavigate(url);
@@ -1102,7 +1157,6 @@ function useChat({
1102
1157
  if (persistKey) {
1103
1158
  try {
1104
1159
  sessionStorage.removeItem(`${persistKey}_msgs`);
1105
- sessionStorage.removeItem(`${persistKey}_tid`);
1106
1160
  localStorage.removeItem(`${persistKey}_tid`);
1107
1161
  } catch {
1108
1162
  }
@@ -3632,7 +3686,7 @@ function CommandPanel({
3632
3686
  const hasContent = hasActions || hasDocs || hasTopics;
3633
3687
  if (!hasContent) return null;
3634
3688
  const isSidebar = mode === "sidebar";
3635
- const handleExecute = (steps) => onExecuteAction?.(steps);
3689
+ const handleExecute = (action) => onExecuteAction?.(action);
3636
3690
  if (isSidebar) {
3637
3691
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ww-w-[300px] ww-shrink-0 ww-border-l ww-border-border ww-bg-muted/50 ww-flex ww-flex-col ww-h-full ww-overflow-hidden ww-animate-in ww-fade-in ww-slide-in-from-right-2 ww-duration-300", children: [
3638
3692
  /* @__PURE__ */ jsxRuntime.jsx(PageBadge, { pageTitle, pageUrl, locale }),
@@ -3645,7 +3699,7 @@ function CommandPanel({
3645
3699
  action,
3646
3700
  index: i,
3647
3701
  accentColor,
3648
- onExecute: () => handleExecute(action.steps)
3702
+ onExecute: () => handleExecute(action)
3649
3703
  },
3650
3704
  actionKeys[i]
3651
3705
  )) })
@@ -3717,7 +3771,7 @@ function CommandPanel({
3717
3771
  action,
3718
3772
  index: i,
3719
3773
  accentColor,
3720
- onExecute: () => handleExecute(action.steps)
3774
+ onExecute: () => handleExecute(action)
3721
3775
  },
3722
3776
  actionKeys[i]
3723
3777
  )) })
@@ -3937,12 +3991,18 @@ function ChatWidget({
3937
3991
  const messagesList = isDefaultGreeting ? [getWidgetTranslation("defaultGreeting", locale)] : initialMessages;
3938
3992
  const showSidebar = showCommandPanel && widgetLayout === "center" && isDesktop;
3939
3993
  const showInlineCommandPanel = showCommandPanel && !showSidebar && chat.messages.length === 0 && (showQuickActions && clientActions.length > 0 || showRagTopics && ragTopics.length > 0 || showRagDocuments && ragDocuments.length > 0);
3940
- const handleExecuteAction = react.useCallback(
3941
- (steps) => {
3942
- executeDeclarativeSteps(steps, onNavigate, userMessageColor);
3943
- },
3944
- [onNavigate, userMessageColor]
3945
- );
3994
+ const handleExecuteAction = (action) => {
3995
+ const plan = planQuickAction(
3996
+ action.steps,
3997
+ resolvePageContext()?.params,
3998
+ userContext?.metadata
3999
+ );
4000
+ if (plan.kind === "run") {
4001
+ executeDeclarativeSteps(plan.steps, onNavigate, userMessageColor);
4002
+ } else {
4003
+ chat.send(action.name);
4004
+ }
4005
+ };
3946
4006
  const [activeActionIndex, setActiveActionIndex] = react.useState(-1);
3947
4007
  const query = chat.input.toLowerCase().trim();
3948
4008
  const matchingActions = react.useMemo(() => {
@@ -3988,7 +4048,7 @@ function ChatWidget({
3988
4048
  e.preventDefault();
3989
4049
  const selectedAction = matchingActions[activeActionIndex];
3990
4050
  if (selectedAction) {
3991
- handleExecuteAction(selectedAction.steps);
4051
+ handleExecuteAction(selectedAction);
3992
4052
  chat.setInput("");
3993
4053
  }
3994
4054
  } else if (e.key === "Escape") {
@@ -4223,7 +4283,7 @@ function ChatWidget({
4223
4283
  "button",
4224
4284
  {
4225
4285
  onClick: () => {
4226
- handleExecuteAction(action.steps);
4286
+ handleExecuteAction(action);
4227
4287
  chat.setInput("");
4228
4288
  },
4229
4289
  className: cn(
package/dist/index.mjs CHANGED
@@ -325,6 +325,55 @@ function applyUiEventToMessages(prev, proto, msgId) {
325
325
  return copy;
326
326
  }
327
327
 
328
+ // src/core/quick-action.ts
329
+ var BRACES = /\{\{(?:toolInput\.)?([a-zA-Z0-9_]+)\}\}|\{([a-zA-Z0-9_]+)\}/g;
330
+ var ROUTE = /\{\{(?:toolInput\.)?([a-zA-Z0-9_]+)\}\}|\{([a-zA-Z0-9_]+)\}|\[([a-zA-Z0-9_]+)\]|(?<=^|\/):([a-zA-Z0-9_]+)(?=[/?#]|$)/g;
331
+ function fill(text, pattern, sources, missing) {
332
+ return text.replace(pattern, (match, ...groups) => {
333
+ const key = groups.find((g) => typeof g === "string");
334
+ if (!key) return match;
335
+ for (const source of sources) {
336
+ const value = source?.[key];
337
+ if (typeof value === "string" && value !== "") return value;
338
+ if (typeof value === "number") return String(value);
339
+ }
340
+ missing?.add(key);
341
+ return match;
342
+ });
343
+ }
344
+ function fillPayload(value, sources) {
345
+ if (typeof value === "string") return fill(value, BRACES, sources);
346
+ if (Array.isArray(value)) return value.map((v) => fillPayload(v, sources));
347
+ if (value && typeof value === "object") {
348
+ return Object.fromEntries(
349
+ Object.entries(value).map(([k, v]) => [k, fillPayload(v, sources)])
350
+ );
351
+ }
352
+ return value;
353
+ }
354
+ function planQuickAction(steps, pageParams, metadata) {
355
+ const sources = [pageParams, metadata];
356
+ const missing = /* @__PURE__ */ new Set();
357
+ const filled = steps.map((original) => {
358
+ const step = original;
359
+ const out = { ...step };
360
+ if (typeof step.value === "string") {
361
+ out.value = step.action === "navigate" ? fill(step.value, ROUTE, sources, missing) : fill(step.value, BRACES, sources);
362
+ }
363
+ if (typeof step.selector === "string") {
364
+ out.selector = fill(step.selector, BRACES, sources);
365
+ }
366
+ if (step.payload !== void 0) {
367
+ out.payload = fillPayload(step.payload, sources);
368
+ }
369
+ return out;
370
+ });
371
+ return missing.size > 0 ? { kind: "ask_agent", missing: [...missing] } : { kind: "run", steps: filled };
372
+ }
373
+ function hasRoutePlaceholder(url) {
374
+ return new RegExp(ROUTE.source).test(url);
375
+ }
376
+
328
377
  // src/core/declarative-actions.ts
329
378
  function sanitizeSelector(selector) {
330
379
  if (!selector || typeof selector !== "string") return null;
@@ -599,6 +648,12 @@ async function executeDeclarativeSteps(steps, onNavigate, copilotColor) {
599
648
  }
600
649
  case "navigate": {
601
650
  const url = sanitizeUrl(step.value);
651
+ if (url && hasRoutePlaceholder(url)) {
652
+ console.warn(
653
+ "[Wallavi Widget] Blocked navigate with unfilled route params"
654
+ );
655
+ break;
656
+ }
602
657
  if (url) {
603
658
  if (onNavigate) {
604
659
  onNavigate(url);
@@ -1076,7 +1131,6 @@ function useChat({
1076
1131
  if (persistKey) {
1077
1132
  try {
1078
1133
  sessionStorage.removeItem(`${persistKey}_msgs`);
1079
- sessionStorage.removeItem(`${persistKey}_tid`);
1080
1134
  localStorage.removeItem(`${persistKey}_tid`);
1081
1135
  } catch {
1082
1136
  }
@@ -3606,7 +3660,7 @@ function CommandPanel({
3606
3660
  const hasContent = hasActions || hasDocs || hasTopics;
3607
3661
  if (!hasContent) return null;
3608
3662
  const isSidebar = mode === "sidebar";
3609
- const handleExecute = (steps) => onExecuteAction?.(steps);
3663
+ const handleExecute = (action) => onExecuteAction?.(action);
3610
3664
  if (isSidebar) {
3611
3665
  return /* @__PURE__ */ jsxs("div", { className: "ww-w-[300px] ww-shrink-0 ww-border-l ww-border-border ww-bg-muted/50 ww-flex ww-flex-col ww-h-full ww-overflow-hidden ww-animate-in ww-fade-in ww-slide-in-from-right-2 ww-duration-300", children: [
3612
3666
  /* @__PURE__ */ jsx(PageBadge, { pageTitle, pageUrl, locale }),
@@ -3619,7 +3673,7 @@ function CommandPanel({
3619
3673
  action,
3620
3674
  index: i,
3621
3675
  accentColor,
3622
- onExecute: () => handleExecute(action.steps)
3676
+ onExecute: () => handleExecute(action)
3623
3677
  },
3624
3678
  actionKeys[i]
3625
3679
  )) })
@@ -3691,7 +3745,7 @@ function CommandPanel({
3691
3745
  action,
3692
3746
  index: i,
3693
3747
  accentColor,
3694
- onExecute: () => handleExecute(action.steps)
3748
+ onExecute: () => handleExecute(action)
3695
3749
  },
3696
3750
  actionKeys[i]
3697
3751
  )) })
@@ -3911,12 +3965,18 @@ function ChatWidget({
3911
3965
  const messagesList = isDefaultGreeting ? [getWidgetTranslation("defaultGreeting", locale)] : initialMessages;
3912
3966
  const showSidebar = showCommandPanel && widgetLayout === "center" && isDesktop;
3913
3967
  const showInlineCommandPanel = showCommandPanel && !showSidebar && chat.messages.length === 0 && (showQuickActions && clientActions.length > 0 || showRagTopics && ragTopics.length > 0 || showRagDocuments && ragDocuments.length > 0);
3914
- const handleExecuteAction = useCallback(
3915
- (steps) => {
3916
- executeDeclarativeSteps(steps, onNavigate, userMessageColor);
3917
- },
3918
- [onNavigate, userMessageColor]
3919
- );
3968
+ const handleExecuteAction = (action) => {
3969
+ const plan = planQuickAction(
3970
+ action.steps,
3971
+ resolvePageContext()?.params,
3972
+ userContext?.metadata
3973
+ );
3974
+ if (plan.kind === "run") {
3975
+ executeDeclarativeSteps(plan.steps, onNavigate, userMessageColor);
3976
+ } else {
3977
+ chat.send(action.name);
3978
+ }
3979
+ };
3920
3980
  const [activeActionIndex, setActiveActionIndex] = useState(-1);
3921
3981
  const query = chat.input.toLowerCase().trim();
3922
3982
  const matchingActions = useMemo(() => {
@@ -3962,7 +4022,7 @@ function ChatWidget({
3962
4022
  e.preventDefault();
3963
4023
  const selectedAction = matchingActions[activeActionIndex];
3964
4024
  if (selectedAction) {
3965
- handleExecuteAction(selectedAction.steps);
4025
+ handleExecuteAction(selectedAction);
3966
4026
  chat.setInput("");
3967
4027
  }
3968
4028
  } else if (e.key === "Escape") {
@@ -4197,7 +4257,7 @@ function ChatWidget({
4197
4257
  "button",
4198
4258
  {
4199
4259
  onClick: () => {
4200
- handleExecuteAction(action.steps);
4260
+ handleExecuteAction(action);
4201
4261
  chat.setInput("");
4202
4262
  },
4203
4263
  className: cn(
package/package.json CHANGED
@@ -22,8 +22,8 @@
22
22
  "tsup": "^8.3.5",
23
23
  "tsx": "^4.19.2",
24
24
  "typescript": "^5.7.3",
25
- "@wallavi/typescript-config": "0.0.1",
26
- "@wallavi/protocol": "0.0.1"
25
+ "@wallavi/protocol": "0.0.1",
26
+ "@wallavi/typescript-config": "0.0.1"
27
27
  },
28
28
  "exports": {
29
29
  ".": {
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "private": false,
46
46
  "types": "./dist/index.d.ts",
47
- "version": "1.13.1",
47
+ "version": "1.13.3",
48
48
  "scripts": {
49
49
  "build": "tsup && pnpm build:css",
50
50
  "build:css": "tailwindcss -i ./src/styles.css -o ./dist/styles.css --config tailwind.config.cjs --minify",