@fiodos/web-core 0.1.7 → 0.1.10
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 +181 -2
- 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 +182 -3
- 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,11 @@ 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;box-sizing:border-box;display:flex;align-items:center;width:max-content;max-width:min(280px,70vw);white-space:nowrap;overflow:hidden;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}}
|
|
47
|
+
@keyframes fyodos-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
|
48
|
+
.fyodos-thought-track{display:flex;flex-shrink:0;width:max-content;will-change:transform;}
|
|
49
|
+
.fyodos-thought-track>span{flex-shrink:0;padding-right:48px;white-space:nowrap;}
|
|
45
50
|
@keyframes fyodos-fade-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
46
51
|
@keyframes fyodos-blink{0%,100%{opacity:.25}50%{opacity:.9}}
|
|
47
52
|
@keyframes fy-spin{to{transform:rotate(360deg)}}
|
|
@@ -130,6 +135,12 @@ function mountOrb(controller, options = {}) {
|
|
|
130
135
|
let publishedAppearance = null;
|
|
131
136
|
let chipTheme = null;
|
|
132
137
|
let modalTheme;
|
|
138
|
+
let bubbleTheme = (0, core_1.resolveBubbleTheme)(null);
|
|
139
|
+
let bubbleEnabled = true;
|
|
140
|
+
// Dashboard VOICE switch. Default true (voice on). When false the orb never
|
|
141
|
+
// listens/speaks: a tap opens the keyboard directly and text turns are silent.
|
|
142
|
+
let voiceInputEnabled = true;
|
|
143
|
+
let currentRoute = null;
|
|
133
144
|
let screenPosition = options.corner
|
|
134
145
|
? cornerToPosition(options.corner)
|
|
135
146
|
: { ...core_1.DEFAULT_ORB_SCREEN_POSITION };
|
|
@@ -160,6 +171,35 @@ function mountOrb(controller, options = {}) {
|
|
|
160
171
|
anchor.appendChild(btn);
|
|
161
172
|
root.appendChild(anchor);
|
|
162
173
|
let chipEl = null;
|
|
174
|
+
// Thought bubble: a short, screen-aware CTA beside the orb. Created once and
|
|
175
|
+
// shown/hidden by syncThoughtBubble; tapping it opens the keyboard.
|
|
176
|
+
const thoughtBubbleEl = document.createElement('div');
|
|
177
|
+
thoughtBubbleEl.className = 'fyodos-thought';
|
|
178
|
+
thoughtBubbleEl.setAttribute('role', 'button');
|
|
179
|
+
thoughtBubbleEl.tabIndex = 0;
|
|
180
|
+
thoughtBubbleEl.style.display = 'none';
|
|
181
|
+
thoughtBubbleEl.addEventListener('click', () => {
|
|
182
|
+
haptics.trigger('orbTap');
|
|
183
|
+
controller.primeAudio();
|
|
184
|
+
if (enableTextInput) {
|
|
185
|
+
const prompt = resolveCurrentBubblePrompt();
|
|
186
|
+
openTextPanel(prompt
|
|
187
|
+
? (0, core_1.bubblePromptToAffirmative)(prompt, {
|
|
188
|
+
locale: controller.config.locale,
|
|
189
|
+
messages: controller.messages,
|
|
190
|
+
})
|
|
191
|
+
: undefined);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
void controller.toggleListening();
|
|
195
|
+
});
|
|
196
|
+
thoughtBubbleEl.addEventListener('keydown', (event) => {
|
|
197
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
thoughtBubbleEl.click();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
anchor.appendChild(thoughtBubbleEl);
|
|
163
203
|
container.appendChild(root);
|
|
164
204
|
// ── Position (dashboard position; user drag is SESSION-only, never persisted) ─
|
|
165
205
|
// The orb ALWAYS starts at the dashboard-published position on every load.
|
|
@@ -209,6 +249,9 @@ function mountOrb(controller, options = {}) {
|
|
|
209
249
|
chipEl.style.right = '';
|
|
210
250
|
}
|
|
211
251
|
}
|
|
252
|
+
// Keep the thought bubble on the orb's free side too while it is visible.
|
|
253
|
+
if (thoughtBubbleEl.style.display !== 'none')
|
|
254
|
+
positionThoughtBubble();
|
|
212
255
|
}
|
|
213
256
|
window.addEventListener('resize', reposition);
|
|
214
257
|
// Recalc when a scrollbar appears/disappears as the page content changes (the
|
|
@@ -423,9 +466,14 @@ function mountOrb(controller, options = {}) {
|
|
|
423
466
|
// STAYS OPEN. This is the unified, reference-matching behaviour.
|
|
424
467
|
void controller.submitTextTurn(value);
|
|
425
468
|
}
|
|
426
|
-
function openTextPanel() {
|
|
469
|
+
function openTextPanel(seed, autoSend = false) {
|
|
427
470
|
controller.cancelAll();
|
|
471
|
+
const draft = seed?.trim() ?? '';
|
|
472
|
+
textInput.value = draft;
|
|
428
473
|
setBubble(true);
|
|
474
|
+
if (autoSend && draft) {
|
|
475
|
+
queueMicrotask(() => submitText());
|
|
476
|
+
}
|
|
429
477
|
}
|
|
430
478
|
// ── Keyboard chip (rebuilt when theme changes / visibility toggles) ──────────
|
|
431
479
|
// Appears beside the ACTIVE orb — whether listening for real (voice granted)
|
|
@@ -459,6 +507,102 @@ function mountOrb(controller, options = {}) {
|
|
|
459
507
|
anchor.appendChild(chipEl);
|
|
460
508
|
applyDeployment();
|
|
461
509
|
}
|
|
510
|
+
// ── Thought bubble (screen-aware CTA beside the orb) ─────────────────────────
|
|
511
|
+
function resolveCurrentBubblePrompt() {
|
|
512
|
+
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 });
|
|
513
|
+
}
|
|
514
|
+
// Render the CTA on ONE line at its NATURAL size. When the phrase is too long
|
|
515
|
+
// to fit the max width, scroll it (marquee, right-to-left, looping) instead of
|
|
516
|
+
// shrinking the font or truncating — the bubble keeps a constant height.
|
|
517
|
+
function layoutThoughtContent(prompt) {
|
|
518
|
+
const t = bubbleTheme;
|
|
519
|
+
const { padH } = (0, core_1.resolveBubblePadding)(t);
|
|
520
|
+
// Measure the natural (single-copy) text width at the base font size.
|
|
521
|
+
thoughtBubbleEl.style.width = '';
|
|
522
|
+
thoughtBubbleEl.textContent = prompt;
|
|
523
|
+
const measured = thoughtBubbleEl.scrollWidth;
|
|
524
|
+
const viewport = typeof window !== 'undefined' ? window.innerWidth : 280;
|
|
525
|
+
const maxW = Math.min(280, viewport * 0.7);
|
|
526
|
+
const overflow = measured > maxW - t.borderWidth * 2;
|
|
527
|
+
if (!overflow) {
|
|
528
|
+
thoughtBubbleEl.style.width = '';
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const textWidth = Math.max(0, measured - padH * 2);
|
|
532
|
+
const durationSec = Math.max(4, (textWidth + 48) / 30);
|
|
533
|
+
thoughtBubbleEl.style.width = `${maxW}px`;
|
|
534
|
+
thoughtBubbleEl.textContent = '';
|
|
535
|
+
const track = document.createElement('div');
|
|
536
|
+
track.className = 'fyodos-thought-track';
|
|
537
|
+
track.style.animation = `fyodos-marquee ${durationSec}s linear infinite`;
|
|
538
|
+
const first = document.createElement('span');
|
|
539
|
+
first.textContent = prompt;
|
|
540
|
+
const second = document.createElement('span');
|
|
541
|
+
second.textContent = prompt;
|
|
542
|
+
second.setAttribute('aria-hidden', 'true');
|
|
543
|
+
track.appendChild(first);
|
|
544
|
+
track.appendChild(second);
|
|
545
|
+
thoughtBubbleEl.appendChild(track);
|
|
546
|
+
}
|
|
547
|
+
function applyThoughtBubbleTheme() {
|
|
548
|
+
const t = bubbleTheme;
|
|
549
|
+
const { fontPx, padH, approxHeight } = (0, core_1.resolveBubblePadding)(t);
|
|
550
|
+
const shapeCss = (0, core_1.bubbleShapeToCss)(t, approxHeight);
|
|
551
|
+
thoughtBubbleEl.style.fontSize = `${fontPx}px`;
|
|
552
|
+
thoughtBubbleEl.style.height = `${approxHeight}px`;
|
|
553
|
+
thoughtBubbleEl.style.padding = `0 ${padH}px`;
|
|
554
|
+
thoughtBubbleEl.style.color = t.textColor;
|
|
555
|
+
thoughtBubbleEl.style.background = t.backgroundColor;
|
|
556
|
+
thoughtBubbleEl.style.border = `${t.borderWidth}px solid ${t.borderColor}`;
|
|
557
|
+
const setImportant = (prop, value) => {
|
|
558
|
+
if (!value) {
|
|
559
|
+
thoughtBubbleEl.style.removeProperty(prop);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
thoughtBubbleEl.style.setProperty(prop, value, 'important');
|
|
563
|
+
};
|
|
564
|
+
setImportant('border-radius', shapeCss.borderRadius);
|
|
565
|
+
setImportant('border-top-left-radius', shapeCss.borderTopLeftRadius);
|
|
566
|
+
setImportant('border-top-right-radius', shapeCss.borderTopRightRadius);
|
|
567
|
+
setImportant('border-bottom-left-radius', shapeCss.borderBottomLeftRadius);
|
|
568
|
+
setImportant('border-bottom-right-radius', shapeCss.borderBottomRightRadius);
|
|
569
|
+
setImportant('clip-path', shapeCss.clipPath);
|
|
570
|
+
}
|
|
571
|
+
function positionThoughtBubble() {
|
|
572
|
+
const dep = (0, core_1.resolveOrbDeployment)(effectivePosition());
|
|
573
|
+
if (dep.horizontal === 'left') {
|
|
574
|
+
thoughtBubbleEl.style.right = 'calc(100% + 10px)';
|
|
575
|
+
thoughtBubbleEl.style.left = '';
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
thoughtBubbleEl.style.left = 'calc(100% + 10px)';
|
|
579
|
+
thoughtBubbleEl.style.right = '';
|
|
580
|
+
}
|
|
581
|
+
if (dep.vertical === 'up') {
|
|
582
|
+
thoughtBubbleEl.style.top = '0';
|
|
583
|
+
thoughtBubbleEl.style.bottom = '';
|
|
584
|
+
thoughtBubbleEl.style.transform = '';
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
thoughtBubbleEl.style.bottom = '0';
|
|
588
|
+
thoughtBubbleEl.style.top = '';
|
|
589
|
+
thoughtBubbleEl.style.transform = '';
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function syncThoughtBubble(state) {
|
|
593
|
+
const idle = !state.showThinking && !state.showWaveform && state.phase !== 'speaking' && !micFallbackActive;
|
|
594
|
+
const prompt = bubbleEnabled && !bubbleOpen && idle ? resolveCurrentBubblePrompt() : null;
|
|
595
|
+
if (!prompt) {
|
|
596
|
+
thoughtBubbleEl.style.display = 'none';
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
thoughtBubbleEl.setAttribute('aria-label', prompt);
|
|
600
|
+
// Display before measuring so `scrollWidth` (used to detect overflow) is real.
|
|
601
|
+
thoughtBubbleEl.style.display = 'flex';
|
|
602
|
+
applyThoughtBubbleTheme();
|
|
603
|
+
layoutThoughtContent(prompt);
|
|
604
|
+
positionThoughtBubble();
|
|
605
|
+
}
|
|
462
606
|
// ── Overlays (confirmation + consent) ────────────────────────────────────────
|
|
463
607
|
let overlayEl = null;
|
|
464
608
|
function clearOverlay() {
|
|
@@ -571,9 +715,16 @@ function mountOrb(controller, options = {}) {
|
|
|
571
715
|
setBubble(false);
|
|
572
716
|
return;
|
|
573
717
|
}
|
|
718
|
+
// Voice disabled in the dashboard: never listen/speak. Tap opens the keyboard
|
|
719
|
+
// directly (text turns are silent by default); animations are unchanged.
|
|
720
|
+
if (!voiceInputEnabled && enableTextInput) {
|
|
721
|
+
micFallbackActive = false;
|
|
722
|
+
openTextPanel();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
574
725
|
// `voiceAvailable` is dynamic: the Web Speech API may be present yet the mic
|
|
575
726
|
// denied/blocked, in which case we skip voice and go straight to the keyboard.
|
|
576
|
-
if (controller.voiceAvailable) {
|
|
727
|
+
if (controller.voiceAvailable && voiceInputEnabled) {
|
|
577
728
|
micFallbackActive = false;
|
|
578
729
|
void controller.toggleListening();
|
|
579
730
|
return;
|
|
@@ -663,6 +814,7 @@ function mountOrb(controller, options = {}) {
|
|
|
663
814
|
sendBtn.disabled = false;
|
|
664
815
|
}
|
|
665
816
|
syncChip(state);
|
|
817
|
+
syncThoughtBubble(state);
|
|
666
818
|
if (state.confirmationMessage !== lastConfirm) {
|
|
667
819
|
lastConfirm = state.confirmationMessage;
|
|
668
820
|
if (state.confirmationMessage)
|
|
@@ -680,6 +832,26 @@ function mountOrb(controller, options = {}) {
|
|
|
680
832
|
}
|
|
681
833
|
const unsubscribe = controller.subscribe(render);
|
|
682
834
|
render(controller.getState());
|
|
835
|
+
// Track the current route so the thought bubble re-resolves its CTA on
|
|
836
|
+
// navigation. No universal navigation event exists, so poll + listen to
|
|
837
|
+
// popstate / visibility for snappier updates.
|
|
838
|
+
const readRoute = () => {
|
|
839
|
+
let next = null;
|
|
840
|
+
try {
|
|
841
|
+
next = controller.config.navigation.getCurrentRoute();
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
/* navigation read is best-effort */
|
|
845
|
+
}
|
|
846
|
+
if (next !== currentRoute) {
|
|
847
|
+
currentRoute = next;
|
|
848
|
+
syncThoughtBubble(controller.getState());
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
readRoute();
|
|
852
|
+
const routeTimer = window.setInterval(readRoute, 700);
|
|
853
|
+
window.addEventListener('popstate', readRoute);
|
|
854
|
+
document.addEventListener('visibilitychange', readRoute);
|
|
683
855
|
// ── Live dashboard config ────────────────────────────────────────────────────
|
|
684
856
|
const stopWatch = (0, publishedConfig_1.watchPublishedConfig)(controller, {
|
|
685
857
|
pollMs: options.appearancePollMs,
|
|
@@ -692,6 +864,9 @@ function mountOrb(controller, options = {}) {
|
|
|
692
864
|
publishedAppearance = config.appearance;
|
|
693
865
|
chipTheme = config.keyboardChip;
|
|
694
866
|
modalTheme = config.modalTheme;
|
|
867
|
+
bubbleTheme = config.bubbleTheme;
|
|
868
|
+
bubbleEnabled = config.bubbleEnabled;
|
|
869
|
+
voiceInputEnabled = config.voiceInputEnabled;
|
|
695
870
|
if (usePublishedPosition && config.screenPosition)
|
|
696
871
|
screenPosition = config.screenPosition;
|
|
697
872
|
}
|
|
@@ -702,6 +877,7 @@ function mountOrb(controller, options = {}) {
|
|
|
702
877
|
chipEl = null;
|
|
703
878
|
}
|
|
704
879
|
syncChip(controller.getState());
|
|
880
|
+
syncThoughtBubble(controller.getState());
|
|
705
881
|
if (bubbleOpen)
|
|
706
882
|
applyBubbleTheme();
|
|
707
883
|
},
|
|
@@ -710,6 +886,9 @@ function mountOrb(controller, options = {}) {
|
|
|
710
886
|
unmount() {
|
|
711
887
|
unsubscribe();
|
|
712
888
|
stopWatch();
|
|
889
|
+
window.clearInterval(routeTimer);
|
|
890
|
+
window.removeEventListener('popstate', readRoute);
|
|
891
|
+
document.removeEventListener('visibilitychange', readRoute);
|
|
713
892
|
window.removeEventListener('resize', reposition);
|
|
714
893
|
contentObserver?.disconnect();
|
|
715
894
|
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.10";
|
|
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.10';
|
|
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, bubblePromptToAffirmative, resolveBubbleTheme, resolveBubblePadding, bubbleShapeToCss, 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,11 @@ 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;box-sizing:border-box;display:flex;align-items:center;width:max-content;max-width:min(280px,70vw);white-space:nowrap;overflow:hidden;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}}
|
|
44
|
+
@keyframes fyodos-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
|
45
|
+
.fyodos-thought-track{display:flex;flex-shrink:0;width:max-content;will-change:transform;}
|
|
46
|
+
.fyodos-thought-track>span{flex-shrink:0;padding-right:48px;white-space:nowrap;}
|
|
42
47
|
@keyframes fyodos-fade-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
43
48
|
@keyframes fyodos-blink{0%,100%{opacity:.25}50%{opacity:.9}}
|
|
44
49
|
@keyframes fy-spin{to{transform:rotate(360deg)}}
|
|
@@ -127,6 +132,12 @@ export function mountOrb(controller, options = {}) {
|
|
|
127
132
|
let publishedAppearance = null;
|
|
128
133
|
let chipTheme = null;
|
|
129
134
|
let modalTheme;
|
|
135
|
+
let bubbleTheme = 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;
|
|
130
141
|
let screenPosition = options.corner
|
|
131
142
|
? cornerToPosition(options.corner)
|
|
132
143
|
: { ...DEFAULT_ORB_SCREEN_POSITION };
|
|
@@ -157,6 +168,35 @@ export function mountOrb(controller, options = {}) {
|
|
|
157
168
|
anchor.appendChild(btn);
|
|
158
169
|
root.appendChild(anchor);
|
|
159
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('div');
|
|
174
|
+
thoughtBubbleEl.className = 'fyodos-thought';
|
|
175
|
+
thoughtBubbleEl.setAttribute('role', 'button');
|
|
176
|
+
thoughtBubbleEl.tabIndex = 0;
|
|
177
|
+
thoughtBubbleEl.style.display = 'none';
|
|
178
|
+
thoughtBubbleEl.addEventListener('click', () => {
|
|
179
|
+
haptics.trigger('orbTap');
|
|
180
|
+
controller.primeAudio();
|
|
181
|
+
if (enableTextInput) {
|
|
182
|
+
const prompt = resolveCurrentBubblePrompt();
|
|
183
|
+
openTextPanel(prompt
|
|
184
|
+
? bubblePromptToAffirmative(prompt, {
|
|
185
|
+
locale: controller.config.locale,
|
|
186
|
+
messages: controller.messages,
|
|
187
|
+
})
|
|
188
|
+
: undefined);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
void controller.toggleListening();
|
|
192
|
+
});
|
|
193
|
+
thoughtBubbleEl.addEventListener('keydown', (event) => {
|
|
194
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
thoughtBubbleEl.click();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
anchor.appendChild(thoughtBubbleEl);
|
|
160
200
|
container.appendChild(root);
|
|
161
201
|
// ── Position (dashboard position; user drag is SESSION-only, never persisted) ─
|
|
162
202
|
// The orb ALWAYS starts at the dashboard-published position on every load.
|
|
@@ -206,6 +246,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
206
246
|
chipEl.style.right = '';
|
|
207
247
|
}
|
|
208
248
|
}
|
|
249
|
+
// Keep the thought bubble on the orb's free side too while it is visible.
|
|
250
|
+
if (thoughtBubbleEl.style.display !== 'none')
|
|
251
|
+
positionThoughtBubble();
|
|
209
252
|
}
|
|
210
253
|
window.addEventListener('resize', reposition);
|
|
211
254
|
// Recalc when a scrollbar appears/disappears as the page content changes (the
|
|
@@ -420,9 +463,14 @@ export function mountOrb(controller, options = {}) {
|
|
|
420
463
|
// STAYS OPEN. This is the unified, reference-matching behaviour.
|
|
421
464
|
void controller.submitTextTurn(value);
|
|
422
465
|
}
|
|
423
|
-
function openTextPanel() {
|
|
466
|
+
function openTextPanel(seed, autoSend = false) {
|
|
424
467
|
controller.cancelAll();
|
|
468
|
+
const draft = seed?.trim() ?? '';
|
|
469
|
+
textInput.value = draft;
|
|
425
470
|
setBubble(true);
|
|
471
|
+
if (autoSend && draft) {
|
|
472
|
+
queueMicrotask(() => submitText());
|
|
473
|
+
}
|
|
426
474
|
}
|
|
427
475
|
// ── Keyboard chip (rebuilt when theme changes / visibility toggles) ──────────
|
|
428
476
|
// Appears beside the ACTIVE orb — whether listening for real (voice granted)
|
|
@@ -456,6 +504,102 @@ export function mountOrb(controller, options = {}) {
|
|
|
456
504
|
anchor.appendChild(chipEl);
|
|
457
505
|
applyDeployment();
|
|
458
506
|
}
|
|
507
|
+
// ── Thought bubble (screen-aware CTA beside the orb) ─────────────────────────
|
|
508
|
+
function resolveCurrentBubblePrompt() {
|
|
509
|
+
return resolveBubblePrompt(matchRouteIntent(currentRoute, controller.config.manifest.routes), controller.messages, { locale: controller.config.locale, actions: controller.config.manifest.actions });
|
|
510
|
+
}
|
|
511
|
+
// Render the CTA on ONE line at its NATURAL size. When the phrase is too long
|
|
512
|
+
// to fit the max width, scroll it (marquee, right-to-left, looping) instead of
|
|
513
|
+
// shrinking the font or truncating — the bubble keeps a constant height.
|
|
514
|
+
function layoutThoughtContent(prompt) {
|
|
515
|
+
const t = bubbleTheme;
|
|
516
|
+
const { padH } = resolveBubblePadding(t);
|
|
517
|
+
// Measure the natural (single-copy) text width at the base font size.
|
|
518
|
+
thoughtBubbleEl.style.width = '';
|
|
519
|
+
thoughtBubbleEl.textContent = prompt;
|
|
520
|
+
const measured = thoughtBubbleEl.scrollWidth;
|
|
521
|
+
const viewport = typeof window !== 'undefined' ? window.innerWidth : 280;
|
|
522
|
+
const maxW = Math.min(280, viewport * 0.7);
|
|
523
|
+
const overflow = measured > maxW - t.borderWidth * 2;
|
|
524
|
+
if (!overflow) {
|
|
525
|
+
thoughtBubbleEl.style.width = '';
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const textWidth = Math.max(0, measured - padH * 2);
|
|
529
|
+
const durationSec = Math.max(4, (textWidth + 48) / 30);
|
|
530
|
+
thoughtBubbleEl.style.width = `${maxW}px`;
|
|
531
|
+
thoughtBubbleEl.textContent = '';
|
|
532
|
+
const track = document.createElement('div');
|
|
533
|
+
track.className = 'fyodos-thought-track';
|
|
534
|
+
track.style.animation = `fyodos-marquee ${durationSec}s linear infinite`;
|
|
535
|
+
const first = document.createElement('span');
|
|
536
|
+
first.textContent = prompt;
|
|
537
|
+
const second = document.createElement('span');
|
|
538
|
+
second.textContent = prompt;
|
|
539
|
+
second.setAttribute('aria-hidden', 'true');
|
|
540
|
+
track.appendChild(first);
|
|
541
|
+
track.appendChild(second);
|
|
542
|
+
thoughtBubbleEl.appendChild(track);
|
|
543
|
+
}
|
|
544
|
+
function applyThoughtBubbleTheme() {
|
|
545
|
+
const t = bubbleTheme;
|
|
546
|
+
const { fontPx, padH, approxHeight } = resolveBubblePadding(t);
|
|
547
|
+
const shapeCss = bubbleShapeToCss(t, approxHeight);
|
|
548
|
+
thoughtBubbleEl.style.fontSize = `${fontPx}px`;
|
|
549
|
+
thoughtBubbleEl.style.height = `${approxHeight}px`;
|
|
550
|
+
thoughtBubbleEl.style.padding = `0 ${padH}px`;
|
|
551
|
+
thoughtBubbleEl.style.color = t.textColor;
|
|
552
|
+
thoughtBubbleEl.style.background = t.backgroundColor;
|
|
553
|
+
thoughtBubbleEl.style.border = `${t.borderWidth}px solid ${t.borderColor}`;
|
|
554
|
+
const setImportant = (prop, value) => {
|
|
555
|
+
if (!value) {
|
|
556
|
+
thoughtBubbleEl.style.removeProperty(prop);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
thoughtBubbleEl.style.setProperty(prop, value, 'important');
|
|
560
|
+
};
|
|
561
|
+
setImportant('border-radius', shapeCss.borderRadius);
|
|
562
|
+
setImportant('border-top-left-radius', shapeCss.borderTopLeftRadius);
|
|
563
|
+
setImportant('border-top-right-radius', shapeCss.borderTopRightRadius);
|
|
564
|
+
setImportant('border-bottom-left-radius', shapeCss.borderBottomLeftRadius);
|
|
565
|
+
setImportant('border-bottom-right-radius', shapeCss.borderBottomRightRadius);
|
|
566
|
+
setImportant('clip-path', shapeCss.clipPath);
|
|
567
|
+
}
|
|
568
|
+
function positionThoughtBubble() {
|
|
569
|
+
const dep = resolveOrbDeployment(effectivePosition());
|
|
570
|
+
if (dep.horizontal === 'left') {
|
|
571
|
+
thoughtBubbleEl.style.right = 'calc(100% + 10px)';
|
|
572
|
+
thoughtBubbleEl.style.left = '';
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
thoughtBubbleEl.style.left = 'calc(100% + 10px)';
|
|
576
|
+
thoughtBubbleEl.style.right = '';
|
|
577
|
+
}
|
|
578
|
+
if (dep.vertical === 'up') {
|
|
579
|
+
thoughtBubbleEl.style.top = '0';
|
|
580
|
+
thoughtBubbleEl.style.bottom = '';
|
|
581
|
+
thoughtBubbleEl.style.transform = '';
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
thoughtBubbleEl.style.bottom = '0';
|
|
585
|
+
thoughtBubbleEl.style.top = '';
|
|
586
|
+
thoughtBubbleEl.style.transform = '';
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function syncThoughtBubble(state) {
|
|
590
|
+
const idle = !state.showThinking && !state.showWaveform && state.phase !== 'speaking' && !micFallbackActive;
|
|
591
|
+
const prompt = bubbleEnabled && !bubbleOpen && idle ? resolveCurrentBubblePrompt() : null;
|
|
592
|
+
if (!prompt) {
|
|
593
|
+
thoughtBubbleEl.style.display = 'none';
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
thoughtBubbleEl.setAttribute('aria-label', prompt);
|
|
597
|
+
// Display before measuring so `scrollWidth` (used to detect overflow) is real.
|
|
598
|
+
thoughtBubbleEl.style.display = 'flex';
|
|
599
|
+
applyThoughtBubbleTheme();
|
|
600
|
+
layoutThoughtContent(prompt);
|
|
601
|
+
positionThoughtBubble();
|
|
602
|
+
}
|
|
459
603
|
// ── Overlays (confirmation + consent) ────────────────────────────────────────
|
|
460
604
|
let overlayEl = null;
|
|
461
605
|
function clearOverlay() {
|
|
@@ -568,9 +712,16 @@ export function mountOrb(controller, options = {}) {
|
|
|
568
712
|
setBubble(false);
|
|
569
713
|
return;
|
|
570
714
|
}
|
|
715
|
+
// Voice disabled in the dashboard: never listen/speak. Tap opens the keyboard
|
|
716
|
+
// directly (text turns are silent by default); animations are unchanged.
|
|
717
|
+
if (!voiceInputEnabled && enableTextInput) {
|
|
718
|
+
micFallbackActive = false;
|
|
719
|
+
openTextPanel();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
571
722
|
// `voiceAvailable` is dynamic: the Web Speech API may be present yet the mic
|
|
572
723
|
// denied/blocked, in which case we skip voice and go straight to the keyboard.
|
|
573
|
-
if (controller.voiceAvailable) {
|
|
724
|
+
if (controller.voiceAvailable && voiceInputEnabled) {
|
|
574
725
|
micFallbackActive = false;
|
|
575
726
|
void controller.toggleListening();
|
|
576
727
|
return;
|
|
@@ -660,6 +811,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
660
811
|
sendBtn.disabled = false;
|
|
661
812
|
}
|
|
662
813
|
syncChip(state);
|
|
814
|
+
syncThoughtBubble(state);
|
|
663
815
|
if (state.confirmationMessage !== lastConfirm) {
|
|
664
816
|
lastConfirm = state.confirmationMessage;
|
|
665
817
|
if (state.confirmationMessage)
|
|
@@ -677,6 +829,26 @@ export function mountOrb(controller, options = {}) {
|
|
|
677
829
|
}
|
|
678
830
|
const unsubscribe = controller.subscribe(render);
|
|
679
831
|
render(controller.getState());
|
|
832
|
+
// Track the current route so the thought bubble re-resolves its CTA on
|
|
833
|
+
// navigation. No universal navigation event exists, so poll + listen to
|
|
834
|
+
// popstate / visibility for snappier updates.
|
|
835
|
+
const readRoute = () => {
|
|
836
|
+
let next = null;
|
|
837
|
+
try {
|
|
838
|
+
next = controller.config.navigation.getCurrentRoute();
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
/* navigation read is best-effort */
|
|
842
|
+
}
|
|
843
|
+
if (next !== currentRoute) {
|
|
844
|
+
currentRoute = next;
|
|
845
|
+
syncThoughtBubble(controller.getState());
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
readRoute();
|
|
849
|
+
const routeTimer = window.setInterval(readRoute, 700);
|
|
850
|
+
window.addEventListener('popstate', readRoute);
|
|
851
|
+
document.addEventListener('visibilitychange', readRoute);
|
|
680
852
|
// ── Live dashboard config ────────────────────────────────────────────────────
|
|
681
853
|
const stopWatch = watchPublishedConfig(controller, {
|
|
682
854
|
pollMs: options.appearancePollMs,
|
|
@@ -689,6 +861,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
689
861
|
publishedAppearance = config.appearance;
|
|
690
862
|
chipTheme = config.keyboardChip;
|
|
691
863
|
modalTheme = config.modalTheme;
|
|
864
|
+
bubbleTheme = config.bubbleTheme;
|
|
865
|
+
bubbleEnabled = config.bubbleEnabled;
|
|
866
|
+
voiceInputEnabled = config.voiceInputEnabled;
|
|
692
867
|
if (usePublishedPosition && config.screenPosition)
|
|
693
868
|
screenPosition = config.screenPosition;
|
|
694
869
|
}
|
|
@@ -699,6 +874,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
699
874
|
chipEl = null;
|
|
700
875
|
}
|
|
701
876
|
syncChip(controller.getState());
|
|
877
|
+
syncThoughtBubble(controller.getState());
|
|
702
878
|
if (bubbleOpen)
|
|
703
879
|
applyBubbleTheme();
|
|
704
880
|
},
|
|
@@ -707,6 +883,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
707
883
|
unmount() {
|
|
708
884
|
unsubscribe();
|
|
709
885
|
stopWatch();
|
|
886
|
+
window.clearInterval(routeTimer);
|
|
887
|
+
window.removeEventListener('popstate', readRoute);
|
|
888
|
+
document.removeEventListener('visibilitychange', readRoute);
|
|
710
889
|
window.removeEventListener('resize', reposition);
|
|
711
890
|
contentObserver?.disconnect();
|
|
712
891
|
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.10";
|
|
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.10';
|
|
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.10",
|
|
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",
|