@fiodos/web-core 0.1.15 → 0.1.18

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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Stable per-browser ANONYMOUS caller id, sent as `x-user-id` when the host
3
+ * app provides no getUserId (public storefronts, marketing sites…).
4
+ *
5
+ * Why: the backend's fairness rate-limit buckets anonymous callers by source
6
+ * IP. Behind shopper NATs / proxies that means MANY real visitors share one
7
+ * bucket and an active session can starve itself into 429 ("Too many
8
+ * requests"). A random per-browser id gives each visitor their own fairness
9
+ * bucket without identifying anyone: it is random, local, never derived from
10
+ * personal data, and never used for conversation identity (that stays 'anon').
11
+ */
12
+ /** Returns the stable anonymous caller id for this browser (creates it once). */
13
+ export declare function getAnonCallerId(): string;
14
+ /**
15
+ * Wraps the host's getUserId so anonymous visitors still send a stable
16
+ * per-browser caller id for rate-limit fairness. The host's real user id
17
+ * always wins when present.
18
+ */
19
+ export declare function withAnonCallerFallback(getUserId?: () => string | null): () => string | null;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Stable per-browser ANONYMOUS caller id, sent as `x-user-id` when the host
4
+ * app provides no getUserId (public storefronts, marketing sites…).
5
+ *
6
+ * Why: the backend's fairness rate-limit buckets anonymous callers by source
7
+ * IP. Behind shopper NATs / proxies that means MANY real visitors share one
8
+ * bucket and an active session can starve itself into 429 ("Too many
9
+ * requests"). A random per-browser id gives each visitor their own fairness
10
+ * bucket without identifying anyone: it is random, local, never derived from
11
+ * personal data, and never used for conversation identity (that stays 'anon').
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.getAnonCallerId = getAnonCallerId;
15
+ exports.withAnonCallerFallback = withAnonCallerFallback;
16
+ const STORAGE_KEY = 'fyodos:anon-caller:v1';
17
+ let inMemoryId = null;
18
+ function randomId() {
19
+ try {
20
+ const c = globalThis.crypto;
21
+ if (c?.randomUUID)
22
+ return `anon-${c.randomUUID()}`;
23
+ }
24
+ catch {
25
+ /* fall through */
26
+ }
27
+ return `anon-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
28
+ }
29
+ /** Returns the stable anonymous caller id for this browser (creates it once). */
30
+ function getAnonCallerId() {
31
+ if (inMemoryId)
32
+ return inMemoryId;
33
+ try {
34
+ const stored = window.localStorage.getItem(STORAGE_KEY);
35
+ if (stored) {
36
+ inMemoryId = stored;
37
+ return stored;
38
+ }
39
+ const fresh = randomId();
40
+ window.localStorage.setItem(STORAGE_KEY, fresh);
41
+ inMemoryId = fresh;
42
+ return fresh;
43
+ }
44
+ catch {
45
+ // Private mode / storage blocked: keep a per-page-load id (still better
46
+ // than sharing the IP bucket with every other visitor).
47
+ inMemoryId = inMemoryId ?? randomId();
48
+ return inMemoryId;
49
+ }
50
+ }
51
+ /**
52
+ * Wraps the host's getUserId so anonymous visitors still send a stable
53
+ * per-browser caller id for rate-limit fairness. The host's real user id
54
+ * always wins when present.
55
+ */
56
+ function withAnonCallerFallback(getUserId) {
57
+ return () => {
58
+ const real = getUserId?.();
59
+ if (real)
60
+ return real;
61
+ return getAnonCallerId();
62
+ };
63
+ }
@@ -145,6 +145,10 @@ function createFiodosBackendClient(options) {
145
145
  intent: request.lastStep.intent,
146
146
  ...(request.lastStep.label ? { label: request.lastStep.label } : {}),
147
147
  success: request.lastStep.success,
148
+ // Bounded result payload of the executed step (product search
149
+ // results…): the backend renders it so the model can read real
150
+ // ids for the NEXT step's parameters.
151
+ ...(request.lastStep.data ? { data: request.lastStep.data } : {}),
148
152
  };
149
153
  }
150
154
  }
@@ -33,6 +33,7 @@ export declare function fetchClientManifest(opts: {
33
33
  baseUrl: string;
34
34
  apiKey: string;
35
35
  }): Promise<ManifestBootstrapResult>;
36
+ export declare function markEmbedDistribution(): void;
36
37
  /**
37
38
  * Best-effort heartbeat proving the orb booted. Never throws and never blocks
38
39
  * the orb — failures are swallowed (the dashboard simply won't flip to ✓).
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_WEB_API_URL = void 0;
4
4
  exports.fetchClientManifest = fetchClientManifest;
5
+ exports.markEmbedDistribution = markEmbedDistribution;
5
6
  exports.sendOrbSeen = sendOrbSeen;
6
7
  const version_1 = require("../version");
7
8
  /**
@@ -42,11 +43,21 @@ async function fetchClientManifest(opts) {
42
43
  }
43
44
  return { status: 'ready', manifest: data.manifest };
44
45
  }
46
+ /**
47
+ * Set by the embed bundle entrypoint (dist/embed/fiodos-embed.js). Embeds are
48
+ * loaded from a CDN `@latest` URL (Shopify themes, Dynamics…), so they update
49
+ * THEMSELVES — the heartbeat marks them so the dashboard never shows an
50
+ * "update available / npm install" notice the developer cannot act on.
51
+ */
52
+ let embedDistribution = false;
53
+ function markEmbedDistribution() {
54
+ embedDistribution = true;
55
+ }
45
56
  function detectPlatform() {
46
- if (typeof navigator !== 'undefined' && /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent)) {
47
- return 'web-mobile';
48
- }
49
- return 'web';
57
+ const mobile = typeof navigator !== 'undefined' && /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent);
58
+ if (embedDistribution)
59
+ return mobile ? 'web-embed-mobile' : 'web-embed';
60
+ return mobile ? 'web-mobile' : 'web';
50
61
  }
51
62
  /**
52
63
  * Best-effort heartbeat proving the orb booted. Never throws and never blocks
@@ -716,9 +716,22 @@ class AgentController {
716
716
  /* keep the assistant's guiding reply; never speak a refusal. */
717
717
  }
718
718
  else if (r.message) {
719
+ // FEEDBACK GUARANTEE: the handler's outcome message (success,
720
+ // idempotent or failure) is the ground truth that the action ran;
721
+ // speak it instead of the LLM's optimistic reply.
719
722
  outReply = r.message;
720
723
  outAudio = null;
721
724
  }
725
+ else if (!r.success) {
726
+ // Same guarantee for a handler that failed WITHOUT setting `message`
727
+ // (e.g. a declarative Shopify/OData `execution` call that hit a
728
+ // network/HTTP/template error — apiActions.ts never throws, so
729
+ // runManifestAction's catch never runs either). Without this, the
730
+ // LLM's pre-action narration ("Adding X to your cart…") stays on
731
+ // screen as if it had happened — a silent false success.
732
+ outReply = this.messages.actionFailed;
733
+ outAudio = null;
734
+ }
722
735
  // Chain signal: only a cleanly EXECUTED, non-recoverable step may advance,
723
736
  // and only when the backend asked to continue (task_status=in_progress).
724
737
  if (turn.action &&
@@ -726,11 +739,16 @@ class AgentController {
726
739
  r.success &&
727
740
  !r.recoverable) {
728
741
  const intent = turn.action.intent ?? '';
742
+ // Feed the step's RESULT DATA back to the next chain turn (bounded):
743
+ // it is how the model reads real technical values (variant ids, line
744
+ // numbers) out of a data action instead of inventing them.
745
+ const chainData = (0, core_1.trimChainStepData)(r.data);
729
746
  lastStep = {
730
747
  type: turn.action.type,
731
748
  intent,
732
749
  label: turn.action.type === 'navigate' ? this.routeLabel(intent) : this.actionLabel(intent),
733
750
  success: true,
751
+ ...(chainData ? { data: chainData } : {}),
734
752
  };
735
753
  actionKey = `${turn.action.type}:${intent}`;
736
754
  canContinue = turn.taskStatus === 'in_progress';
@@ -24,6 +24,7 @@ const AgentController_1 = require("../controller/AgentController");
24
24
  const mountOrb_1 = require("../orb/mountOrb");
25
25
  const devErrorBadge_1 = require("../orb/devErrorBadge");
26
26
  const clientBootstrap_1 = require("../api/clientBootstrap");
27
+ const anonUserId_1 = require("../api/anonUserId");
27
28
  function defaultNavigate(route) {
28
29
  if (typeof window !== 'undefined' && window.location)
29
30
  window.location.assign(route);
@@ -63,15 +64,18 @@ function assemble(options, manifest, baseUrl) {
63
64
  const fallback = browserLocale();
64
65
  const locale = resolveAgentLocale(options);
65
66
  const sttLocale = options.sttLocale ?? options.locale ?? fallback.sttLocale;
67
+ // Rate-limit fairness: anonymous visitors still send a stable per-browser
68
+ // caller id (never used for conversation identity — that stays 'anon').
69
+ const callerId = (0, anonUserId_1.withAnonCallerFallback)(options.getUserId);
66
70
  const backend = (0, backendClient_1.createFiodosBackendClient)({
67
71
  baseUrl,
68
72
  apiKey: options.apiKey,
69
- getUserId: options.getUserId,
73
+ getUserId: callerId,
70
74
  });
71
75
  const telemetry = (0, backendTelemetry_1.createFiodosTelemetry)({
72
76
  baseUrl,
73
77
  apiKey: options.apiKey,
74
- getUserId: options.getUserId,
78
+ getUserId: callerId,
75
79
  });
76
80
  const config = {
77
81
  manifest,
@@ -27,7 +27,7 @@ import { createScreenContextStore } from '../context/screenContextStore';
27
27
  import { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId } from './dynamics';
28
28
  import { buildApiActionRegistries, validateManifest } from '@fiodos/core';
29
29
  declare const api: {
30
- readonly version: "0.1.15";
30
+ readonly version: "0.1.18";
31
31
  readonly createFiodosAgent: typeof createFiodosAgent;
32
32
  readonly mountOrb: typeof mountOrb;
33
33
  readonly AgentController: typeof AgentController;
@@ -28,7 +28,11 @@ const AgentController_1 = require("../controller/AgentController");
28
28
  const screenContextStore_1 = require("../context/screenContextStore");
29
29
  const dynamics_1 = require("./dynamics");
30
30
  const version_1 = require("../version");
31
+ const clientBootstrap_1 = require("../api/clientBootstrap");
31
32
  const core_1 = require("@fiodos/core");
33
+ // This bundle is served from a CDN `@latest` URL and updates itself: mark the
34
+ // heartbeat so the dashboard never shows an npm update command for it.
35
+ (0, clientBootstrap_1.markEmbedDistribution)();
32
36
  const api = {
33
37
  version: version_1.SDK_VERSION,
34
38
  createFiodosAgent: createFiodosAgent_1.createFiodosAgent,
@@ -710,13 +710,44 @@ function mountOrb(controller, options = {}) {
710
710
  // Live-activity line: white (reply color), italic — not the faint gray.
711
711
  busyStep.style.color = t.titleColor;
712
712
  }
713
+ // The panel's open state survives FULL PAGE LOADS within the tab session:
714
+ // on multi-page hosts (Shopify storefronts…) every agent navigation reloads
715
+ // the document, and without this the conversation panel closed mid-dialogue
716
+ // on every step. sessionStorage is per-tab and cleared when the tab closes,
717
+ // so a fresh visit never starts with the panel open.
718
+ const panelOpenKey = `fyodos:panel-open:${controller.config.manifest.appId}`;
719
+ function persistPanelOpen(open) {
720
+ try {
721
+ if (open)
722
+ window.sessionStorage.setItem(panelOpenKey, '1');
723
+ else
724
+ window.sessionStorage.removeItem(panelOpenKey);
725
+ }
726
+ catch {
727
+ /* storage unavailable (private mode) — persistence is best-effort */
728
+ }
729
+ }
730
+ function wasPanelOpen() {
731
+ try {
732
+ return window.sessionStorage.getItem(panelOpenKey) === '1';
733
+ }
734
+ catch {
735
+ return false;
736
+ }
737
+ }
713
738
  function setBubble(open) {
714
739
  bubbleOpen = open;
715
740
  bubble.style.display = open ? 'flex' : 'none';
741
+ persistPanelOpen(open);
716
742
  if (open) {
717
743
  applyBubbleTheme();
718
744
  positionBubble();
719
745
  textInput.focus();
746
+ // Paint the CURRENT conversation immediately: exchanges are only
747
+ // rendered inside render() while the panel is open, so without this
748
+ // the restored history stayed invisible (an apparently stuck panel)
749
+ // until the next controller event — i.e. until the user typed.
750
+ render(controller.getState());
720
751
  }
721
752
  else {
722
753
  bubbleDictation.stop();
@@ -1159,6 +1190,13 @@ function mountOrb(controller, options = {}) {
1159
1190
  }
1160
1191
  const unsubscribe = controller.subscribe(render);
1161
1192
  render(controller.getState());
1193
+ // Re-open the conversation panel after a full page load when it was open
1194
+ // before (MPA hosts reload on every navigation): the dialogue visually
1195
+ // continues where it was — the persisted conversation restores async in the
1196
+ // controller and repaints via emit → render once loaded.
1197
+ if (enableTextInput && wasPanelOpen()) {
1198
+ setBubble(true);
1199
+ }
1162
1200
  // Track the current route AND the app's live locale so the thought bubble
1163
1201
  // re-resolves its CTA on navigation and on language switches. No universal
1164
1202
  // navigation/i18n event exists, so poll + listen to popstate / visibility
@@ -5,5 +5,5 @@
5
5
  * Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
6
6
  * by hand — change package.json `version` and re-run the sync/release script.
7
7
  */
8
- export declare const SDK_VERSION = "0.1.15";
8
+ export declare const SDK_VERSION = "0.1.18";
9
9
  export declare const SDK_PACKAGE = "@fiodos/web-core";
@@ -8,5 +8,5 @@ exports.SDK_PACKAGE = exports.SDK_VERSION = void 0;
8
8
  * Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
9
9
  * by hand — change package.json `version` and re-run the sync/release script.
10
10
  */
11
- exports.SDK_VERSION = '0.1.15';
11
+ exports.SDK_VERSION = '0.1.18';
12
12
  exports.SDK_PACKAGE = '@fiodos/web-core';