@fiodos/web-core 0.1.7 → 0.1.9
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/dropin/createFiodosAgent.d.ts +5 -1
- package/dist/cjs/dropin/createFiodosAgent.js +18 -1
- package/dist/cjs/orb/mountOrb.js +122 -1
- package/dist/cjs/orb/publishedConfig.d.ts +11 -1
- package/dist/cjs/orb/publishedConfig.js +6 -2
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/dropin/createFiodosAgent.d.ts +5 -1
- package/dist/esm/dropin/createFiodosAgent.js +18 -1
- package/dist/esm/orb/mountOrb.js +123 -2
- package/dist/esm/orb/publishedConfig.d.ts +11 -1
- package/dist/esm/orb/publishedConfig.js +7 -3
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
|
@@ -27,8 +27,12 @@ export interface CreateFiodosAgentOptions {
|
|
|
27
27
|
manifest?: AppManifest;
|
|
28
28
|
/** Action handlers / context validators (defaults to empty registries). */
|
|
29
29
|
registries?: ActionRegistries;
|
|
30
|
-
/** Agent
|
|
30
|
+
/** Agent UI locale (e.g. 'en', 'es'). Defaults to `'en'`. */
|
|
31
31
|
locale?: string;
|
|
32
|
+
/** Live locale resolver — overrides `locale` when set. */
|
|
33
|
+
getLocale?: () => string;
|
|
34
|
+
/** Auto-detect locale when `locale` / `getLocale` are unset. Default: none (English). */
|
|
35
|
+
localeDetection?: 'browser' | 'document';
|
|
32
36
|
/** STT locale (e.g. 'es-ES', 'en-US'). Defaults to the browser language. */
|
|
33
37
|
sttLocale?: string;
|
|
34
38
|
/** Host router navigate delegate. Defaults to location.assign. */
|
|
@@ -32,10 +32,27 @@ 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 resolveAgentLocale(options) {
|
|
36
|
+
const fromApp = options.getLocale?.()?.trim();
|
|
37
|
+
if (fromApp)
|
|
38
|
+
return fromApp;
|
|
39
|
+
if (options.locale?.trim())
|
|
40
|
+
return options.locale.trim();
|
|
41
|
+
if (options.localeDetection === 'browser')
|
|
42
|
+
return browserLocale().locale;
|
|
43
|
+
if (options.localeDetection === 'document') {
|
|
44
|
+
const docLang = typeof document !== 'undefined'
|
|
45
|
+
? document.documentElement.lang?.split(/[-_]/)[0]?.trim()
|
|
46
|
+
: '';
|
|
47
|
+
if (docLang)
|
|
48
|
+
return docLang;
|
|
49
|
+
}
|
|
50
|
+
return 'en';
|
|
51
|
+
}
|
|
35
52
|
/** Build the controller + adapters from a known manifest (shared by both paths). */
|
|
36
53
|
function assemble(options, manifest, baseUrl) {
|
|
37
54
|
const fallback = browserLocale();
|
|
38
|
-
const locale = options
|
|
55
|
+
const locale = resolveAgentLocale(options);
|
|
39
56
|
const sttLocale = options.sttLocale ?? options.locale ?? fallback.sttLocale;
|
|
40
57
|
const backend = (0, backendClient_1.createFiodosBackendClient)({
|
|
41
58
|
baseUrl,
|
package/dist/cjs/orb/mountOrb.js
CHANGED
|
@@ -42,6 +42,8 @@ const CSS = `
|
|
|
42
42
|
.fyodos-bubble-row{display:flex;gap:8px;align-items:flex-end;padding:8px 10px;border-top:1px solid rgba(160,180,220,0.18);}
|
|
43
43
|
.fyodos-bubble-row input{flex:1;border:1px solid rgba(15,23,42,0.12);outline:none;border-radius:10px;padding:9px 11px;font-size:15px;}
|
|
44
44
|
.fyodos-bubble-row button{appearance:none;border:none;flex-shrink:0;display:flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:32px;padding:0;font-size:17px;font-weight:700;line-height:1;color:#000;cursor:pointer;}
|
|
45
|
+
.fyodos-thought{position:absolute;appearance:none;display:inline-block;width:max-content;max-width:min(280px,70vw);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:left;line-height:1.25;font-weight:500;cursor:pointer;box-shadow:0 8px 24px rgba(0,0,0,0.22);animation:fyodos-thought-in .18s ease-out;}
|
|
46
|
+
@keyframes fyodos-thought-in{from{opacity:0;transform:translateY(4px) scale(0.96)}to{opacity:1;transform:none}}
|
|
45
47
|
@keyframes fyodos-fade-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
46
48
|
@keyframes fyodos-blink{0%,100%{opacity:.25}50%{opacity:.9}}
|
|
47
49
|
@keyframes fy-spin{to{transform:rotate(360deg)}}
|
|
@@ -130,6 +132,12 @@ function mountOrb(controller, options = {}) {
|
|
|
130
132
|
let publishedAppearance = null;
|
|
131
133
|
let chipTheme = null;
|
|
132
134
|
let modalTheme;
|
|
135
|
+
let bubbleTheme = (0, core_1.resolveBubbleTheme)(null);
|
|
136
|
+
let bubbleEnabled = true;
|
|
137
|
+
// Dashboard VOICE switch. Default true (voice on). When false the orb never
|
|
138
|
+
// listens/speaks: a tap opens the keyboard directly and text turns are silent.
|
|
139
|
+
let voiceInputEnabled = true;
|
|
140
|
+
let currentRoute = null;
|
|
133
141
|
let screenPosition = options.corner
|
|
134
142
|
? cornerToPosition(options.corner)
|
|
135
143
|
: { ...core_1.DEFAULT_ORB_SCREEN_POSITION };
|
|
@@ -160,6 +168,22 @@ function mountOrb(controller, options = {}) {
|
|
|
160
168
|
anchor.appendChild(btn);
|
|
161
169
|
root.appendChild(anchor);
|
|
162
170
|
let chipEl = null;
|
|
171
|
+
// Thought bubble: a short, screen-aware CTA beside the orb. Created once and
|
|
172
|
+
// shown/hidden by syncThoughtBubble; tapping it opens the keyboard.
|
|
173
|
+
const thoughtBubbleEl = document.createElement('button');
|
|
174
|
+
thoughtBubbleEl.type = 'button';
|
|
175
|
+
thoughtBubbleEl.className = 'fyodos-thought';
|
|
176
|
+
thoughtBubbleEl.style.display = 'none';
|
|
177
|
+
thoughtBubbleEl.addEventListener('click', () => {
|
|
178
|
+
haptics.trigger('orbTap');
|
|
179
|
+
controller.primeAudio();
|
|
180
|
+
if (enableTextInput) {
|
|
181
|
+
openTextPanel();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
void controller.toggleListening();
|
|
185
|
+
});
|
|
186
|
+
anchor.appendChild(thoughtBubbleEl);
|
|
163
187
|
container.appendChild(root);
|
|
164
188
|
// ── Position (dashboard position; user drag is SESSION-only, never persisted) ─
|
|
165
189
|
// The orb ALWAYS starts at the dashboard-published position on every load.
|
|
@@ -209,6 +233,9 @@ function mountOrb(controller, options = {}) {
|
|
|
209
233
|
chipEl.style.right = '';
|
|
210
234
|
}
|
|
211
235
|
}
|
|
236
|
+
// Keep the thought bubble on the orb's free side too while it is visible.
|
|
237
|
+
if (thoughtBubbleEl.style.display !== 'none')
|
|
238
|
+
positionThoughtBubble();
|
|
212
239
|
}
|
|
213
240
|
window.addEventListener('resize', reposition);
|
|
214
241
|
// Recalc when a scrollbar appears/disappears as the page content changes (the
|
|
@@ -459,6 +486,65 @@ function mountOrb(controller, options = {}) {
|
|
|
459
486
|
anchor.appendChild(chipEl);
|
|
460
487
|
applyDeployment();
|
|
461
488
|
}
|
|
489
|
+
// ── Thought bubble (screen-aware CTA beside the orb) ─────────────────────────
|
|
490
|
+
function resolveCurrentBubblePrompt() {
|
|
491
|
+
return (0, core_1.resolveBubblePrompt)((0, core_1.matchRouteIntent)(currentRoute, controller.config.manifest.routes), controller.messages, { locale: controller.config.locale, actions: controller.config.manifest.actions });
|
|
492
|
+
}
|
|
493
|
+
function applyThoughtBubbleTheme() {
|
|
494
|
+
const t = bubbleTheme;
|
|
495
|
+
const { fontPx, padV, padH, approxHeight } = (0, core_1.resolveBubblePadding)(t);
|
|
496
|
+
const shapeStyle = (0, core_1.resolveBubbleShapeStyle)(t, approxHeight);
|
|
497
|
+
thoughtBubbleEl.style.fontSize = `${fontPx}px`;
|
|
498
|
+
thoughtBubbleEl.style.padding = `${padV}px ${padH}px`;
|
|
499
|
+
thoughtBubbleEl.style.color = t.textColor;
|
|
500
|
+
thoughtBubbleEl.style.background = t.backgroundColor;
|
|
501
|
+
thoughtBubbleEl.style.border = `${t.borderWidth}px solid ${t.borderColor}`;
|
|
502
|
+
thoughtBubbleEl.style.borderRadius =
|
|
503
|
+
shapeStyle.borderRadius != null ? `${shapeStyle.borderRadius}px` : '';
|
|
504
|
+
thoughtBubbleEl.style.borderTopLeftRadius =
|
|
505
|
+
shapeStyle.borderTopLeftRadius != null ? `${shapeStyle.borderTopLeftRadius}px` : '';
|
|
506
|
+
thoughtBubbleEl.style.borderTopRightRadius =
|
|
507
|
+
shapeStyle.borderTopRightRadius != null ? `${shapeStyle.borderTopRightRadius}px` : '';
|
|
508
|
+
thoughtBubbleEl.style.borderBottomLeftRadius =
|
|
509
|
+
shapeStyle.borderBottomLeftRadius != null ? `${shapeStyle.borderBottomLeftRadius}px` : '';
|
|
510
|
+
thoughtBubbleEl.style.borderBottomRightRadius =
|
|
511
|
+
shapeStyle.borderBottomRightRadius != null ? `${shapeStyle.borderBottomRightRadius}px` : '';
|
|
512
|
+
thoughtBubbleEl.style.clipPath = shapeStyle.clipPath ?? '';
|
|
513
|
+
}
|
|
514
|
+
function positionThoughtBubble() {
|
|
515
|
+
const dep = (0, core_1.resolveOrbDeployment)(effectivePosition());
|
|
516
|
+
if (dep.horizontal === 'left') {
|
|
517
|
+
thoughtBubbleEl.style.right = 'calc(100% + 10px)';
|
|
518
|
+
thoughtBubbleEl.style.left = '';
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
thoughtBubbleEl.style.left = 'calc(100% + 10px)';
|
|
522
|
+
thoughtBubbleEl.style.right = '';
|
|
523
|
+
}
|
|
524
|
+
if (dep.vertical === 'up') {
|
|
525
|
+
thoughtBubbleEl.style.top = '0';
|
|
526
|
+
thoughtBubbleEl.style.bottom = '';
|
|
527
|
+
thoughtBubbleEl.style.transform = '';
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
thoughtBubbleEl.style.bottom = '0';
|
|
531
|
+
thoughtBubbleEl.style.top = '';
|
|
532
|
+
thoughtBubbleEl.style.transform = '';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function syncThoughtBubble(state) {
|
|
536
|
+
const idle = !state.showThinking && !state.showWaveform && state.phase !== 'speaking' && !micFallbackActive;
|
|
537
|
+
const prompt = bubbleEnabled && !bubbleOpen && idle ? resolveCurrentBubblePrompt() : null;
|
|
538
|
+
if (!prompt) {
|
|
539
|
+
thoughtBubbleEl.style.display = 'none';
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
thoughtBubbleEl.textContent = prompt;
|
|
543
|
+
thoughtBubbleEl.setAttribute('aria-label', ui.orbLabel);
|
|
544
|
+
applyThoughtBubbleTheme();
|
|
545
|
+
positionThoughtBubble();
|
|
546
|
+
thoughtBubbleEl.style.display = 'block';
|
|
547
|
+
}
|
|
462
548
|
// ── Overlays (confirmation + consent) ────────────────────────────────────────
|
|
463
549
|
let overlayEl = null;
|
|
464
550
|
function clearOverlay() {
|
|
@@ -571,9 +657,16 @@ function mountOrb(controller, options = {}) {
|
|
|
571
657
|
setBubble(false);
|
|
572
658
|
return;
|
|
573
659
|
}
|
|
660
|
+
// Voice disabled in the dashboard: never listen/speak. Tap opens the keyboard
|
|
661
|
+
// directly (text turns are silent by default); animations are unchanged.
|
|
662
|
+
if (!voiceInputEnabled && enableTextInput) {
|
|
663
|
+
micFallbackActive = false;
|
|
664
|
+
openTextPanel();
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
574
667
|
// `voiceAvailable` is dynamic: the Web Speech API may be present yet the mic
|
|
575
668
|
// denied/blocked, in which case we skip voice and go straight to the keyboard.
|
|
576
|
-
if (controller.voiceAvailable) {
|
|
669
|
+
if (controller.voiceAvailable && voiceInputEnabled) {
|
|
577
670
|
micFallbackActive = false;
|
|
578
671
|
void controller.toggleListening();
|
|
579
672
|
return;
|
|
@@ -663,6 +756,7 @@ function mountOrb(controller, options = {}) {
|
|
|
663
756
|
sendBtn.disabled = false;
|
|
664
757
|
}
|
|
665
758
|
syncChip(state);
|
|
759
|
+
syncThoughtBubble(state);
|
|
666
760
|
if (state.confirmationMessage !== lastConfirm) {
|
|
667
761
|
lastConfirm = state.confirmationMessage;
|
|
668
762
|
if (state.confirmationMessage)
|
|
@@ -680,6 +774,26 @@ function mountOrb(controller, options = {}) {
|
|
|
680
774
|
}
|
|
681
775
|
const unsubscribe = controller.subscribe(render);
|
|
682
776
|
render(controller.getState());
|
|
777
|
+
// Track the current route so the thought bubble re-resolves its CTA on
|
|
778
|
+
// navigation. No universal navigation event exists, so poll + listen to
|
|
779
|
+
// popstate / visibility for snappier updates.
|
|
780
|
+
const readRoute = () => {
|
|
781
|
+
let next = null;
|
|
782
|
+
try {
|
|
783
|
+
next = controller.config.navigation.getCurrentRoute();
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
/* navigation read is best-effort */
|
|
787
|
+
}
|
|
788
|
+
if (next !== currentRoute) {
|
|
789
|
+
currentRoute = next;
|
|
790
|
+
syncThoughtBubble(controller.getState());
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
readRoute();
|
|
794
|
+
const routeTimer = window.setInterval(readRoute, 700);
|
|
795
|
+
window.addEventListener('popstate', readRoute);
|
|
796
|
+
document.addEventListener('visibilitychange', readRoute);
|
|
683
797
|
// ── Live dashboard config ────────────────────────────────────────────────────
|
|
684
798
|
const stopWatch = (0, publishedConfig_1.watchPublishedConfig)(controller, {
|
|
685
799
|
pollMs: options.appearancePollMs,
|
|
@@ -692,6 +806,9 @@ function mountOrb(controller, options = {}) {
|
|
|
692
806
|
publishedAppearance = config.appearance;
|
|
693
807
|
chipTheme = config.keyboardChip;
|
|
694
808
|
modalTheme = config.modalTheme;
|
|
809
|
+
bubbleTheme = config.bubbleTheme;
|
|
810
|
+
bubbleEnabled = config.bubbleEnabled;
|
|
811
|
+
voiceInputEnabled = config.voiceInputEnabled;
|
|
695
812
|
if (usePublishedPosition && config.screenPosition)
|
|
696
813
|
screenPosition = config.screenPosition;
|
|
697
814
|
}
|
|
@@ -702,6 +819,7 @@ function mountOrb(controller, options = {}) {
|
|
|
702
819
|
chipEl = null;
|
|
703
820
|
}
|
|
704
821
|
syncChip(controller.getState());
|
|
822
|
+
syncThoughtBubble(controller.getState());
|
|
705
823
|
if (bubbleOpen)
|
|
706
824
|
applyBubbleTheme();
|
|
707
825
|
},
|
|
@@ -710,6 +828,9 @@ function mountOrb(controller, options = {}) {
|
|
|
710
828
|
unmount() {
|
|
711
829
|
unsubscribe();
|
|
712
830
|
stopWatch();
|
|
831
|
+
window.clearInterval(routeTimer);
|
|
832
|
+
window.removeEventListener('popstate', readRoute);
|
|
833
|
+
document.removeEventListener('visibilitychange', readRoute);
|
|
713
834
|
window.removeEventListener('resize', reposition);
|
|
714
835
|
contentObserver?.disconnect();
|
|
715
836
|
visualViewport?.removeEventListener('resize', onViewportChange);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* mobile — in real time, via polling. So Vue/Svelte/Angular read the dashboard
|
|
10
10
|
* config identically to the reference, instead of ignoring it.
|
|
11
11
|
*/
|
|
12
|
-
import { type OrbScreenPosition, type PublishedModalTheme } from '@fiodos/core';
|
|
12
|
+
import { type OrbScreenPosition, type PublishedModalTheme, type ResolvedBubbleTheme } from '@fiodos/core';
|
|
13
13
|
import type { AgentController } from '../controller/AgentController';
|
|
14
14
|
import { type ChipTheme, type OrbAppearance } from './orbView';
|
|
15
15
|
export interface ResolvedOrbConfig {
|
|
@@ -17,6 +17,16 @@ export interface ResolvedOrbConfig {
|
|
|
17
17
|
keyboardChip: ChipTheme | null;
|
|
18
18
|
modalTheme: PublishedModalTheme | undefined;
|
|
19
19
|
screenPosition: OrbScreenPosition | undefined;
|
|
20
|
+
/** Resolved orb thought-bubble theme (defaults filled). */
|
|
21
|
+
bubbleTheme: ResolvedBubbleTheme;
|
|
22
|
+
/** Dashboard on/off for the thought bubble. Default true (absent = enabled). */
|
|
23
|
+
bubbleEnabled: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Dashboard on/off for the orb's VOICE channel (`voiceInputEnabled`). False
|
|
26
|
+
* ONLY when the developer turned voice off; then the orb opens the keyboard
|
|
27
|
+
* directly on tap and never plays TTS. Default true (absent = voice on).
|
|
28
|
+
*/
|
|
29
|
+
voiceInputEnabled: boolean;
|
|
20
30
|
/**
|
|
21
31
|
* Dashboard master visibility switch (`orbEnabled`). False ONLY when the
|
|
22
32
|
* developer explicitly turned the orb off. The orb is hidden when false and
|
|
@@ -14,7 +14,8 @@ exports.watchPublishedConfig = watchPublishedConfig;
|
|
|
14
14
|
*/
|
|
15
15
|
const core_1 = require("@fiodos/core");
|
|
16
16
|
const orbView_1 = require("./orbView");
|
|
17
|
-
function toResolved(props,
|
|
17
|
+
function toResolved(props, published) {
|
|
18
|
+
const enabled = published.orbEnabled !== false;
|
|
18
19
|
const theme = props.theme;
|
|
19
20
|
const appearance = {
|
|
20
21
|
accentColor: theme.accentColor ?? orbView_1.DEFAULT_ORB_APPEARANCE.accentColor,
|
|
@@ -33,6 +34,9 @@ function toResolved(props, enabled) {
|
|
|
33
34
|
keyboardChip: theme.keyboardChip ?? null,
|
|
34
35
|
modalTheme: props.modalTheme,
|
|
35
36
|
screenPosition: props.screenPosition,
|
|
37
|
+
bubbleTheme: (0, core_1.resolveBubbleTheme)(published.bubbleTheme),
|
|
38
|
+
bubbleEnabled: published.bubbleEnabled !== false,
|
|
39
|
+
voiceInputEnabled: published.voiceInputEnabled !== false,
|
|
36
40
|
enabled,
|
|
37
41
|
};
|
|
38
42
|
}
|
|
@@ -54,7 +58,7 @@ function watchPublishedConfig(controller, options) {
|
|
|
54
58
|
if (cancelled)
|
|
55
59
|
return;
|
|
56
60
|
options.onChange(res.published
|
|
57
|
-
? toResolved((0, core_1.mapPublishedConfigToOrbProps)(res.published), res.published
|
|
61
|
+
? toResolved((0, core_1.mapPublishedConfigToOrbProps)(res.published), res.published)
|
|
58
62
|
: null);
|
|
59
63
|
}
|
|
60
64
|
catch {
|
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.9";
|
|
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.9';
|
|
12
12
|
exports.SDK_PACKAGE = '@fiodos/web-core';
|
|
@@ -27,8 +27,12 @@ export interface CreateFiodosAgentOptions {
|
|
|
27
27
|
manifest?: AppManifest;
|
|
28
28
|
/** Action handlers / context validators (defaults to empty registries). */
|
|
29
29
|
registries?: ActionRegistries;
|
|
30
|
-
/** Agent
|
|
30
|
+
/** Agent UI locale (e.g. 'en', 'es'). Defaults to `'en'`. */
|
|
31
31
|
locale?: string;
|
|
32
|
+
/** Live locale resolver — overrides `locale` when set. */
|
|
33
|
+
getLocale?: () => string;
|
|
34
|
+
/** Auto-detect locale when `locale` / `getLocale` are unset. Default: none (English). */
|
|
35
|
+
localeDetection?: 'browser' | 'document';
|
|
32
36
|
/** STT locale (e.g. 'es-ES', 'en-US'). Defaults to the browser language. */
|
|
33
37
|
sttLocale?: string;
|
|
34
38
|
/** Host router navigate delegate. Defaults to location.assign. */
|
|
@@ -29,10 +29,27 @@ 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 resolveAgentLocale(options) {
|
|
33
|
+
const fromApp = options.getLocale?.()?.trim();
|
|
34
|
+
if (fromApp)
|
|
35
|
+
return fromApp;
|
|
36
|
+
if (options.locale?.trim())
|
|
37
|
+
return options.locale.trim();
|
|
38
|
+
if (options.localeDetection === 'browser')
|
|
39
|
+
return browserLocale().locale;
|
|
40
|
+
if (options.localeDetection === 'document') {
|
|
41
|
+
const docLang = typeof document !== 'undefined'
|
|
42
|
+
? document.documentElement.lang?.split(/[-_]/)[0]?.trim()
|
|
43
|
+
: '';
|
|
44
|
+
if (docLang)
|
|
45
|
+
return docLang;
|
|
46
|
+
}
|
|
47
|
+
return 'en';
|
|
48
|
+
}
|
|
32
49
|
/** Build the controller + adapters from a known manifest (shared by both paths). */
|
|
33
50
|
function assemble(options, manifest, baseUrl) {
|
|
34
51
|
const fallback = browserLocale();
|
|
35
|
-
const locale = options
|
|
52
|
+
const locale = resolveAgentLocale(options);
|
|
36
53
|
const sttLocale = options.sttLocale ?? options.locale ?? fallback.sttLocale;
|
|
37
54
|
const backend = createFiodosBackendClient({
|
|
38
55
|
baseUrl,
|
package/dist/esm/orb/mountOrb.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - CONFIRMATIONS: the same security model (manifest decides; strict phrase /
|
|
17
17
|
* deliberate tap for payment-grade; loose "yes" never resolves strict).
|
|
18
18
|
*/
|
|
19
|
-
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, resolveOrbAnchorPx, resolveOrbDeployment, snapOrbScreenPositionToSide, viewportPixelsToOrbScreenPosition, } from '@fiodos/core';
|
|
19
|
+
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, matchRouteIntent, resolveBubblePrompt, resolveBubbleTheme, resolveBubblePadding, resolveBubbleShapeStyle, resolveOrbAnchorPx, resolveOrbDeployment, snapOrbScreenPositionToSide, viewportPixelsToOrbScreenPosition, } from '@fiodos/core';
|
|
20
20
|
import { buildKeyboardChip, createOrbVisual, DEFAULT_ORB_APPEARANCE, } from './orbView.js';
|
|
21
21
|
import { watchPublishedConfig } from './publishedConfig.js';
|
|
22
22
|
const STYLE_ID = 'fyodos-orb-styles';
|
|
@@ -39,6 +39,8 @@ const CSS = `
|
|
|
39
39
|
.fyodos-bubble-row{display:flex;gap:8px;align-items:flex-end;padding:8px 10px;border-top:1px solid rgba(160,180,220,0.18);}
|
|
40
40
|
.fyodos-bubble-row input{flex:1;border:1px solid rgba(15,23,42,0.12);outline:none;border-radius:10px;padding:9px 11px;font-size:15px;}
|
|
41
41
|
.fyodos-bubble-row button{appearance:none;border:none;flex-shrink:0;display:flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:32px;padding:0;font-size:17px;font-weight:700;line-height:1;color:#000;cursor:pointer;}
|
|
42
|
+
.fyodos-thought{position:absolute;appearance:none;display:inline-block;width:max-content;max-width:min(280px,70vw);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:left;line-height:1.25;font-weight:500;cursor:pointer;box-shadow:0 8px 24px rgba(0,0,0,0.22);animation:fyodos-thought-in .18s ease-out;}
|
|
43
|
+
@keyframes fyodos-thought-in{from{opacity:0;transform:translateY(4px) scale(0.96)}to{opacity:1;transform:none}}
|
|
42
44
|
@keyframes fyodos-fade-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
43
45
|
@keyframes fyodos-blink{0%,100%{opacity:.25}50%{opacity:.9}}
|
|
44
46
|
@keyframes fy-spin{to{transform:rotate(360deg)}}
|
|
@@ -127,6 +129,12 @@ export function mountOrb(controller, options = {}) {
|
|
|
127
129
|
let publishedAppearance = null;
|
|
128
130
|
let chipTheme = null;
|
|
129
131
|
let modalTheme;
|
|
132
|
+
let bubbleTheme = resolveBubbleTheme(null);
|
|
133
|
+
let bubbleEnabled = true;
|
|
134
|
+
// Dashboard VOICE switch. Default true (voice on). When false the orb never
|
|
135
|
+
// listens/speaks: a tap opens the keyboard directly and text turns are silent.
|
|
136
|
+
let voiceInputEnabled = true;
|
|
137
|
+
let currentRoute = null;
|
|
130
138
|
let screenPosition = options.corner
|
|
131
139
|
? cornerToPosition(options.corner)
|
|
132
140
|
: { ...DEFAULT_ORB_SCREEN_POSITION };
|
|
@@ -157,6 +165,22 @@ export function mountOrb(controller, options = {}) {
|
|
|
157
165
|
anchor.appendChild(btn);
|
|
158
166
|
root.appendChild(anchor);
|
|
159
167
|
let chipEl = null;
|
|
168
|
+
// Thought bubble: a short, screen-aware CTA beside the orb. Created once and
|
|
169
|
+
// shown/hidden by syncThoughtBubble; tapping it opens the keyboard.
|
|
170
|
+
const thoughtBubbleEl = document.createElement('button');
|
|
171
|
+
thoughtBubbleEl.type = 'button';
|
|
172
|
+
thoughtBubbleEl.className = 'fyodos-thought';
|
|
173
|
+
thoughtBubbleEl.style.display = 'none';
|
|
174
|
+
thoughtBubbleEl.addEventListener('click', () => {
|
|
175
|
+
haptics.trigger('orbTap');
|
|
176
|
+
controller.primeAudio();
|
|
177
|
+
if (enableTextInput) {
|
|
178
|
+
openTextPanel();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
void controller.toggleListening();
|
|
182
|
+
});
|
|
183
|
+
anchor.appendChild(thoughtBubbleEl);
|
|
160
184
|
container.appendChild(root);
|
|
161
185
|
// ── Position (dashboard position; user drag is SESSION-only, never persisted) ─
|
|
162
186
|
// The orb ALWAYS starts at the dashboard-published position on every load.
|
|
@@ -206,6 +230,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
206
230
|
chipEl.style.right = '';
|
|
207
231
|
}
|
|
208
232
|
}
|
|
233
|
+
// Keep the thought bubble on the orb's free side too while it is visible.
|
|
234
|
+
if (thoughtBubbleEl.style.display !== 'none')
|
|
235
|
+
positionThoughtBubble();
|
|
209
236
|
}
|
|
210
237
|
window.addEventListener('resize', reposition);
|
|
211
238
|
// Recalc when a scrollbar appears/disappears as the page content changes (the
|
|
@@ -456,6 +483,65 @@ export function mountOrb(controller, options = {}) {
|
|
|
456
483
|
anchor.appendChild(chipEl);
|
|
457
484
|
applyDeployment();
|
|
458
485
|
}
|
|
486
|
+
// ── Thought bubble (screen-aware CTA beside the orb) ─────────────────────────
|
|
487
|
+
function resolveCurrentBubblePrompt() {
|
|
488
|
+
return resolveBubblePrompt(matchRouteIntent(currentRoute, controller.config.manifest.routes), controller.messages, { locale: controller.config.locale, actions: controller.config.manifest.actions });
|
|
489
|
+
}
|
|
490
|
+
function applyThoughtBubbleTheme() {
|
|
491
|
+
const t = bubbleTheme;
|
|
492
|
+
const { fontPx, padV, padH, approxHeight } = resolveBubblePadding(t);
|
|
493
|
+
const shapeStyle = resolveBubbleShapeStyle(t, approxHeight);
|
|
494
|
+
thoughtBubbleEl.style.fontSize = `${fontPx}px`;
|
|
495
|
+
thoughtBubbleEl.style.padding = `${padV}px ${padH}px`;
|
|
496
|
+
thoughtBubbleEl.style.color = t.textColor;
|
|
497
|
+
thoughtBubbleEl.style.background = t.backgroundColor;
|
|
498
|
+
thoughtBubbleEl.style.border = `${t.borderWidth}px solid ${t.borderColor}`;
|
|
499
|
+
thoughtBubbleEl.style.borderRadius =
|
|
500
|
+
shapeStyle.borderRadius != null ? `${shapeStyle.borderRadius}px` : '';
|
|
501
|
+
thoughtBubbleEl.style.borderTopLeftRadius =
|
|
502
|
+
shapeStyle.borderTopLeftRadius != null ? `${shapeStyle.borderTopLeftRadius}px` : '';
|
|
503
|
+
thoughtBubbleEl.style.borderTopRightRadius =
|
|
504
|
+
shapeStyle.borderTopRightRadius != null ? `${shapeStyle.borderTopRightRadius}px` : '';
|
|
505
|
+
thoughtBubbleEl.style.borderBottomLeftRadius =
|
|
506
|
+
shapeStyle.borderBottomLeftRadius != null ? `${shapeStyle.borderBottomLeftRadius}px` : '';
|
|
507
|
+
thoughtBubbleEl.style.borderBottomRightRadius =
|
|
508
|
+
shapeStyle.borderBottomRightRadius != null ? `${shapeStyle.borderBottomRightRadius}px` : '';
|
|
509
|
+
thoughtBubbleEl.style.clipPath = shapeStyle.clipPath ?? '';
|
|
510
|
+
}
|
|
511
|
+
function positionThoughtBubble() {
|
|
512
|
+
const dep = resolveOrbDeployment(effectivePosition());
|
|
513
|
+
if (dep.horizontal === 'left') {
|
|
514
|
+
thoughtBubbleEl.style.right = 'calc(100% + 10px)';
|
|
515
|
+
thoughtBubbleEl.style.left = '';
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
thoughtBubbleEl.style.left = 'calc(100% + 10px)';
|
|
519
|
+
thoughtBubbleEl.style.right = '';
|
|
520
|
+
}
|
|
521
|
+
if (dep.vertical === 'up') {
|
|
522
|
+
thoughtBubbleEl.style.top = '0';
|
|
523
|
+
thoughtBubbleEl.style.bottom = '';
|
|
524
|
+
thoughtBubbleEl.style.transform = '';
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
thoughtBubbleEl.style.bottom = '0';
|
|
528
|
+
thoughtBubbleEl.style.top = '';
|
|
529
|
+
thoughtBubbleEl.style.transform = '';
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function syncThoughtBubble(state) {
|
|
533
|
+
const idle = !state.showThinking && !state.showWaveform && state.phase !== 'speaking' && !micFallbackActive;
|
|
534
|
+
const prompt = bubbleEnabled && !bubbleOpen && idle ? resolveCurrentBubblePrompt() : null;
|
|
535
|
+
if (!prompt) {
|
|
536
|
+
thoughtBubbleEl.style.display = 'none';
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
thoughtBubbleEl.textContent = prompt;
|
|
540
|
+
thoughtBubbleEl.setAttribute('aria-label', ui.orbLabel);
|
|
541
|
+
applyThoughtBubbleTheme();
|
|
542
|
+
positionThoughtBubble();
|
|
543
|
+
thoughtBubbleEl.style.display = 'block';
|
|
544
|
+
}
|
|
459
545
|
// ── Overlays (confirmation + consent) ────────────────────────────────────────
|
|
460
546
|
let overlayEl = null;
|
|
461
547
|
function clearOverlay() {
|
|
@@ -568,9 +654,16 @@ export function mountOrb(controller, options = {}) {
|
|
|
568
654
|
setBubble(false);
|
|
569
655
|
return;
|
|
570
656
|
}
|
|
657
|
+
// Voice disabled in the dashboard: never listen/speak. Tap opens the keyboard
|
|
658
|
+
// directly (text turns are silent by default); animations are unchanged.
|
|
659
|
+
if (!voiceInputEnabled && enableTextInput) {
|
|
660
|
+
micFallbackActive = false;
|
|
661
|
+
openTextPanel();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
571
664
|
// `voiceAvailable` is dynamic: the Web Speech API may be present yet the mic
|
|
572
665
|
// denied/blocked, in which case we skip voice and go straight to the keyboard.
|
|
573
|
-
if (controller.voiceAvailable) {
|
|
666
|
+
if (controller.voiceAvailable && voiceInputEnabled) {
|
|
574
667
|
micFallbackActive = false;
|
|
575
668
|
void controller.toggleListening();
|
|
576
669
|
return;
|
|
@@ -660,6 +753,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
660
753
|
sendBtn.disabled = false;
|
|
661
754
|
}
|
|
662
755
|
syncChip(state);
|
|
756
|
+
syncThoughtBubble(state);
|
|
663
757
|
if (state.confirmationMessage !== lastConfirm) {
|
|
664
758
|
lastConfirm = state.confirmationMessage;
|
|
665
759
|
if (state.confirmationMessage)
|
|
@@ -677,6 +771,26 @@ export function mountOrb(controller, options = {}) {
|
|
|
677
771
|
}
|
|
678
772
|
const unsubscribe = controller.subscribe(render);
|
|
679
773
|
render(controller.getState());
|
|
774
|
+
// Track the current route so the thought bubble re-resolves its CTA on
|
|
775
|
+
// navigation. No universal navigation event exists, so poll + listen to
|
|
776
|
+
// popstate / visibility for snappier updates.
|
|
777
|
+
const readRoute = () => {
|
|
778
|
+
let next = null;
|
|
779
|
+
try {
|
|
780
|
+
next = controller.config.navigation.getCurrentRoute();
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
/* navigation read is best-effort */
|
|
784
|
+
}
|
|
785
|
+
if (next !== currentRoute) {
|
|
786
|
+
currentRoute = next;
|
|
787
|
+
syncThoughtBubble(controller.getState());
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
readRoute();
|
|
791
|
+
const routeTimer = window.setInterval(readRoute, 700);
|
|
792
|
+
window.addEventListener('popstate', readRoute);
|
|
793
|
+
document.addEventListener('visibilitychange', readRoute);
|
|
680
794
|
// ── Live dashboard config ────────────────────────────────────────────────────
|
|
681
795
|
const stopWatch = watchPublishedConfig(controller, {
|
|
682
796
|
pollMs: options.appearancePollMs,
|
|
@@ -689,6 +803,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
689
803
|
publishedAppearance = config.appearance;
|
|
690
804
|
chipTheme = config.keyboardChip;
|
|
691
805
|
modalTheme = config.modalTheme;
|
|
806
|
+
bubbleTheme = config.bubbleTheme;
|
|
807
|
+
bubbleEnabled = config.bubbleEnabled;
|
|
808
|
+
voiceInputEnabled = config.voiceInputEnabled;
|
|
692
809
|
if (usePublishedPosition && config.screenPosition)
|
|
693
810
|
screenPosition = config.screenPosition;
|
|
694
811
|
}
|
|
@@ -699,6 +816,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
699
816
|
chipEl = null;
|
|
700
817
|
}
|
|
701
818
|
syncChip(controller.getState());
|
|
819
|
+
syncThoughtBubble(controller.getState());
|
|
702
820
|
if (bubbleOpen)
|
|
703
821
|
applyBubbleTheme();
|
|
704
822
|
},
|
|
@@ -707,6 +825,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
707
825
|
unmount() {
|
|
708
826
|
unsubscribe();
|
|
709
827
|
stopWatch();
|
|
828
|
+
window.clearInterval(routeTimer);
|
|
829
|
+
window.removeEventListener('popstate', readRoute);
|
|
830
|
+
document.removeEventListener('visibilitychange', readRoute);
|
|
710
831
|
window.removeEventListener('resize', reposition);
|
|
711
832
|
contentObserver?.disconnect();
|
|
712
833
|
visualViewport?.removeEventListener('resize', onViewportChange);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* mobile — in real time, via polling. So Vue/Svelte/Angular read the dashboard
|
|
10
10
|
* config identically to the reference, instead of ignoring it.
|
|
11
11
|
*/
|
|
12
|
-
import { type OrbScreenPosition, type PublishedModalTheme } from '@fiodos/core';
|
|
12
|
+
import { type OrbScreenPosition, type PublishedModalTheme, type ResolvedBubbleTheme } from '@fiodos/core';
|
|
13
13
|
import type { AgentController } from '../controller/AgentController.js';
|
|
14
14
|
import { type ChipTheme, type OrbAppearance } from './orbView.js';
|
|
15
15
|
export interface ResolvedOrbConfig {
|
|
@@ -17,6 +17,16 @@ export interface ResolvedOrbConfig {
|
|
|
17
17
|
keyboardChip: ChipTheme | null;
|
|
18
18
|
modalTheme: PublishedModalTheme | undefined;
|
|
19
19
|
screenPosition: OrbScreenPosition | undefined;
|
|
20
|
+
/** Resolved orb thought-bubble theme (defaults filled). */
|
|
21
|
+
bubbleTheme: ResolvedBubbleTheme;
|
|
22
|
+
/** Dashboard on/off for the thought bubble. Default true (absent = enabled). */
|
|
23
|
+
bubbleEnabled: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Dashboard on/off for the orb's VOICE channel (`voiceInputEnabled`). False
|
|
26
|
+
* ONLY when the developer turned voice off; then the orb opens the keyboard
|
|
27
|
+
* directly on tap and never plays TTS. Default true (absent = voice on).
|
|
28
|
+
*/
|
|
29
|
+
voiceInputEnabled: boolean;
|
|
20
30
|
/**
|
|
21
31
|
* Dashboard master visibility switch (`orbEnabled`). False ONLY when the
|
|
22
32
|
* developer explicitly turned the orb off. The orb is hidden when false and
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* mobile — in real time, via polling. So Vue/Svelte/Angular read the dashboard
|
|
10
10
|
* config identically to the reference, instead of ignoring it.
|
|
11
11
|
*/
|
|
12
|
-
import { mapPublishedConfigToOrbProps, } from '@fiodos/core';
|
|
12
|
+
import { mapPublishedConfigToOrbProps, resolveBubbleTheme, } from '@fiodos/core';
|
|
13
13
|
import { DEFAULT_ORB_APPEARANCE } from './orbView.js';
|
|
14
|
-
function toResolved(props,
|
|
14
|
+
function toResolved(props, published) {
|
|
15
|
+
const enabled = published.orbEnabled !== false;
|
|
15
16
|
const theme = props.theme;
|
|
16
17
|
const appearance = {
|
|
17
18
|
accentColor: theme.accentColor ?? DEFAULT_ORB_APPEARANCE.accentColor,
|
|
@@ -30,6 +31,9 @@ function toResolved(props, enabled) {
|
|
|
30
31
|
keyboardChip: theme.keyboardChip ?? null,
|
|
31
32
|
modalTheme: props.modalTheme,
|
|
32
33
|
screenPosition: props.screenPosition,
|
|
34
|
+
bubbleTheme: resolveBubbleTheme(published.bubbleTheme),
|
|
35
|
+
bubbleEnabled: published.bubbleEnabled !== false,
|
|
36
|
+
voiceInputEnabled: published.voiceInputEnabled !== false,
|
|
33
37
|
enabled,
|
|
34
38
|
};
|
|
35
39
|
}
|
|
@@ -51,7 +55,7 @@ export function watchPublishedConfig(controller, options) {
|
|
|
51
55
|
if (cancelled)
|
|
52
56
|
return;
|
|
53
57
|
options.onChange(res.published
|
|
54
|
-
? toResolved(mapPublishedConfigToOrbProps(res.published), res.published
|
|
58
|
+
? toResolved(mapPublishedConfigToOrbProps(res.published), res.published)
|
|
55
59
|
: null);
|
|
56
60
|
}
|
|
57
61
|
catch {
|
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.9";
|
|
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.9';
|
|
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.9",
|
|
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": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"prepublishOnly": "node ../../scripts/sync-sdk-version.mjs && npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@fiodos/core": "^0.1.
|
|
32
|
+
"@fiodos/core": "^0.1.7"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^22.0.0",
|