@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.
- package/dist/cjs/api/anonUserId.d.ts +19 -0
- package/dist/cjs/api/anonUserId.js +63 -0
- package/dist/cjs/api/backendClient.js +4 -0
- package/dist/cjs/api/clientBootstrap.d.ts +1 -0
- package/dist/cjs/api/clientBootstrap.js +15 -4
- package/dist/cjs/controller/AgentController.js +18 -0
- package/dist/cjs/dropin/createFiodosAgent.js +6 -2
- package/dist/cjs/embed/global.d.ts +1 -1
- package/dist/cjs/embed/global.js +4 -0
- package/dist/cjs/orb/mountOrb.js +38 -0
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/embed/fiodos-embed.js +4 -4
- package/dist/esm/api/anonUserId.d.ts +19 -0
- package/dist/esm/api/anonUserId.js +59 -0
- package/dist/esm/api/backendClient.js +4 -0
- package/dist/esm/api/clientBootstrap.d.ts +1 -0
- package/dist/esm/api/clientBootstrap.js +14 -4
- package/dist/esm/controller/AgentController.js +19 -1
- package/dist/esm/dropin/createFiodosAgent.js +6 -2
- package/dist/esm/embed/global.d.ts +1 -1
- package/dist/esm/embed/global.js +4 -0
- package/dist/esm/orb/mountOrb.js +38 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
47
|
-
|
|
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:
|
|
73
|
+
getUserId: callerId,
|
|
70
74
|
});
|
|
71
75
|
const telemetry = (0, backendTelemetry_1.createFiodosTelemetry)({
|
|
72
76
|
baseUrl,
|
|
73
77
|
apiKey: options.apiKey,
|
|
74
|
-
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.
|
|
30
|
+
readonly version: "0.1.18";
|
|
31
31
|
readonly createFiodosAgent: typeof createFiodosAgent;
|
|
32
32
|
readonly mountOrb: typeof mountOrb;
|
|
33
33
|
readonly AgentController: typeof AgentController;
|
package/dist/cjs/embed/global.js
CHANGED
|
@@ -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,
|
package/dist/cjs/orb/mountOrb.js
CHANGED
|
@@ -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
|
package/dist/cjs/version.d.ts
CHANGED
|
@@ -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.
|
|
8
|
+
export declare const SDK_VERSION = "0.1.18";
|
|
9
9
|
export declare const SDK_PACKAGE = "@fiodos/web-core";
|
package/dist/cjs/version.js
CHANGED
|
@@ -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.
|
|
11
|
+
exports.SDK_VERSION = '0.1.18';
|
|
12
12
|
exports.SDK_PACKAGE = '@fiodos/web-core';
|