@wallavi/widget 1.13.2 → 1.14.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.d.mts CHANGED
@@ -358,7 +358,10 @@ interface CommandPanelProps {
358
358
  ragTopics?: RagTopicInfo[];
359
359
  ragDocuments?: RagDocumentInfo[];
360
360
  onSend: (text: string) => void;
361
- onExecuteAction?: (steps: any[]) => void;
361
+ onExecuteAction?: (action: {
362
+ name: string;
363
+ steps: any[];
364
+ }) => void;
362
365
  accentColor?: string;
363
366
  isDark?: boolean;
364
367
  watermark?: boolean;
package/dist/index.d.ts CHANGED
@@ -358,7 +358,10 @@ interface CommandPanelProps {
358
358
  ragTopics?: RagTopicInfo[];
359
359
  ragDocuments?: RagDocumentInfo[];
360
360
  onSend: (text: string) => void;
361
- onExecuteAction?: (steps: any[]) => void;
361
+ onExecuteAction?: (action: {
362
+ name: string;
363
+ steps: any[];
364
+ }) => void;
362
365
  accentColor?: string;
363
366
  isDark?: boolean;
364
367
  watermark?: boolean;
package/dist/index.js CHANGED
@@ -227,7 +227,7 @@ async function consumeStream(body, handler) {
227
227
  function applyUiEventToMessages(prev, proto, msgId) {
228
228
  let idx = prev.findIndex((m) => m.id === msgId);
229
229
  if (idx === -1) {
230
- if (proto.type === "finish" || proto.type === "debug-trace" || proto.type === "step-start" || proto.type === "step-end" || proto.type === "navigate" || proto.type === "suggestions") {
230
+ if (proto.type === "finish" || proto.type === "debug-trace" || proto.type === "step-start" || proto.type === "step-end" || proto.type === "suggestions") {
231
231
  return prev;
232
232
  }
233
233
  prev = [...prev, { id: msgId, role: "assistant", parts: [] }];
@@ -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);
@@ -846,10 +901,6 @@ function useChat({
846
901
  }, [messages]);
847
902
  const applyStreamEvent = react.useCallback(
848
903
  (proto, msgId) => {
849
- if (proto.type === "navigate") {
850
- if (proto.path.startsWith("/")) onNavigateRef.current?.(proto.path);
851
- return;
852
- }
853
904
  if (proto.type === "client-action") {
854
905
  executeDeclarativeSteps(
855
906
  proto.steps,
@@ -3631,7 +3682,7 @@ function CommandPanel({
3631
3682
  const hasContent = hasActions || hasDocs || hasTopics;
3632
3683
  if (!hasContent) return null;
3633
3684
  const isSidebar = mode === "sidebar";
3634
- const handleExecute = (steps) => onExecuteAction?.(steps);
3685
+ const handleExecute = (action) => onExecuteAction?.(action);
3635
3686
  if (isSidebar) {
3636
3687
  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: [
3637
3688
  /* @__PURE__ */ jsxRuntime.jsx(PageBadge, { pageTitle, pageUrl, locale }),
@@ -3644,7 +3695,7 @@ function CommandPanel({
3644
3695
  action,
3645
3696
  index: i,
3646
3697
  accentColor,
3647
- onExecute: () => handleExecute(action.steps)
3698
+ onExecute: () => handleExecute(action)
3648
3699
  },
3649
3700
  actionKeys[i]
3650
3701
  )) })
@@ -3716,7 +3767,7 @@ function CommandPanel({
3716
3767
  action,
3717
3768
  index: i,
3718
3769
  accentColor,
3719
- onExecute: () => handleExecute(action.steps)
3770
+ onExecute: () => handleExecute(action)
3720
3771
  },
3721
3772
  actionKeys[i]
3722
3773
  )) })
@@ -3936,12 +3987,18 @@ function ChatWidget({
3936
3987
  const messagesList = isDefaultGreeting ? [getWidgetTranslation("defaultGreeting", locale)] : initialMessages;
3937
3988
  const showSidebar = showCommandPanel && widgetLayout === "center" && isDesktop;
3938
3989
  const showInlineCommandPanel = showCommandPanel && !showSidebar && chat.messages.length === 0 && (showQuickActions && clientActions.length > 0 || showRagTopics && ragTopics.length > 0 || showRagDocuments && ragDocuments.length > 0);
3939
- const handleExecuteAction = react.useCallback(
3940
- (steps) => {
3941
- executeDeclarativeSteps(steps, onNavigate, userMessageColor);
3942
- },
3943
- [onNavigate, userMessageColor]
3944
- );
3990
+ const handleExecuteAction = (action) => {
3991
+ const plan = planQuickAction(
3992
+ action.steps,
3993
+ resolvePageContext()?.params,
3994
+ userContext?.metadata
3995
+ );
3996
+ if (plan.kind === "run") {
3997
+ executeDeclarativeSteps(plan.steps, onNavigate, userMessageColor);
3998
+ } else {
3999
+ chat.send(action.name);
4000
+ }
4001
+ };
3945
4002
  const [activeActionIndex, setActiveActionIndex] = react.useState(-1);
3946
4003
  const query = chat.input.toLowerCase().trim();
3947
4004
  const matchingActions = react.useMemo(() => {
@@ -3987,7 +4044,7 @@ function ChatWidget({
3987
4044
  e.preventDefault();
3988
4045
  const selectedAction = matchingActions[activeActionIndex];
3989
4046
  if (selectedAction) {
3990
- handleExecuteAction(selectedAction.steps);
4047
+ handleExecuteAction(selectedAction);
3991
4048
  chat.setInput("");
3992
4049
  }
3993
4050
  } else if (e.key === "Escape") {
@@ -4222,7 +4279,7 @@ function ChatWidget({
4222
4279
  "button",
4223
4280
  {
4224
4281
  onClick: () => {
4225
- handleExecuteAction(action.steps);
4282
+ handleExecuteAction(action);
4226
4283
  chat.setInput("");
4227
4284
  },
4228
4285
  className: cn(
package/dist/index.mjs CHANGED
@@ -201,7 +201,7 @@ async function consumeStream(body, handler) {
201
201
  function applyUiEventToMessages(prev, proto, msgId) {
202
202
  let idx = prev.findIndex((m) => m.id === msgId);
203
203
  if (idx === -1) {
204
- if (proto.type === "finish" || proto.type === "debug-trace" || proto.type === "step-start" || proto.type === "step-end" || proto.type === "navigate" || proto.type === "suggestions") {
204
+ if (proto.type === "finish" || proto.type === "debug-trace" || proto.type === "step-start" || proto.type === "step-end" || proto.type === "suggestions") {
205
205
  return prev;
206
206
  }
207
207
  prev = [...prev, { id: msgId, role: "assistant", parts: [] }];
@@ -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);
@@ -820,10 +875,6 @@ function useChat({
820
875
  }, [messages]);
821
876
  const applyStreamEvent = useCallback(
822
877
  (proto, msgId) => {
823
- if (proto.type === "navigate") {
824
- if (proto.path.startsWith("/")) onNavigateRef.current?.(proto.path);
825
- return;
826
- }
827
878
  if (proto.type === "client-action") {
828
879
  executeDeclarativeSteps(
829
880
  proto.steps,
@@ -3605,7 +3656,7 @@ function CommandPanel({
3605
3656
  const hasContent = hasActions || hasDocs || hasTopics;
3606
3657
  if (!hasContent) return null;
3607
3658
  const isSidebar = mode === "sidebar";
3608
- const handleExecute = (steps) => onExecuteAction?.(steps);
3659
+ const handleExecute = (action) => onExecuteAction?.(action);
3609
3660
  if (isSidebar) {
3610
3661
  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: [
3611
3662
  /* @__PURE__ */ jsx(PageBadge, { pageTitle, pageUrl, locale }),
@@ -3618,7 +3669,7 @@ function CommandPanel({
3618
3669
  action,
3619
3670
  index: i,
3620
3671
  accentColor,
3621
- onExecute: () => handleExecute(action.steps)
3672
+ onExecute: () => handleExecute(action)
3622
3673
  },
3623
3674
  actionKeys[i]
3624
3675
  )) })
@@ -3690,7 +3741,7 @@ function CommandPanel({
3690
3741
  action,
3691
3742
  index: i,
3692
3743
  accentColor,
3693
- onExecute: () => handleExecute(action.steps)
3744
+ onExecute: () => handleExecute(action)
3694
3745
  },
3695
3746
  actionKeys[i]
3696
3747
  )) })
@@ -3910,12 +3961,18 @@ function ChatWidget({
3910
3961
  const messagesList = isDefaultGreeting ? [getWidgetTranslation("defaultGreeting", locale)] : initialMessages;
3911
3962
  const showSidebar = showCommandPanel && widgetLayout === "center" && isDesktop;
3912
3963
  const showInlineCommandPanel = showCommandPanel && !showSidebar && chat.messages.length === 0 && (showQuickActions && clientActions.length > 0 || showRagTopics && ragTopics.length > 0 || showRagDocuments && ragDocuments.length > 0);
3913
- const handleExecuteAction = useCallback(
3914
- (steps) => {
3915
- executeDeclarativeSteps(steps, onNavigate, userMessageColor);
3916
- },
3917
- [onNavigate, userMessageColor]
3918
- );
3964
+ const handleExecuteAction = (action) => {
3965
+ const plan = planQuickAction(
3966
+ action.steps,
3967
+ resolvePageContext()?.params,
3968
+ userContext?.metadata
3969
+ );
3970
+ if (plan.kind === "run") {
3971
+ executeDeclarativeSteps(plan.steps, onNavigate, userMessageColor);
3972
+ } else {
3973
+ chat.send(action.name);
3974
+ }
3975
+ };
3919
3976
  const [activeActionIndex, setActiveActionIndex] = useState(-1);
3920
3977
  const query = chat.input.toLowerCase().trim();
3921
3978
  const matchingActions = useMemo(() => {
@@ -3961,7 +4018,7 @@ function ChatWidget({
3961
4018
  e.preventDefault();
3962
4019
  const selectedAction = matchingActions[activeActionIndex];
3963
4020
  if (selectedAction) {
3964
- handleExecuteAction(selectedAction.steps);
4021
+ handleExecuteAction(selectedAction);
3965
4022
  chat.setInput("");
3966
4023
  }
3967
4024
  } else if (e.key === "Escape") {
@@ -4196,7 +4253,7 @@ function ChatWidget({
4196
4253
  "button",
4197
4254
  {
4198
4255
  onClick: () => {
4199
- handleExecuteAction(action.steps);
4256
+ handleExecuteAction(action);
4200
4257
  chat.setInput("");
4201
4258
  },
4202
4259
  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/protocol": "0.0.1",
26
- "@wallavi/typescript-config": "0.0.1"
25
+ "@wallavi/typescript-config": "0.0.1",
26
+ "@wallavi/protocol": "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.2",
47
+ "version": "1.14.0",
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",