@fiodos/web-core 0.1.14 → 0.1.17
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/backendClient.d.ts +10 -3
- package/dist/cjs/api/backendClient.js +21 -9
- package/dist/cjs/config/types.d.ts +7 -0
- package/dist/cjs/controller/AgentController.js +20 -2
- package/dist/cjs/dropin/createFiodosAgent.js +16 -3
- package/dist/cjs/embed/global.d.ts +1 -1
- package/dist/cjs/orb/mountOrb.js +99 -10
- package/dist/cjs/orb/orbView.js +5 -4
- 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/backendClient.d.ts +10 -3
- package/dist/esm/api/backendClient.js +22 -10
- package/dist/esm/config/types.d.ts +7 -0
- package/dist/esm/controller/AgentController.js +21 -3
- package/dist/esm/dropin/createFiodosAgent.js +16 -3
- package/dist/esm/embed/global.d.ts +1 -1
- package/dist/esm/orb/mountOrb.js +99 -10
- package/dist/esm/orb/orbView.js +5 -4
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* way. The only platform dependency is `fetch` + `AbortController` (native in
|
|
7
7
|
* the browser). No client source code is ever sent — only the manifest payload.
|
|
8
8
|
*/
|
|
9
|
-
import { type AgentBackendClient, type AppManifest } from '@fiodos/core';
|
|
9
|
+
import { type ActionRegistries, type AgentBackendClient, type AppManifest } from '@fiodos/core';
|
|
10
10
|
export interface FiodosBackendClientOptions {
|
|
11
11
|
baseUrl: string;
|
|
12
12
|
/** Client API key sent as x-api-key. */
|
|
@@ -19,8 +19,15 @@ export interface FiodosBackendClientOptions {
|
|
|
19
19
|
ttsTimeoutMs?: number;
|
|
20
20
|
}
|
|
21
21
|
export declare function createFiodosBackendClient(options: FiodosBackendClientOptions): AgentBackendClient;
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Serializes the manifest payloads for a turn request (used by AgentController).
|
|
24
|
+
*
|
|
25
|
+
* When the live registries are provided, actions with no registered handler
|
|
26
|
+
* are dropped from the payload (runtime "wired or nonexistent"): the LLM never
|
|
27
|
+
* hears about an action this mounted app cannot execute, so it can never
|
|
28
|
+
* promise it. Fail-open without registries.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildManifestPayload(manifest: AppManifest, registries?: ActionRegistries | null): {
|
|
24
31
|
appFlow?: string | undefined;
|
|
25
32
|
appType?: string | undefined;
|
|
26
33
|
manifestVersion: string;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* way. The only platform dependency is `fetch` + `AbortController` (native in
|
|
7
7
|
* the browser). No client source code is ever sent — only the manifest payload.
|
|
8
8
|
*/
|
|
9
|
-
import { manifestActionsForBackend, manifestRoutesForBackend, parseAgentAction, } from '@fiodos/core';
|
|
9
|
+
import { manifestActionsForBackend, manifestRoutesForBackend, parseAgentAction, withExecutableActions, } from '@fiodos/core';
|
|
10
10
|
import { AgentApiError } from './errors.js';
|
|
11
11
|
const DEFAULT_TURN_TIMEOUT_MS = 15000;
|
|
12
12
|
const DEFAULT_TTS_TIMEOUT_MS = 12000;
|
|
@@ -141,6 +141,10 @@ export function createFiodosBackendClient(options) {
|
|
|
141
141
|
intent: request.lastStep.intent,
|
|
142
142
|
...(request.lastStep.label ? { label: request.lastStep.label } : {}),
|
|
143
143
|
success: request.lastStep.success,
|
|
144
|
+
// Bounded result payload of the executed step (product search
|
|
145
|
+
// results…): the backend renders it so the model can read real
|
|
146
|
+
// ids for the NEXT step's parameters.
|
|
147
|
+
...(request.lastStep.data ? { data: request.lastStep.data } : {}),
|
|
144
148
|
};
|
|
145
149
|
}
|
|
146
150
|
}
|
|
@@ -198,15 +202,23 @@ export function createFiodosBackendClient(options) {
|
|
|
198
202
|
},
|
|
199
203
|
};
|
|
200
204
|
}
|
|
201
|
-
/**
|
|
202
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Serializes the manifest payloads for a turn request (used by AgentController).
|
|
207
|
+
*
|
|
208
|
+
* When the live registries are provided, actions with no registered handler
|
|
209
|
+
* are dropped from the payload (runtime "wired or nonexistent"): the LLM never
|
|
210
|
+
* hears about an action this mounted app cannot execute, so it can never
|
|
211
|
+
* promise it. Fail-open without registries.
|
|
212
|
+
*/
|
|
213
|
+
export function buildManifestPayload(manifest, registries) {
|
|
214
|
+
const effective = withExecutableActions(manifest, registries);
|
|
203
215
|
return {
|
|
204
|
-
manifestVersion:
|
|
205
|
-
manifestRoutes: manifestRoutesForBackend(
|
|
206
|
-
manifestActions: manifestActionsForBackend(
|
|
207
|
-
appName:
|
|
208
|
-
appDescription:
|
|
209
|
-
...(
|
|
210
|
-
...(
|
|
216
|
+
manifestVersion: effective.version,
|
|
217
|
+
manifestRoutes: manifestRoutesForBackend(effective),
|
|
218
|
+
manifestActions: manifestActionsForBackend(effective),
|
|
219
|
+
appName: effective.appName,
|
|
220
|
+
appDescription: effective.appDescription,
|
|
221
|
+
...(effective.appType ? { appType: effective.appType } : {}),
|
|
222
|
+
...(effective.appFlow ? { appFlow: effective.appFlow } : {}),
|
|
211
223
|
};
|
|
212
224
|
}
|
|
@@ -44,6 +44,13 @@ export interface FiodosWebAgentConfig {
|
|
|
44
44
|
manifest: AppManifest;
|
|
45
45
|
/** Agent locale (reply language, message catalogs). MANDATORY. */
|
|
46
46
|
locale: string;
|
|
47
|
+
/**
|
|
48
|
+
* LIVE locale resolver. When set, the orb re-reads it periodically so the
|
|
49
|
+
* thought bubble follows the app's ACTIVE language (host language switcher,
|
|
50
|
+
* `<html lang>`, i18n runtime) without remounting. Falls back to `locale`
|
|
51
|
+
* whenever it returns nothing.
|
|
52
|
+
*/
|
|
53
|
+
getLocale?: () => string | null | undefined;
|
|
47
54
|
/** Speech recognition locale, e.g. 'en-US', 'es-ES'. MANDATORY. */
|
|
48
55
|
sttLocale: string;
|
|
49
56
|
/** Device-TTS fallback locale. Defaults to sttLocale. */
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* deliberate modal tap; a loose "yes" never confirms them.
|
|
16
16
|
* - The agent only ever executes actions declared in the manifest.
|
|
17
17
|
*/
|
|
18
|
-
import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, buildBubbleExchangeWindow, BUBBLE_PAGE_EXCHANGES, clearPersistedConversation, conversationIdentityKey, conversationRetentionOptions, createConversationSession, persistConversationSession, restoreConversationSession, defaultRouteContextText, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, validateManifest, } from '@fiodos/core';
|
|
18
|
+
import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, buildBubbleExchangeWindow, BUBBLE_PAGE_EXCHANGES, clearPersistedConversation, conversationIdentityKey, conversationRetentionOptions, createConversationSession, persistConversationSession, restoreConversationSession, defaultRouteContextText, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, trimChainStepData, validateManifest, } from '@fiodos/core';
|
|
19
19
|
import { createWebCredentialAutofillAdapter } from '../adapters/webCredentialAutofillAdapter.js';
|
|
20
20
|
import { AgentApiError } from '../api/errors.js';
|
|
21
21
|
import { formatAgentTurnError } from '../api/formatTurnError.js';
|
|
@@ -713,9 +713,22 @@ export class AgentController {
|
|
|
713
713
|
/* keep the assistant's guiding reply; never speak a refusal. */
|
|
714
714
|
}
|
|
715
715
|
else if (r.message) {
|
|
716
|
+
// FEEDBACK GUARANTEE: the handler's outcome message (success,
|
|
717
|
+
// idempotent or failure) is the ground truth that the action ran;
|
|
718
|
+
// speak it instead of the LLM's optimistic reply.
|
|
716
719
|
outReply = r.message;
|
|
717
720
|
outAudio = null;
|
|
718
721
|
}
|
|
722
|
+
else if (!r.success) {
|
|
723
|
+
// Same guarantee for a handler that failed WITHOUT setting `message`
|
|
724
|
+
// (e.g. a declarative Shopify/OData `execution` call that hit a
|
|
725
|
+
// network/HTTP/template error — apiActions.ts never throws, so
|
|
726
|
+
// runManifestAction's catch never runs either). Without this, the
|
|
727
|
+
// LLM's pre-action narration ("Adding X to your cart…") stays on
|
|
728
|
+
// screen as if it had happened — a silent false success.
|
|
729
|
+
outReply = this.messages.actionFailed;
|
|
730
|
+
outAudio = null;
|
|
731
|
+
}
|
|
719
732
|
// Chain signal: only a cleanly EXECUTED, non-recoverable step may advance,
|
|
720
733
|
// and only when the backend asked to continue (task_status=in_progress).
|
|
721
734
|
if (turn.action &&
|
|
@@ -723,11 +736,16 @@ export class AgentController {
|
|
|
723
736
|
r.success &&
|
|
724
737
|
!r.recoverable) {
|
|
725
738
|
const intent = turn.action.intent ?? '';
|
|
739
|
+
// Feed the step's RESULT DATA back to the next chain turn (bounded):
|
|
740
|
+
// it is how the model reads real technical values (variant ids, line
|
|
741
|
+
// numbers) out of a data action instead of inventing them.
|
|
742
|
+
const chainData = trimChainStepData(r.data);
|
|
726
743
|
lastStep = {
|
|
727
744
|
type: turn.action.type,
|
|
728
745
|
intent,
|
|
729
746
|
label: turn.action.type === 'navigate' ? this.routeLabel(intent) : this.actionLabel(intent),
|
|
730
747
|
success: true,
|
|
748
|
+
...(chainData ? { data: chainData } : {}),
|
|
731
749
|
};
|
|
732
750
|
actionKey = `${turn.action.type}:${intent}`;
|
|
733
751
|
canContinue = turn.taskStatus === 'in_progress';
|
|
@@ -827,7 +845,7 @@ export class AgentController {
|
|
|
827
845
|
lastStep,
|
|
828
846
|
platformCapabilities: this.platformCapabilities(),
|
|
829
847
|
sessionContext: this.sessionContext(),
|
|
830
|
-
...buildManifestPayload(this.config.manifest),
|
|
848
|
+
...buildManifestPayload(this.config.manifest, this.config.registries),
|
|
831
849
|
}, { signal: controller.signal });
|
|
832
850
|
oc = await this.applyOutcome(turn, routeNow, snap?.context ?? {}, state.userMessage);
|
|
833
851
|
}
|
|
@@ -986,7 +1004,7 @@ export class AgentController {
|
|
|
986
1004
|
sessionId: this.sessionId,
|
|
987
1005
|
platformCapabilities: this.platformCapabilities(),
|
|
988
1006
|
sessionContext: this.sessionContext(),
|
|
989
|
-
...buildManifestPayload(this.config.manifest),
|
|
1007
|
+
...buildManifestPayload(this.config.manifest, this.config.registries),
|
|
990
1008
|
}, { signal: controller.signal });
|
|
991
1009
|
outcome = await this.applyOutcome(turn, currentRoute, snapshot?.context ?? {}, text);
|
|
992
1010
|
}
|
|
@@ -29,6 +29,11 @@ function browserLocale() {
|
|
|
29
29
|
const lang = typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'en-US';
|
|
30
30
|
return { locale: lang.split('-')[0] || 'en', sttLocale: lang };
|
|
31
31
|
}
|
|
32
|
+
function documentLocale() {
|
|
33
|
+
return typeof document !== 'undefined'
|
|
34
|
+
? document.documentElement.lang?.split(/[-_]/)[0]?.trim() || ''
|
|
35
|
+
: '';
|
|
36
|
+
}
|
|
32
37
|
function resolveAgentLocale(options) {
|
|
33
38
|
const fromApp = options.getLocale?.()?.trim();
|
|
34
39
|
if (fromApp)
|
|
@@ -38,12 +43,16 @@ function resolveAgentLocale(options) {
|
|
|
38
43
|
if (options.localeDetection === 'browser')
|
|
39
44
|
return browserLocale().locale;
|
|
40
45
|
if (options.localeDetection === 'document') {
|
|
41
|
-
const docLang =
|
|
42
|
-
? document.documentElement.lang?.split(/[-_]/)[0]?.trim()
|
|
43
|
-
: '';
|
|
46
|
+
const docLang = documentLocale();
|
|
44
47
|
if (docLang)
|
|
45
48
|
return docLang;
|
|
46
49
|
}
|
|
50
|
+
// Default (nothing configured): follow the HOST PAGE's language when the
|
|
51
|
+
// page declares one (<html lang>), so the orb bubble speaks the language the
|
|
52
|
+
// end user is actually browsing in. English only as the last resort.
|
|
53
|
+
const docLang = documentLocale();
|
|
54
|
+
if (docLang)
|
|
55
|
+
return docLang;
|
|
47
56
|
return 'en';
|
|
48
57
|
}
|
|
49
58
|
/** Build the controller + adapters from a known manifest (shared by both paths). */
|
|
@@ -64,6 +73,10 @@ function assemble(options, manifest, baseUrl) {
|
|
|
64
73
|
const config = {
|
|
65
74
|
manifest,
|
|
66
75
|
locale,
|
|
76
|
+
// Live resolver: re-runs the same resolution chain (app getLocale → static
|
|
77
|
+
// locale → detection) on every read, so the bubble follows the host app's
|
|
78
|
+
// language switcher / <html lang> without remounting the agent.
|
|
79
|
+
getLocale: () => resolveAgentLocale(options),
|
|
67
80
|
sttLocale,
|
|
68
81
|
navigation: createWebNavigationAdapter({
|
|
69
82
|
navigate: options.navigate ?? defaultNavigate,
|
|
@@ -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.17";
|
|
31
31
|
readonly createFiodosAgent: typeof createFiodosAgent;
|
|
32
32
|
readonly mountOrb: typeof mountOrb;
|
|
33
33
|
readonly AgentController: typeof AgentController;
|
package/dist/esm/orb/mountOrb.js
CHANGED
|
@@ -210,12 +210,13 @@ export function mountOrb(controller, options = {}) {
|
|
|
210
210
|
const prompt = resolveCurrentBubblePrompt();
|
|
211
211
|
openTextPanel(prompt
|
|
212
212
|
? bubblePromptToAffirmative(prompt, {
|
|
213
|
-
locale:
|
|
213
|
+
locale: currentBubbleLocale(),
|
|
214
214
|
messages: controller.messages,
|
|
215
215
|
})
|
|
216
216
|
: undefined);
|
|
217
217
|
return;
|
|
218
218
|
}
|
|
219
|
+
userTriedVoice = true;
|
|
219
220
|
void controller.toggleListening();
|
|
220
221
|
});
|
|
221
222
|
thoughtBubbleEl.addEventListener('keydown', (event) => {
|
|
@@ -394,6 +395,13 @@ export function mountOrb(controller, options = {}) {
|
|
|
394
395
|
// SAME "active/listening" visual state and reveals the keyboard chip — the
|
|
395
396
|
// user is never blocked, text is always an open avenue (Change 1).
|
|
396
397
|
let micFallbackActive = false;
|
|
398
|
+
// True once the user actually ATTEMPTED voice (tapped the orb/bubble to
|
|
399
|
+
// listen). The voiceBlocked→keyboard fallback in render() must only fire for
|
|
400
|
+
// a mic that failed MID-ATTEMPT: the upfront Permissions-API detection also
|
|
401
|
+
// sets voiceBlocked at mount (e.g. the site has the mic permission denied),
|
|
402
|
+
// and without this guard the conversation panel would pop open on page load
|
|
403
|
+
// with zero interaction.
|
|
404
|
+
let userTriedVoice = false;
|
|
397
405
|
const bubble = document.createElement('div');
|
|
398
406
|
bubble.className = 'fyodos-bubble';
|
|
399
407
|
bubble.style.display = 'none';
|
|
@@ -462,6 +470,12 @@ export function mountOrb(controller, options = {}) {
|
|
|
462
470
|
signature.endsWith(renderedSignature);
|
|
463
471
|
const fromBottom = exchangeScroll.scrollHeight - exchangeScroll.scrollTop;
|
|
464
472
|
renderedSignature = signature;
|
|
473
|
+
// Theme colors INLINE at node creation: the embed lives inside arbitrary
|
|
474
|
+
// host pages (e.g. Shopify themes) whose CSS sets `color` on everything,
|
|
475
|
+
// so a freshly-rendered message must never wait for the next
|
|
476
|
+
// applyBubbleTheme() pass to become readable (it would inherit the host's
|
|
477
|
+
// ink — black on our dark panel — until the next config poll).
|
|
478
|
+
const t = { ...DEFAULT_MODAL_THEME, ...(modalTheme ?? {}) };
|
|
465
479
|
exchangeList.replaceChildren();
|
|
466
480
|
for (const ex of exchanges) {
|
|
467
481
|
const block = document.createElement('div');
|
|
@@ -469,11 +483,13 @@ export function mountOrb(controller, options = {}) {
|
|
|
469
483
|
const user = document.createElement('div');
|
|
470
484
|
user.className = 'fyodos-bubble-user';
|
|
471
485
|
user.textContent = ex.userText;
|
|
486
|
+
user.style.color = t.bodyColor;
|
|
472
487
|
block.append(user);
|
|
473
488
|
if (ex.replyText) {
|
|
474
489
|
const reply = document.createElement('div');
|
|
475
490
|
reply.className = 'fyodos-bubble-reply';
|
|
476
491
|
reply.textContent = ex.replyText;
|
|
492
|
+
reply.style.color = t.titleColor;
|
|
477
493
|
block.append(reply);
|
|
478
494
|
}
|
|
479
495
|
exchangeList.append(block);
|
|
@@ -580,7 +596,8 @@ export function mountOrb(controller, options = {}) {
|
|
|
580
596
|
if (active) {
|
|
581
597
|
micBtn.style.background = accent;
|
|
582
598
|
micBtn.style.border = 'none';
|
|
583
|
-
|
|
599
|
+
// Ink readable on the (customizable) accent — dark accents need light ink.
|
|
600
|
+
micBtn.style.color = fiodosInputInkColor(accent);
|
|
584
601
|
micBtn.style.borderRadius = sendRadius;
|
|
585
602
|
}
|
|
586
603
|
else {
|
|
@@ -679,6 +696,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
679
696
|
textInput.style.color = fiodosInputInkColor(inputBg);
|
|
680
697
|
textInput.style.setProperty('--fyodos-input-placeholder', fiodosInputMutedInkColor(inputBg));
|
|
681
698
|
sendBtn.style.background = accent;
|
|
699
|
+
// The glyph (↑ / ◼) must stay readable on ANY published accent: dark ink
|
|
700
|
+
// on light accents, light ink on dark ones (never the hardcoded black).
|
|
701
|
+
sendBtn.style.color = fiodosInputInkColor(accent);
|
|
682
702
|
sendBtn.style.borderRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
|
|
683
703
|
syncMicButton();
|
|
684
704
|
busyRow.querySelectorAll('span').forEach((s) => {
|
|
@@ -687,13 +707,44 @@ export function mountOrb(controller, options = {}) {
|
|
|
687
707
|
// Live-activity line: white (reply color), italic — not the faint gray.
|
|
688
708
|
busyStep.style.color = t.titleColor;
|
|
689
709
|
}
|
|
710
|
+
// The panel's open state survives FULL PAGE LOADS within the tab session:
|
|
711
|
+
// on multi-page hosts (Shopify storefronts…) every agent navigation reloads
|
|
712
|
+
// the document, and without this the conversation panel closed mid-dialogue
|
|
713
|
+
// on every step. sessionStorage is per-tab and cleared when the tab closes,
|
|
714
|
+
// so a fresh visit never starts with the panel open.
|
|
715
|
+
const panelOpenKey = `fyodos:panel-open:${controller.config.manifest.appId}`;
|
|
716
|
+
function persistPanelOpen(open) {
|
|
717
|
+
try {
|
|
718
|
+
if (open)
|
|
719
|
+
window.sessionStorage.setItem(panelOpenKey, '1');
|
|
720
|
+
else
|
|
721
|
+
window.sessionStorage.removeItem(panelOpenKey);
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
/* storage unavailable (private mode) — persistence is best-effort */
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function wasPanelOpen() {
|
|
728
|
+
try {
|
|
729
|
+
return window.sessionStorage.getItem(panelOpenKey) === '1';
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
690
735
|
function setBubble(open) {
|
|
691
736
|
bubbleOpen = open;
|
|
692
737
|
bubble.style.display = open ? 'flex' : 'none';
|
|
738
|
+
persistPanelOpen(open);
|
|
693
739
|
if (open) {
|
|
694
740
|
applyBubbleTheme();
|
|
695
741
|
positionBubble();
|
|
696
742
|
textInput.focus();
|
|
743
|
+
// Paint the CURRENT conversation immediately: exchanges are only
|
|
744
|
+
// rendered inside render() while the panel is open, so without this
|
|
745
|
+
// the restored history stayed invisible (an apparently stuck panel)
|
|
746
|
+
// until the next controller event — i.e. until the user typed.
|
|
747
|
+
render(controller.getState());
|
|
697
748
|
}
|
|
698
749
|
else {
|
|
699
750
|
bubbleDictation.stop();
|
|
@@ -707,6 +758,13 @@ export function mountOrb(controller, options = {}) {
|
|
|
707
758
|
setBubbleExpanded(false);
|
|
708
759
|
}
|
|
709
760
|
syncChip(controller.getState());
|
|
761
|
+
// Re-resolve the thought bubble NOW: closing the panel must bring the
|
|
762
|
+
// screen-aware CTA back immediately. Nothing else is guaranteed to run
|
|
763
|
+
// right after close (controller state does not change on close, the route
|
|
764
|
+
// poll only fires on changes, the config poll every ~12s) — without this
|
|
765
|
+
// the orb sat "naked" until some unrelated event. Opening hides it (the
|
|
766
|
+
// resolver returns null while the panel is open), so one call covers both.
|
|
767
|
+
syncThoughtBubble(controller.getState());
|
|
710
768
|
}
|
|
711
769
|
function submitText() {
|
|
712
770
|
// While a turn is in flight the button is a STOP button; never start a new
|
|
@@ -771,8 +829,22 @@ export function mountOrb(controller, options = {}) {
|
|
|
771
829
|
applyDeployment();
|
|
772
830
|
}
|
|
773
831
|
// ── Thought bubble (screen-aware CTA beside the orb) ─────────────────────────
|
|
832
|
+
// LIVE locale: re-read the host app's active language on every resolution so
|
|
833
|
+
// the bubble text (analyzer `bubblePrompts` translations) follows the app's
|
|
834
|
+
// language switcher without remounting. Falls back to the mount-time locale.
|
|
835
|
+
function currentBubbleLocale() {
|
|
836
|
+
try {
|
|
837
|
+
const live = controller.config.getLocale?.();
|
|
838
|
+
if (typeof live === 'string' && live.trim())
|
|
839
|
+
return live.trim();
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
/* live locale read is best-effort */
|
|
843
|
+
}
|
|
844
|
+
return controller.config.locale;
|
|
845
|
+
}
|
|
774
846
|
function resolveCurrentBubblePrompt() {
|
|
775
|
-
return resolveBubblePrompt(matchRouteIntent(currentRoute, controller.config.manifest.routes), controller.messages, { locale:
|
|
847
|
+
return resolveBubblePrompt(matchRouteIntent(currentRoute, controller.config.manifest.routes), controller.messages, { locale: currentBubbleLocale(), actions: controller.config.manifest.actions });
|
|
776
848
|
}
|
|
777
849
|
// Render the CTA on ONE line at its NATURAL size. When the phrase is too long
|
|
778
850
|
// to fit the max width, scroll it (marquee, right-to-left, looping) instead of
|
|
@@ -987,6 +1059,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
987
1059
|
// denied/blocked, in which case we skip voice and go straight to the keyboard.
|
|
988
1060
|
if (controller.voiceAvailable && voiceInputEnabled) {
|
|
989
1061
|
micFallbackActive = false;
|
|
1062
|
+
userTriedVoice = true;
|
|
990
1063
|
void controller.toggleListening();
|
|
991
1064
|
return;
|
|
992
1065
|
}
|
|
@@ -998,6 +1071,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
998
1071
|
openTextPanel();
|
|
999
1072
|
return;
|
|
1000
1073
|
}
|
|
1074
|
+
userTriedVoice = true;
|
|
1001
1075
|
void controller.toggleListening();
|
|
1002
1076
|
};
|
|
1003
1077
|
sendBtn.onclick = () => {
|
|
@@ -1036,11 +1110,15 @@ export function mountOrb(controller, options = {}) {
|
|
|
1036
1110
|
// Track phase transitions to fire haptics: start/stop listening + response.
|
|
1037
1111
|
let lastPhase = null;
|
|
1038
1112
|
function render(state) {
|
|
1039
|
-
// Mic just became unusable
|
|
1040
|
-
// keyboard so the user always has a working channel.
|
|
1113
|
+
// Mic just became unusable MID-ATTEMPT (no Permissions API): fall back to
|
|
1114
|
+
// the keyboard so the user always has a working channel. Guarded by
|
|
1115
|
+
// userTriedVoice: the upfront permission detection also sets voiceBlocked
|
|
1116
|
+
// right at mount (site-level mic denial), and that must NEVER auto-open
|
|
1117
|
+
// the conversation panel on page load — the user did nothing yet; their
|
|
1118
|
+
// first orb tap will route to the keyboard via the voiceAvailable check.
|
|
1041
1119
|
if (state.voiceBlocked && !lastVoiceBlocked) {
|
|
1042
1120
|
lastVoiceBlocked = true;
|
|
1043
|
-
if (enableTextInput && !bubbleOpen && !state.isBusy) {
|
|
1121
|
+
if (userTriedVoice && enableTextInput && !bubbleOpen && !state.isBusy) {
|
|
1044
1122
|
openTextPanel();
|
|
1045
1123
|
return;
|
|
1046
1124
|
}
|
|
@@ -1109,9 +1187,18 @@ export function mountOrb(controller, options = {}) {
|
|
|
1109
1187
|
}
|
|
1110
1188
|
const unsubscribe = controller.subscribe(render);
|
|
1111
1189
|
render(controller.getState());
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
1114
|
-
//
|
|
1190
|
+
// Re-open the conversation panel after a full page load when it was open
|
|
1191
|
+
// before (MPA hosts reload on every navigation): the dialogue visually
|
|
1192
|
+
// continues where it was — the persisted conversation restores async in the
|
|
1193
|
+
// controller and repaints via emit → render once loaded.
|
|
1194
|
+
if (enableTextInput && wasPanelOpen()) {
|
|
1195
|
+
setBubble(true);
|
|
1196
|
+
}
|
|
1197
|
+
// Track the current route AND the app's live locale so the thought bubble
|
|
1198
|
+
// re-resolves its CTA on navigation and on language switches. No universal
|
|
1199
|
+
// navigation/i18n event exists, so poll + listen to popstate / visibility
|
|
1200
|
+
// for snappier updates.
|
|
1201
|
+
let lastBubbleLocale = currentBubbleLocale();
|
|
1115
1202
|
const readRoute = () => {
|
|
1116
1203
|
let next = null;
|
|
1117
1204
|
try {
|
|
@@ -1120,8 +1207,10 @@ export function mountOrb(controller, options = {}) {
|
|
|
1120
1207
|
catch {
|
|
1121
1208
|
/* navigation read is best-effort */
|
|
1122
1209
|
}
|
|
1123
|
-
|
|
1210
|
+
const nextLocale = currentBubbleLocale();
|
|
1211
|
+
if (next !== currentRoute || nextLocale !== lastBubbleLocale) {
|
|
1124
1212
|
currentRoute = next;
|
|
1213
|
+
lastBubbleLocale = nextLocale;
|
|
1125
1214
|
syncThoughtBubble(controller.getState());
|
|
1126
1215
|
}
|
|
1127
1216
|
};
|
package/dist/esm/orb/orbView.js
CHANGED
|
@@ -74,13 +74,14 @@ function buildDoubleBorder(opts) {
|
|
|
74
74
|
return { root, fill };
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
77
|
+
* Fiodos brand mark (idle, no logo). Breathes very gently on web — a
|
|
78
78
|
* direct port of the landing hero orbs' waveform easing (see
|
|
79
79
|
* `fiodosOrbMarkIdleHalfHeight` in @fiodos/core) — deliberately subtler than
|
|
80
80
|
* and distinct from the volume-reactive waveform shown while listening.
|
|
81
|
+
* Tinted with the same "audio color" (waveformColor) as that waveform.
|
|
81
82
|
*/
|
|
82
|
-
function buildOrbMark(orbSize) {
|
|
83
|
-
const { bars, color, radius } = resolveFiodosOrbMarkAnimatedBars(orbSize);
|
|
83
|
+
function buildOrbMark(orbSize, markColor) {
|
|
84
|
+
const { bars, color, radius } = resolveFiodosOrbMarkAnimatedBars(orbSize, markColor);
|
|
84
85
|
const svg = svgEl('svg', {
|
|
85
86
|
width: orbSize,
|
|
86
87
|
height: orbSize,
|
|
@@ -484,7 +485,7 @@ export function createOrbVisual(initial) {
|
|
|
484
485
|
return;
|
|
485
486
|
}
|
|
486
487
|
if (state === 'idle') {
|
|
487
|
-
markAnim = buildOrbMark(size);
|
|
488
|
+
markAnim = buildOrbMark(size, appearance.waveformColor);
|
|
488
489
|
fillEl.appendChild(markAnim.el);
|
|
489
490
|
markAnim.start();
|
|
490
491
|
}
|
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.17";
|
|
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.17';
|
|
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.17",
|
|
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": {
|