@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;
|
|
@@ -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
|
}
|
|
@@ -202,15 +206,23 @@ function createFiodosBackendClient(options) {
|
|
|
202
206
|
},
|
|
203
207
|
};
|
|
204
208
|
}
|
|
205
|
-
/**
|
|
206
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Serializes the manifest payloads for a turn request (used by AgentController).
|
|
211
|
+
*
|
|
212
|
+
* When the live registries are provided, actions with no registered handler
|
|
213
|
+
* are dropped from the payload (runtime "wired or nonexistent"): the LLM never
|
|
214
|
+
* hears about an action this mounted app cannot execute, so it can never
|
|
215
|
+
* promise it. Fail-open without registries.
|
|
216
|
+
*/
|
|
217
|
+
function buildManifestPayload(manifest, registries) {
|
|
218
|
+
const effective = (0, core_1.withExecutableActions)(manifest, registries);
|
|
207
219
|
return {
|
|
208
|
-
manifestVersion:
|
|
209
|
-
manifestRoutes: (0, core_1.manifestRoutesForBackend)(
|
|
210
|
-
manifestActions: (0, core_1.manifestActionsForBackend)(
|
|
211
|
-
appName:
|
|
212
|
-
appDescription:
|
|
213
|
-
...(
|
|
214
|
-
...(
|
|
220
|
+
manifestVersion: effective.version,
|
|
221
|
+
manifestRoutes: (0, core_1.manifestRoutesForBackend)(effective),
|
|
222
|
+
manifestActions: (0, core_1.manifestActionsForBackend)(effective),
|
|
223
|
+
appName: effective.appName,
|
|
224
|
+
appDescription: effective.appDescription,
|
|
225
|
+
...(effective.appType ? { appType: effective.appType } : {}),
|
|
226
|
+
...(effective.appFlow ? { appFlow: effective.appFlow } : {}),
|
|
215
227
|
};
|
|
216
228
|
}
|
|
@@ -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. */
|
|
@@ -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';
|
|
@@ -830,7 +848,7 @@ class AgentController {
|
|
|
830
848
|
lastStep,
|
|
831
849
|
platformCapabilities: this.platformCapabilities(),
|
|
832
850
|
sessionContext: this.sessionContext(),
|
|
833
|
-
...(0, backendClient_1.buildManifestPayload)(this.config.manifest),
|
|
851
|
+
...(0, backendClient_1.buildManifestPayload)(this.config.manifest, this.config.registries),
|
|
834
852
|
}, { signal: controller.signal });
|
|
835
853
|
oc = await this.applyOutcome(turn, routeNow, snap?.context ?? {}, state.userMessage);
|
|
836
854
|
}
|
|
@@ -989,7 +1007,7 @@ class AgentController {
|
|
|
989
1007
|
sessionId: this.sessionId,
|
|
990
1008
|
platformCapabilities: this.platformCapabilities(),
|
|
991
1009
|
sessionContext: this.sessionContext(),
|
|
992
|
-
...(0, backendClient_1.buildManifestPayload)(this.config.manifest),
|
|
1010
|
+
...(0, backendClient_1.buildManifestPayload)(this.config.manifest, this.config.registries),
|
|
993
1011
|
}, { signal: controller.signal });
|
|
994
1012
|
outcome = await this.applyOutcome(turn, currentRoute, snapshot?.context ?? {}, text);
|
|
995
1013
|
}
|
|
@@ -32,6 +32,11 @@ function browserLocale() {
|
|
|
32
32
|
const lang = typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'en-US';
|
|
33
33
|
return { locale: lang.split('-')[0] || 'en', sttLocale: lang };
|
|
34
34
|
}
|
|
35
|
+
function documentLocale() {
|
|
36
|
+
return typeof document !== 'undefined'
|
|
37
|
+
? document.documentElement.lang?.split(/[-_]/)[0]?.trim() || ''
|
|
38
|
+
: '';
|
|
39
|
+
}
|
|
35
40
|
function resolveAgentLocale(options) {
|
|
36
41
|
const fromApp = options.getLocale?.()?.trim();
|
|
37
42
|
if (fromApp)
|
|
@@ -41,12 +46,16 @@ function resolveAgentLocale(options) {
|
|
|
41
46
|
if (options.localeDetection === 'browser')
|
|
42
47
|
return browserLocale().locale;
|
|
43
48
|
if (options.localeDetection === 'document') {
|
|
44
|
-
const docLang =
|
|
45
|
-
? document.documentElement.lang?.split(/[-_]/)[0]?.trim()
|
|
46
|
-
: '';
|
|
49
|
+
const docLang = documentLocale();
|
|
47
50
|
if (docLang)
|
|
48
51
|
return docLang;
|
|
49
52
|
}
|
|
53
|
+
// Default (nothing configured): follow the HOST PAGE's language when the
|
|
54
|
+
// page declares one (<html lang>), so the orb bubble speaks the language the
|
|
55
|
+
// end user is actually browsing in. English only as the last resort.
|
|
56
|
+
const docLang = documentLocale();
|
|
57
|
+
if (docLang)
|
|
58
|
+
return docLang;
|
|
50
59
|
return 'en';
|
|
51
60
|
}
|
|
52
61
|
/** Build the controller + adapters from a known manifest (shared by both paths). */
|
|
@@ -67,6 +76,10 @@ function assemble(options, manifest, baseUrl) {
|
|
|
67
76
|
const config = {
|
|
68
77
|
manifest,
|
|
69
78
|
locale,
|
|
79
|
+
// Live resolver: re-runs the same resolution chain (app getLocale → static
|
|
80
|
+
// locale → detection) on every read, so the bubble follows the host app's
|
|
81
|
+
// language switcher / <html lang> without remounting the agent.
|
|
82
|
+
getLocale: () => resolveAgentLocale(options),
|
|
70
83
|
sttLocale,
|
|
71
84
|
navigation: (0, webNavigationAdapter_1.createWebNavigationAdapter)({
|
|
72
85
|
navigate: options.navigate ?? defaultNavigate,
|
|
@@ -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.17";
|
|
31
31
|
readonly createFiodosAgent: typeof createFiodosAgent;
|
|
32
32
|
readonly mountOrb: typeof mountOrb;
|
|
33
33
|
readonly AgentController: typeof AgentController;
|
package/dist/cjs/orb/mountOrb.js
CHANGED
|
@@ -213,12 +213,13 @@ function mountOrb(controller, options = {}) {
|
|
|
213
213
|
const prompt = resolveCurrentBubblePrompt();
|
|
214
214
|
openTextPanel(prompt
|
|
215
215
|
? (0, core_1.bubblePromptToAffirmative)(prompt, {
|
|
216
|
-
locale:
|
|
216
|
+
locale: currentBubbleLocale(),
|
|
217
217
|
messages: controller.messages,
|
|
218
218
|
})
|
|
219
219
|
: undefined);
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
|
+
userTriedVoice = true;
|
|
222
223
|
void controller.toggleListening();
|
|
223
224
|
});
|
|
224
225
|
thoughtBubbleEl.addEventListener('keydown', (event) => {
|
|
@@ -397,6 +398,13 @@ function mountOrb(controller, options = {}) {
|
|
|
397
398
|
// SAME "active/listening" visual state and reveals the keyboard chip — the
|
|
398
399
|
// user is never blocked, text is always an open avenue (Change 1).
|
|
399
400
|
let micFallbackActive = false;
|
|
401
|
+
// True once the user actually ATTEMPTED voice (tapped the orb/bubble to
|
|
402
|
+
// listen). The voiceBlocked→keyboard fallback in render() must only fire for
|
|
403
|
+
// a mic that failed MID-ATTEMPT: the upfront Permissions-API detection also
|
|
404
|
+
// sets voiceBlocked at mount (e.g. the site has the mic permission denied),
|
|
405
|
+
// and without this guard the conversation panel would pop open on page load
|
|
406
|
+
// with zero interaction.
|
|
407
|
+
let userTriedVoice = false;
|
|
400
408
|
const bubble = document.createElement('div');
|
|
401
409
|
bubble.className = 'fyodos-bubble';
|
|
402
410
|
bubble.style.display = 'none';
|
|
@@ -465,6 +473,12 @@ function mountOrb(controller, options = {}) {
|
|
|
465
473
|
signature.endsWith(renderedSignature);
|
|
466
474
|
const fromBottom = exchangeScroll.scrollHeight - exchangeScroll.scrollTop;
|
|
467
475
|
renderedSignature = signature;
|
|
476
|
+
// Theme colors INLINE at node creation: the embed lives inside arbitrary
|
|
477
|
+
// host pages (e.g. Shopify themes) whose CSS sets `color` on everything,
|
|
478
|
+
// so a freshly-rendered message must never wait for the next
|
|
479
|
+
// applyBubbleTheme() pass to become readable (it would inherit the host's
|
|
480
|
+
// ink — black on our dark panel — until the next config poll).
|
|
481
|
+
const t = { ...DEFAULT_MODAL_THEME, ...(modalTheme ?? {}) };
|
|
468
482
|
exchangeList.replaceChildren();
|
|
469
483
|
for (const ex of exchanges) {
|
|
470
484
|
const block = document.createElement('div');
|
|
@@ -472,11 +486,13 @@ function mountOrb(controller, options = {}) {
|
|
|
472
486
|
const user = document.createElement('div');
|
|
473
487
|
user.className = 'fyodos-bubble-user';
|
|
474
488
|
user.textContent = ex.userText;
|
|
489
|
+
user.style.color = t.bodyColor;
|
|
475
490
|
block.append(user);
|
|
476
491
|
if (ex.replyText) {
|
|
477
492
|
const reply = document.createElement('div');
|
|
478
493
|
reply.className = 'fyodos-bubble-reply';
|
|
479
494
|
reply.textContent = ex.replyText;
|
|
495
|
+
reply.style.color = t.titleColor;
|
|
480
496
|
block.append(reply);
|
|
481
497
|
}
|
|
482
498
|
exchangeList.append(block);
|
|
@@ -583,7 +599,8 @@ function mountOrb(controller, options = {}) {
|
|
|
583
599
|
if (active) {
|
|
584
600
|
micBtn.style.background = accent;
|
|
585
601
|
micBtn.style.border = 'none';
|
|
586
|
-
|
|
602
|
+
// Ink readable on the (customizable) accent — dark accents need light ink.
|
|
603
|
+
micBtn.style.color = (0, core_1.fiodosInputInkColor)(accent);
|
|
587
604
|
micBtn.style.borderRadius = sendRadius;
|
|
588
605
|
}
|
|
589
606
|
else {
|
|
@@ -682,6 +699,9 @@ function mountOrb(controller, options = {}) {
|
|
|
682
699
|
textInput.style.color = (0, core_1.fiodosInputInkColor)(inputBg);
|
|
683
700
|
textInput.style.setProperty('--fyodos-input-placeholder', (0, core_1.fiodosInputMutedInkColor)(inputBg));
|
|
684
701
|
sendBtn.style.background = accent;
|
|
702
|
+
// The glyph (↑ / ◼) must stay readable on ANY published accent: dark ink
|
|
703
|
+
// on light accents, light ink on dark ones (never the hardcoded black).
|
|
704
|
+
sendBtn.style.color = (0, core_1.fiodosInputInkColor)(accent);
|
|
685
705
|
sendBtn.style.borderRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
|
|
686
706
|
syncMicButton();
|
|
687
707
|
busyRow.querySelectorAll('span').forEach((s) => {
|
|
@@ -690,13 +710,44 @@ function mountOrb(controller, options = {}) {
|
|
|
690
710
|
// Live-activity line: white (reply color), italic — not the faint gray.
|
|
691
711
|
busyStep.style.color = t.titleColor;
|
|
692
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
|
+
}
|
|
693
738
|
function setBubble(open) {
|
|
694
739
|
bubbleOpen = open;
|
|
695
740
|
bubble.style.display = open ? 'flex' : 'none';
|
|
741
|
+
persistPanelOpen(open);
|
|
696
742
|
if (open) {
|
|
697
743
|
applyBubbleTheme();
|
|
698
744
|
positionBubble();
|
|
699
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());
|
|
700
751
|
}
|
|
701
752
|
else {
|
|
702
753
|
bubbleDictation.stop();
|
|
@@ -710,6 +761,13 @@ function mountOrb(controller, options = {}) {
|
|
|
710
761
|
setBubbleExpanded(false);
|
|
711
762
|
}
|
|
712
763
|
syncChip(controller.getState());
|
|
764
|
+
// Re-resolve the thought bubble NOW: closing the panel must bring the
|
|
765
|
+
// screen-aware CTA back immediately. Nothing else is guaranteed to run
|
|
766
|
+
// right after close (controller state does not change on close, the route
|
|
767
|
+
// poll only fires on changes, the config poll every ~12s) — without this
|
|
768
|
+
// the orb sat "naked" until some unrelated event. Opening hides it (the
|
|
769
|
+
// resolver returns null while the panel is open), so one call covers both.
|
|
770
|
+
syncThoughtBubble(controller.getState());
|
|
713
771
|
}
|
|
714
772
|
function submitText() {
|
|
715
773
|
// While a turn is in flight the button is a STOP button; never start a new
|
|
@@ -774,8 +832,22 @@ function mountOrb(controller, options = {}) {
|
|
|
774
832
|
applyDeployment();
|
|
775
833
|
}
|
|
776
834
|
// ── Thought bubble (screen-aware CTA beside the orb) ─────────────────────────
|
|
835
|
+
// LIVE locale: re-read the host app's active language on every resolution so
|
|
836
|
+
// the bubble text (analyzer `bubblePrompts` translations) follows the app's
|
|
837
|
+
// language switcher without remounting. Falls back to the mount-time locale.
|
|
838
|
+
function currentBubbleLocale() {
|
|
839
|
+
try {
|
|
840
|
+
const live = controller.config.getLocale?.();
|
|
841
|
+
if (typeof live === 'string' && live.trim())
|
|
842
|
+
return live.trim();
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
/* live locale read is best-effort */
|
|
846
|
+
}
|
|
847
|
+
return controller.config.locale;
|
|
848
|
+
}
|
|
777
849
|
function resolveCurrentBubblePrompt() {
|
|
778
|
-
return (0, core_1.resolveBubblePrompt)((0, core_1.matchRouteIntent)(currentRoute, controller.config.manifest.routes), controller.messages, { locale:
|
|
850
|
+
return (0, core_1.resolveBubblePrompt)((0, core_1.matchRouteIntent)(currentRoute, controller.config.manifest.routes), controller.messages, { locale: currentBubbleLocale(), actions: controller.config.manifest.actions });
|
|
779
851
|
}
|
|
780
852
|
// Render the CTA on ONE line at its NATURAL size. When the phrase is too long
|
|
781
853
|
// to fit the max width, scroll it (marquee, right-to-left, looping) instead of
|
|
@@ -990,6 +1062,7 @@ function mountOrb(controller, options = {}) {
|
|
|
990
1062
|
// denied/blocked, in which case we skip voice and go straight to the keyboard.
|
|
991
1063
|
if (controller.voiceAvailable && voiceInputEnabled) {
|
|
992
1064
|
micFallbackActive = false;
|
|
1065
|
+
userTriedVoice = true;
|
|
993
1066
|
void controller.toggleListening();
|
|
994
1067
|
return;
|
|
995
1068
|
}
|
|
@@ -1001,6 +1074,7 @@ function mountOrb(controller, options = {}) {
|
|
|
1001
1074
|
openTextPanel();
|
|
1002
1075
|
return;
|
|
1003
1076
|
}
|
|
1077
|
+
userTriedVoice = true;
|
|
1004
1078
|
void controller.toggleListening();
|
|
1005
1079
|
};
|
|
1006
1080
|
sendBtn.onclick = () => {
|
|
@@ -1039,11 +1113,15 @@ function mountOrb(controller, options = {}) {
|
|
|
1039
1113
|
// Track phase transitions to fire haptics: start/stop listening + response.
|
|
1040
1114
|
let lastPhase = null;
|
|
1041
1115
|
function render(state) {
|
|
1042
|
-
// Mic just became unusable
|
|
1043
|
-
// keyboard so the user always has a working channel.
|
|
1116
|
+
// Mic just became unusable MID-ATTEMPT (no Permissions API): fall back to
|
|
1117
|
+
// the keyboard so the user always has a working channel. Guarded by
|
|
1118
|
+
// userTriedVoice: the upfront permission detection also sets voiceBlocked
|
|
1119
|
+
// right at mount (site-level mic denial), and that must NEVER auto-open
|
|
1120
|
+
// the conversation panel on page load — the user did nothing yet; their
|
|
1121
|
+
// first orb tap will route to the keyboard via the voiceAvailable check.
|
|
1044
1122
|
if (state.voiceBlocked && !lastVoiceBlocked) {
|
|
1045
1123
|
lastVoiceBlocked = true;
|
|
1046
|
-
if (enableTextInput && !bubbleOpen && !state.isBusy) {
|
|
1124
|
+
if (userTriedVoice && enableTextInput && !bubbleOpen && !state.isBusy) {
|
|
1047
1125
|
openTextPanel();
|
|
1048
1126
|
return;
|
|
1049
1127
|
}
|
|
@@ -1112,9 +1190,18 @@ function mountOrb(controller, options = {}) {
|
|
|
1112
1190
|
}
|
|
1113
1191
|
const unsubscribe = controller.subscribe(render);
|
|
1114
1192
|
render(controller.getState());
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
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
|
+
}
|
|
1200
|
+
// Track the current route AND the app's live locale so the thought bubble
|
|
1201
|
+
// re-resolves its CTA on navigation and on language switches. No universal
|
|
1202
|
+
// navigation/i18n event exists, so poll + listen to popstate / visibility
|
|
1203
|
+
// for snappier updates.
|
|
1204
|
+
let lastBubbleLocale = currentBubbleLocale();
|
|
1118
1205
|
const readRoute = () => {
|
|
1119
1206
|
let next = null;
|
|
1120
1207
|
try {
|
|
@@ -1123,8 +1210,10 @@ function mountOrb(controller, options = {}) {
|
|
|
1123
1210
|
catch {
|
|
1124
1211
|
/* navigation read is best-effort */
|
|
1125
1212
|
}
|
|
1126
|
-
|
|
1213
|
+
const nextLocale = currentBubbleLocale();
|
|
1214
|
+
if (next !== currentRoute || nextLocale !== lastBubbleLocale) {
|
|
1127
1215
|
currentRoute = next;
|
|
1216
|
+
lastBubbleLocale = nextLocale;
|
|
1128
1217
|
syncThoughtBubble(controller.getState());
|
|
1129
1218
|
}
|
|
1130
1219
|
};
|
package/dist/cjs/orb/orbView.js
CHANGED
|
@@ -79,13 +79,14 @@ function buildDoubleBorder(opts) {
|
|
|
79
79
|
return { root, fill };
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
82
|
+
* Fiodos brand mark (idle, no logo). Breathes very gently on web — a
|
|
83
83
|
* direct port of the landing hero orbs' waveform easing (see
|
|
84
84
|
* `fiodosOrbMarkIdleHalfHeight` in @fiodos/core) — deliberately subtler than
|
|
85
85
|
* and distinct from the volume-reactive waveform shown while listening.
|
|
86
|
+
* Tinted with the same "audio color" (waveformColor) as that waveform.
|
|
86
87
|
*/
|
|
87
|
-
function buildOrbMark(orbSize) {
|
|
88
|
-
const { bars, color, radius } = (0, core_1.resolveFiodosOrbMarkAnimatedBars)(orbSize);
|
|
88
|
+
function buildOrbMark(orbSize, markColor) {
|
|
89
|
+
const { bars, color, radius } = (0, core_1.resolveFiodosOrbMarkAnimatedBars)(orbSize, markColor);
|
|
89
90
|
const svg = svgEl('svg', {
|
|
90
91
|
width: orbSize,
|
|
91
92
|
height: orbSize,
|
|
@@ -489,7 +490,7 @@ function createOrbVisual(initial) {
|
|
|
489
490
|
return;
|
|
490
491
|
}
|
|
491
492
|
if (state === 'idle') {
|
|
492
|
-
markAnim = buildOrbMark(size);
|
|
493
|
+
markAnim = buildOrbMark(size, appearance.waveformColor);
|
|
493
494
|
fillEl.appendChild(markAnim.el);
|
|
494
495
|
markAnim.start();
|
|
495
496
|
}
|
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.17";
|
|
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.17';
|
|
12
12
|
exports.SDK_PACKAGE = '@fiodos/web-core';
|