@fiodos/web-core 0.1.17 → 0.1.19
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 +7 -1
- package/dist/cjs/api/clientBootstrap.d.ts +1 -0
- package/dist/cjs/api/clientBootstrap.js +15 -4
- package/dist/cjs/config/types.d.ts +9 -3
- package/dist/cjs/config/types.js +1 -1
- package/dist/cjs/controller/AgentController.d.ts +13 -0
- package/dist/cjs/controller/AgentController.js +37 -0
- package/dist/cjs/core/turnEngine.d.ts +6 -0
- package/dist/cjs/core/turnEngine.js +2 -2
- 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/speech/bubbleDictation.js +27 -3
- 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 +7 -1
- package/dist/esm/api/clientBootstrap.d.ts +1 -0
- package/dist/esm/api/clientBootstrap.js +14 -4
- package/dist/esm/config/types.d.ts +9 -3
- package/dist/esm/config/types.js +1 -1
- package/dist/esm/controller/AgentController.d.ts +13 -0
- package/dist/esm/controller/AgentController.js +37 -0
- package/dist/esm/core/turnEngine.d.ts +6 -0
- package/dist/esm/core/turnEngine.js +2 -2
- 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/speech/bubbleDictation.js +27 -3
- 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,59 @@
|
|
|
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
|
+
const STORAGE_KEY = 'fyodos:anon-caller:v1';
|
|
13
|
+
let inMemoryId = null;
|
|
14
|
+
function randomId() {
|
|
15
|
+
try {
|
|
16
|
+
const c = globalThis.crypto;
|
|
17
|
+
if (c?.randomUUID)
|
|
18
|
+
return `anon-${c.randomUUID()}`;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
/* fall through */
|
|
22
|
+
}
|
|
23
|
+
return `anon-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
24
|
+
}
|
|
25
|
+
/** Returns the stable anonymous caller id for this browser (creates it once). */
|
|
26
|
+
export function getAnonCallerId() {
|
|
27
|
+
if (inMemoryId)
|
|
28
|
+
return inMemoryId;
|
|
29
|
+
try {
|
|
30
|
+
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
31
|
+
if (stored) {
|
|
32
|
+
inMemoryId = stored;
|
|
33
|
+
return stored;
|
|
34
|
+
}
|
|
35
|
+
const fresh = randomId();
|
|
36
|
+
window.localStorage.setItem(STORAGE_KEY, fresh);
|
|
37
|
+
inMemoryId = fresh;
|
|
38
|
+
return fresh;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Private mode / storage blocked: keep a per-page-load id (still better
|
|
42
|
+
// than sharing the IP bucket with every other visitor).
|
|
43
|
+
inMemoryId = inMemoryId ?? randomId();
|
|
44
|
+
return inMemoryId;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wraps the host's getUserId so anonymous visitors still send a stable
|
|
49
|
+
* per-browser caller id for rate-limit fairness. The host's real user id
|
|
50
|
+
* always wins when present.
|
|
51
|
+
*/
|
|
52
|
+
export function withAnonCallerFallback(getUserId) {
|
|
53
|
+
return () => {
|
|
54
|
+
const real = getUserId?.();
|
|
55
|
+
if (real)
|
|
56
|
+
return real;
|
|
57
|
+
return getAnonCallerId();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -57,7 +57,13 @@ async function errorFromResponse(res) {
|
|
|
57
57
|
return new AgentApiError('unauthorized', detail, res.status);
|
|
58
58
|
}
|
|
59
59
|
if (res.status === 429) {
|
|
60
|
-
|
|
60
|
+
// The backend marks plan/budget 429s with error:"quota_exceeded" (rate
|
|
61
|
+
// limits carry error:"rate_limited"); the detail/remaining checks cover
|
|
62
|
+
// older backends. Getting this right matters: a quota 429 shown as "too
|
|
63
|
+
// many requests" sends the developer chasing rate limits that are fine.
|
|
64
|
+
const quotaSignal = body.error === 'quota_exceeded' ||
|
|
65
|
+
typeof body.remaining === 'number' ||
|
|
66
|
+
(detail ?? '').toLowerCase().includes('quota');
|
|
61
67
|
return new AgentApiError(quotaSignal ? 'quota_exceeded' : 'rate_limited', detail, res.status);
|
|
62
68
|
}
|
|
63
69
|
if (res.status === 404)
|
|
@@ -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 ✓).
|
|
@@ -37,11 +37,21 @@ export async function fetchClientManifest(opts) {
|
|
|
37
37
|
}
|
|
38
38
|
return { status: 'ready', manifest: data.manifest };
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Set by the embed bundle entrypoint (dist/embed/fiodos-embed.js). Embeds are
|
|
42
|
+
* loaded from a CDN `@latest` URL (Shopify themes, Dynamics…), so they update
|
|
43
|
+
* THEMSELVES — the heartbeat marks them so the dashboard never shows an
|
|
44
|
+
* "update available / npm install" notice the developer cannot act on.
|
|
45
|
+
*/
|
|
46
|
+
let embedDistribution = false;
|
|
47
|
+
export function markEmbedDistribution() {
|
|
48
|
+
embedDistribution = true;
|
|
49
|
+
}
|
|
40
50
|
function detectPlatform() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return 'web';
|
|
51
|
+
const mobile = typeof navigator !== 'undefined' && /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent);
|
|
52
|
+
if (embedDistribution)
|
|
53
|
+
return mobile ? 'web-embed-mobile' : 'web-embed';
|
|
54
|
+
return mobile ? 'web-mobile' : 'web';
|
|
45
55
|
}
|
|
46
56
|
/**
|
|
47
57
|
* Best-effort heartbeat proving the orb booted. Never throws and never blocks
|
|
@@ -18,14 +18,20 @@ export interface AgentTimings {
|
|
|
18
18
|
}
|
|
19
19
|
export declare const DEFAULT_AGENT_TIMINGS: AgentTimings;
|
|
20
20
|
/**
|
|
21
|
-
* Autonomous multi-step chaining.
|
|
22
|
-
* behaves exactly as before (one action per turn). When ON, after a successful
|
|
21
|
+
* Autonomous multi-step chaining. ON by default: after a successful
|
|
23
22
|
* navigate/execute whose backend `task_status` is "in_progress", the orb takes
|
|
24
23
|
* the next step on its own — capped at `maxSteps` and guarded by no-progress
|
|
25
24
|
* detection and sensitive-confirmation pauses (which always require the human).
|
|
25
|
+
*
|
|
26
|
+
* Default ON is a product invariant, not a tuning choice: data actions
|
|
27
|
+
* (search, get-cart…) are INVISIBLE — without the continuation turn the orb
|
|
28
|
+
* runs them and then goes silent ("I'll look for products…" and nothing
|
|
29
|
+
* happens). Every install (embeds included, which pass no config) must get
|
|
30
|
+
* the full act→observe→act loop. Pass `chaining: { enabled: false }` to opt
|
|
31
|
+
* out and return to one-action-per-turn.
|
|
26
32
|
*/
|
|
27
33
|
export interface AgentChaining {
|
|
28
|
-
/** Master switch. Default
|
|
34
|
+
/** Master switch. Default true (the agent completes multi-step tasks). */
|
|
29
35
|
enabled: boolean;
|
|
30
36
|
/** Hard cap on steps per user request (safety). Default 6. */
|
|
31
37
|
maxSteps: number;
|
package/dist/esm/config/types.js
CHANGED
|
@@ -104,6 +104,12 @@ export declare class AgentController {
|
|
|
104
104
|
private pendingConsentIntent;
|
|
105
105
|
/** Parked chain state (set when a chain pauses on a sensitive action). */
|
|
106
106
|
private chainState;
|
|
107
|
+
/**
|
|
108
|
+
* Intent whose showcase escort was DEFERRED mid-chain (suppressEscort):
|
|
109
|
+
* flushed when the chain ends so the user is walked to where the last
|
|
110
|
+
* action's effect is visible — never in the middle of a running chain.
|
|
111
|
+
*/
|
|
112
|
+
private deferredEscortIntent;
|
|
107
113
|
/** REAL live activity for the panel (replaces the old canned walk-through). */
|
|
108
114
|
private liveActivity;
|
|
109
115
|
private chainActive;
|
|
@@ -221,6 +227,13 @@ export declare class AgentController {
|
|
|
221
227
|
* maxSteps, with no-progress detection and sensitive-confirmation pauses.
|
|
222
228
|
*/
|
|
223
229
|
private runChainLoop;
|
|
230
|
+
/**
|
|
231
|
+
* Runs the showcase escort that was deferred while the chain was executing
|
|
232
|
+
* (see suppressEscort in applyOutcome). Skipped while a confirmation is
|
|
233
|
+
* pending: the chain may resume after the user answers, and the confirmed
|
|
234
|
+
* action escorts on its own.
|
|
235
|
+
*/
|
|
236
|
+
private flushDeferredEscort;
|
|
224
237
|
private handleTranscript;
|
|
225
238
|
private onTranscript;
|
|
226
239
|
private onSpeechError;
|
|
@@ -60,6 +60,12 @@ export class AgentController {
|
|
|
60
60
|
this.pendingConsentIntent = null;
|
|
61
61
|
/** Parked chain state (set when a chain pauses on a sensitive action). */
|
|
62
62
|
this.chainState = null;
|
|
63
|
+
/**
|
|
64
|
+
* Intent whose showcase escort was DEFERRED mid-chain (suppressEscort):
|
|
65
|
+
* flushed when the chain ends so the user is walked to where the last
|
|
66
|
+
* action's effect is visible — never in the middle of a running chain.
|
|
67
|
+
*/
|
|
68
|
+
this.deferredEscortIntent = null;
|
|
63
69
|
/** REAL live activity for the panel (replaces the old canned walk-through). */
|
|
64
70
|
this.liveActivity = null;
|
|
65
71
|
this.chainActive = false;
|
|
@@ -539,6 +545,7 @@ export class AgentController {
|
|
|
539
545
|
// Declining a sensitive action stops any chain it was part of, cleanly.
|
|
540
546
|
const chainState = this.chainState;
|
|
541
547
|
this.chainState = null;
|
|
548
|
+
this.deferredEscortIntent = null;
|
|
542
549
|
if (chainState) {
|
|
543
550
|
this.telemetry.logEvent({
|
|
544
551
|
eventType: 'agent_chain_aborted',
|
|
@@ -658,6 +665,10 @@ export class AgentController {
|
|
|
658
665
|
let outAudio = turn.audioBase64;
|
|
659
666
|
if (outReply)
|
|
660
667
|
this.serverTtsUnavailable = !outAudio;
|
|
668
|
+
// Mid-chain: hold the showcase escort. On multi-page hosts (Shopify
|
|
669
|
+
// embeds) the escort is a full page load that would kill the chain
|
|
670
|
+
// before its continuation turn; the chain escorts once when it ends.
|
|
671
|
+
const escortDeferred = this.chaining.enabled && turn.taskStatus === 'in_progress';
|
|
661
672
|
const decision = await decideAction({
|
|
662
673
|
action: turn.action,
|
|
663
674
|
manifest: this.config.manifest,
|
|
@@ -665,6 +676,7 @@ export class AgentController {
|
|
|
665
676
|
context: snapshotContext,
|
|
666
677
|
messages: this.messages,
|
|
667
678
|
userMessage,
|
|
679
|
+
suppressEscort: escortDeferred,
|
|
668
680
|
});
|
|
669
681
|
let canContinue = false;
|
|
670
682
|
let lastStep;
|
|
@@ -735,6 +747,15 @@ export class AgentController {
|
|
|
735
747
|
(turn.action.type === 'navigate' || turn.action.type === 'execute') &&
|
|
736
748
|
r.success &&
|
|
737
749
|
!r.recoverable) {
|
|
750
|
+
// Deferred-escort bookkeeping: remember the intent whose escort was
|
|
751
|
+
// held (flushed at chain end); a navigate — or an execute whose escort
|
|
752
|
+
// already ran inside the executor — supersedes any earlier deferral.
|
|
753
|
+
if (turn.action.type === 'execute') {
|
|
754
|
+
this.deferredEscortIntent = escortDeferred ? (turn.action.intent ?? null) : null;
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
this.deferredEscortIntent = null;
|
|
758
|
+
}
|
|
738
759
|
const intent = turn.action.intent ?? '';
|
|
739
760
|
// Feed the step's RESULT DATA back to the next chain turn (bounded):
|
|
740
761
|
// it is how the model reads real technical values (variant ids, line
|
|
@@ -938,8 +959,23 @@ export class AgentController {
|
|
|
938
959
|
}
|
|
939
960
|
finally {
|
|
940
961
|
this.setChainActive(false);
|
|
962
|
+
this.flushDeferredEscort();
|
|
941
963
|
}
|
|
942
964
|
}
|
|
965
|
+
/**
|
|
966
|
+
* Runs the showcase escort that was deferred while the chain was executing
|
|
967
|
+
* (see suppressEscort in applyOutcome). Skipped while a confirmation is
|
|
968
|
+
* pending: the chain may resume after the user answers, and the confirmed
|
|
969
|
+
* action escorts on its own.
|
|
970
|
+
*/
|
|
971
|
+
flushDeferredEscort() {
|
|
972
|
+
if (this.pending != null)
|
|
973
|
+
return;
|
|
974
|
+
const intent = this.deferredEscortIntent;
|
|
975
|
+
this.deferredEscortIntent = null;
|
|
976
|
+
if (intent)
|
|
977
|
+
this.executor.escortForIntent(intent);
|
|
978
|
+
}
|
|
943
979
|
// ── Main turn ────────────────────────────────────────────────────────────────
|
|
944
980
|
async handleTranscript(rawText) {
|
|
945
981
|
const text = rawText.trim();
|
|
@@ -1223,6 +1259,7 @@ export class AgentController {
|
|
|
1223
1259
|
this.config.voice.stopPlayback();
|
|
1224
1260
|
this.pending = null;
|
|
1225
1261
|
this.chainState = null;
|
|
1262
|
+
this.deferredEscortIntent = null;
|
|
1226
1263
|
this.confirmationMessage = null;
|
|
1227
1264
|
this.confirmationVoiceHint = null;
|
|
1228
1265
|
this.liveActivity = null;
|
|
@@ -52,6 +52,12 @@ export interface DecideActionParams {
|
|
|
52
52
|
context: AgentScreenContext;
|
|
53
53
|
messages: AgentMessages;
|
|
54
54
|
userMessage: string;
|
|
55
|
+
/**
|
|
56
|
+
* Skip the post-action showcase escort (mid-chain steps): on multi-page
|
|
57
|
+
* hosts the escort is a full page load that would kill the running chain.
|
|
58
|
+
* The orchestrator escorts once at the END of the chain.
|
|
59
|
+
*/
|
|
60
|
+
suppressEscort?: boolean;
|
|
55
61
|
}
|
|
56
62
|
/**
|
|
57
63
|
* Decides what to do with a backend-proposed action. Sensitive actions are
|
|
@@ -27,7 +27,7 @@ function findManifestAction(manifest, intent) {
|
|
|
27
27
|
* never executed here — they return `needs-confirmation`.
|
|
28
28
|
*/
|
|
29
29
|
export async function decideAction(params) {
|
|
30
|
-
const { action, manifest, executor, context, messages, userMessage } = params;
|
|
30
|
+
const { action, manifest, executor, context, messages, userMessage, suppressEscort } = params;
|
|
31
31
|
if (!action || action.type === 'none') {
|
|
32
32
|
return { kind: 'noop' };
|
|
33
33
|
}
|
|
@@ -39,7 +39,7 @@ export async function decideAction(params) {
|
|
|
39
39
|
const manifestAction = findManifestAction(manifest, action.intent);
|
|
40
40
|
const needsConfirmation = manifestAction?.requireConfirmation === true;
|
|
41
41
|
if (!needsConfirmation) {
|
|
42
|
-
const result = await executor.execute(action, context);
|
|
42
|
+
const result = await executor.execute(action, context, { suppressEscort });
|
|
43
43
|
return { kind: 'executed', result };
|
|
44
44
|
}
|
|
45
45
|
// Precheck (context + idempotency) BEFORE asking: applying an already-applied
|
|
@@ -21,6 +21,7 @@ import { AgentController } from '../controller/AgentController.js';
|
|
|
21
21
|
import { mountOrb } from '../orb/mountOrb.js';
|
|
22
22
|
import { showOrbDevError } from '../orb/devErrorBadge.js';
|
|
23
23
|
import { DEFAULT_WEB_API_URL, fetchClientManifest, sendOrbSeen } from '../api/clientBootstrap.js';
|
|
24
|
+
import { withAnonCallerFallback } from '../api/anonUserId.js';
|
|
24
25
|
function defaultNavigate(route) {
|
|
25
26
|
if (typeof window !== 'undefined' && window.location)
|
|
26
27
|
window.location.assign(route);
|
|
@@ -60,15 +61,18 @@ function assemble(options, manifest, baseUrl) {
|
|
|
60
61
|
const fallback = browserLocale();
|
|
61
62
|
const locale = resolveAgentLocale(options);
|
|
62
63
|
const sttLocale = options.sttLocale ?? options.locale ?? fallback.sttLocale;
|
|
64
|
+
// Rate-limit fairness: anonymous visitors still send a stable per-browser
|
|
65
|
+
// caller id (never used for conversation identity — that stays 'anon').
|
|
66
|
+
const callerId = withAnonCallerFallback(options.getUserId);
|
|
63
67
|
const backend = createFiodosBackendClient({
|
|
64
68
|
baseUrl,
|
|
65
69
|
apiKey: options.apiKey,
|
|
66
|
-
getUserId:
|
|
70
|
+
getUserId: callerId,
|
|
67
71
|
});
|
|
68
72
|
const telemetry = createFiodosTelemetry({
|
|
69
73
|
baseUrl,
|
|
70
74
|
apiKey: options.apiKey,
|
|
71
|
-
getUserId:
|
|
75
|
+
getUserId: callerId,
|
|
72
76
|
});
|
|
73
77
|
const config = {
|
|
74
78
|
manifest,
|
|
@@ -27,7 +27,7 @@ import { createScreenContextStore } from '../context/screenContextStore.js';
|
|
|
27
27
|
import { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId } from './dynamics.js';
|
|
28
28
|
import { buildApiActionRegistries, validateManifest } from '@fiodos/core';
|
|
29
29
|
declare const api: {
|
|
30
|
-
readonly version: "0.1.
|
|
30
|
+
readonly version: "0.1.19";
|
|
31
31
|
readonly createFiodosAgent: typeof createFiodosAgent;
|
|
32
32
|
readonly mountOrb: typeof mountOrb;
|
|
33
33
|
readonly AgentController: typeof AgentController;
|
package/dist/esm/embed/global.js
CHANGED
|
@@ -26,7 +26,11 @@ import { AgentController } from '../controller/AgentController.js';
|
|
|
26
26
|
import { createScreenContextStore } from '../context/screenContextStore.js';
|
|
27
27
|
import { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId, } from './dynamics.js';
|
|
28
28
|
import { SDK_VERSION } from '../version.js';
|
|
29
|
+
import { markEmbedDistribution } from '../api/clientBootstrap.js';
|
|
29
30
|
import { buildApiActionRegistries, validateManifest } from '@fiodos/core';
|
|
31
|
+
// This bundle is served from a CDN `@latest` URL and updates itself: mark the
|
|
32
|
+
// heartbeat so the dashboard never shows an npm update command for it.
|
|
33
|
+
markEmbedDistribution();
|
|
30
34
|
const api = {
|
|
31
35
|
version: SDK_VERSION,
|
|
32
36
|
createFiodosAgent,
|
|
@@ -25,12 +25,29 @@ export function createBubbleDictation(locale) {
|
|
|
25
25
|
let listening = false;
|
|
26
26
|
let baseText = '';
|
|
27
27
|
const stop = () => {
|
|
28
|
-
recognition
|
|
28
|
+
const rec = recognition;
|
|
29
|
+
if (!rec)
|
|
30
|
+
return;
|
|
31
|
+
// DISCARD-ON-STOP: recognition engines fire one last consolidated
|
|
32
|
+
// `onresult` AFTER stop() is requested. If the user hits send while still
|
|
33
|
+
// dictating, that late result would rewrite the input we just cleared and
|
|
34
|
+
// the message would reappear (duplicate-send bug). Once a stop is
|
|
35
|
+
// requested, nothing may ever write to the input again.
|
|
36
|
+
rec.onresult = null;
|
|
37
|
+
recognition = null;
|
|
38
|
+
listening = false;
|
|
39
|
+
try {
|
|
40
|
+
if (typeof rec.abort === 'function')
|
|
41
|
+
rec.abort();
|
|
42
|
+
else
|
|
43
|
+
rec.stop();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* already stopped */
|
|
47
|
+
}
|
|
29
48
|
};
|
|
30
49
|
const dispose = () => {
|
|
31
50
|
stop();
|
|
32
|
-
recognition = null;
|
|
33
|
-
listening = false;
|
|
34
51
|
};
|
|
35
52
|
return {
|
|
36
53
|
isSupported: () => Ctor != null,
|
|
@@ -68,11 +85,18 @@ export function createBubbleDictation(locale) {
|
|
|
68
85
|
: dictated;
|
|
69
86
|
setValue(combined);
|
|
70
87
|
};
|
|
88
|
+
// Identity-guarded teardown: a session stopped via stop() may fire its
|
|
89
|
+
// onend AFTER the user already started a NEW dictation — it must never
|
|
90
|
+
// tear down the new session.
|
|
71
91
|
rec.onend = () => {
|
|
92
|
+
if (recognition !== rec)
|
|
93
|
+
return;
|
|
72
94
|
listening = false;
|
|
73
95
|
recognition = null;
|
|
74
96
|
};
|
|
75
97
|
rec.onerror = () => {
|
|
98
|
+
if (recognition !== rec)
|
|
99
|
+
return;
|
|
76
100
|
listening = false;
|
|
77
101
|
recognition = null;
|
|
78
102
|
};
|
package/dist/esm/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.19";
|
|
9
9
|
export declare const SDK_PACKAGE = "@fiodos/web-core";
|
package/dist/esm/version.js
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 const SDK_VERSION = '0.1.
|
|
8
|
+
export const SDK_VERSION = '0.1.19';
|
|
9
9
|
export const SDK_PACKAGE = '@fiodos/web-core';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/web-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Framework-agnostic browser layer for the Fiodos agent: a vanilla AgentController (orchestrator), DOM adapters (navigation/voice/storage), HTTP client and decision engine over @fiodos/core. Shared base for @fiodos/vue, @fiodos/svelte and @fiodos/angular (no framework imports).",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"prepublishOnly": "node ../../scripts/sync-sdk-version.mjs && npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@fiodos/core": "^0.1.
|
|
33
|
+
"@fiodos/core": "^0.1.13"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.0.0",
|