@fiodos/web-core 0.1.12 → 0.1.15
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/adapters/webCredentialAutofillAdapter.d.ts +16 -0
- package/dist/cjs/adapters/webCredentialAutofillAdapter.js +47 -0
- package/dist/cjs/api/backendClient.d.ts +10 -3
- package/dist/cjs/api/backendClient.js +31 -9
- package/dist/cjs/config/types.d.ts +22 -2
- package/dist/cjs/controller/AgentController.d.ts +14 -0
- package/dist/cjs/controller/AgentController.js +65 -2
- package/dist/cjs/dropin/createFiodosAgent.d.ts +27 -1
- package/dist/cjs/dropin/createFiodosAgent.js +48 -4
- package/dist/cjs/embed/dynamics.d.ts +61 -0
- package/dist/cjs/embed/dynamics.js +173 -0
- package/dist/cjs/embed/global.d.ts +45 -0
- package/dist/cjs/embed/global.js +48 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/orb/mountOrb.js +172 -28
- package/dist/cjs/orb/orbView.js +42 -8
- package/dist/cjs/orb/publishedConfig.d.ts +4 -2
- package/dist/cjs/orb/publishedConfig.js +56 -7
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/embed/fiodos-embed.js +66 -0
- package/dist/esm/adapters/webCredentialAutofillAdapter.d.ts +16 -0
- package/dist/esm/adapters/webCredentialAutofillAdapter.js +44 -0
- package/dist/esm/api/backendClient.d.ts +10 -3
- package/dist/esm/api/backendClient.js +32 -10
- package/dist/esm/config/types.d.ts +22 -2
- package/dist/esm/controller/AgentController.d.ts +14 -0
- package/dist/esm/controller/AgentController.js +65 -2
- package/dist/esm/dropin/createFiodosAgent.d.ts +27 -1
- package/dist/esm/dropin/createFiodosAgent.js +48 -4
- package/dist/esm/embed/dynamics.d.ts +61 -0
- package/dist/esm/embed/dynamics.js +168 -0
- package/dist/esm/embed/global.d.ts +45 -0
- package/dist/esm/embed/global.js +46 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/orb/mountOrb.js +173 -29
- package/dist/esm/orb/orbView.js +43 -9
- package/dist/esm/orb/publishedConfig.d.ts +4 -2
- package/dist/esm/orb/publishedConfig.js +56 -7
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +5 -3
package/dist/cjs/orb/mountOrb.js
CHANGED
|
@@ -30,8 +30,23 @@ const CSS = `
|
|
|
30
30
|
.fyodos-orb-btn img{pointer-events:none;-webkit-user-drag:none;user-select:none;}
|
|
31
31
|
.fyodos-orb-btn:focus-visible{outline:2px solid #8ab6ff;outline-offset:4px;border-radius:50%;}
|
|
32
32
|
.fyodos-orb-anchor{position:relative;display:inline-flex;}
|
|
33
|
-
.fyodos-bubble{position:absolute;display:flex;flex-direction:column;width:min(320px,calc(100vw - 48px));max-height:340px;overflow:hidden;border-radius:16px;box-shadow:0 16px 40px rgba(0,0,0,0.28);border:1px solid rgba(160,180,220,0.22);animation:fyodos-fade-in .14s ease-out;}
|
|
34
|
-
.fyodos-bubble
|
|
33
|
+
.fyodos-bubble{position:absolute;display:flex;flex-direction:column;width:min(320px,calc(100vw - 48px));max-height:340px;overflow:hidden;border-radius:16px;box-shadow:0 16px 40px rgba(0,0,0,0.28);border:1px solid rgba(160,180,220,0.22);animation:fyodos-fade-in .14s ease-out;transition:width .25s cubic-bezier(0.22,1,0.36,1),max-height .25s cubic-bezier(0.22,1,0.36,1);cursor:zoom-in;}
|
|
34
|
+
.fyodos-bubble--expanded{width:min(560px,calc(100vw - 32px));max-height:min(620px,75vh);cursor:auto;}
|
|
35
|
+
.fyodos-bubble-user,.fyodos-bubble-reply{cursor:text;}
|
|
36
|
+
.fyodos-bubble-row{cursor:default;}
|
|
37
|
+
.fyodos-bubble--expanded .fyodos-bubble-scroll{padding:22px 24px 8px;gap:10px;}
|
|
38
|
+
.fyodos-bubble--expanded .fyodos-bubble-exchanges{gap:14px;}
|
|
39
|
+
.fyodos-bubble--expanded .fyodos-bubble-exchange{gap:9px;}
|
|
40
|
+
.fyodos-bubble--expanded .fyodos-bubble-exchange:first-child .fyodos-bubble-user{padding-right:0;}
|
|
41
|
+
.fyodos-bubble--expanded .fyodos-bubble-user{font-size:14.5px;line-height:21px;}
|
|
42
|
+
.fyodos-bubble--expanded .fyodos-bubble-reply{font-size:17px;line-height:25px;}
|
|
43
|
+
.fyodos-bubble--expanded .fyodos-bubble-row{padding:12px 16px;gap:10px;}
|
|
44
|
+
.fyodos-bubble--expanded .fyodos-bubble-busy{padding:8px 24px;}
|
|
45
|
+
.fyodos-bubble--expanded .fyodos-bubble-row input{padding:12px 14px;font-size:16px;}
|
|
46
|
+
.fyodos-bubble--expanded .fyodos-bubble-mic,.fyodos-bubble--expanded .fyodos-bubble-row button{width:38px;height:38px;min-width:38px;font-size:19px;}
|
|
47
|
+
.fyodos-bubble-close,.fyodos-bubble-unexpand{position:absolute;top:4px;z-index:1;display:flex;align-items:center;justify-content:center;border:none;background:transparent;font-size:18px;line-height:18px;width:28px;height:28px;cursor:pointer;padding:0;opacity:.75;}
|
|
48
|
+
.fyodos-bubble-close{right:6px;}
|
|
49
|
+
.fyodos-bubble-unexpand{right:38px;display:none;}
|
|
35
50
|
.fyodos-bubble-user-row{display:flex;flex-direction:row;align-items:flex-start;gap:6px;}
|
|
36
51
|
.fyodos-bubble-scroll{overflow-y:auto;padding:12px 14px 4px;display:flex;flex-direction:column;gap:6px;min-height:18px;scrollbar-width:none;-ms-overflow-style:none;}
|
|
37
52
|
.fyodos-bubble-scroll::-webkit-scrollbar{display:none;width:0;height:0;}
|
|
@@ -198,12 +213,13 @@ function mountOrb(controller, options = {}) {
|
|
|
198
213
|
const prompt = resolveCurrentBubblePrompt();
|
|
199
214
|
openTextPanel(prompt
|
|
200
215
|
? (0, core_1.bubblePromptToAffirmative)(prompt, {
|
|
201
|
-
locale:
|
|
216
|
+
locale: currentBubbleLocale(),
|
|
202
217
|
messages: controller.messages,
|
|
203
218
|
})
|
|
204
219
|
: undefined);
|
|
205
220
|
return;
|
|
206
221
|
}
|
|
222
|
+
userTriedVoice = true;
|
|
207
223
|
void controller.toggleListening();
|
|
208
224
|
});
|
|
209
225
|
thoughtBubbleEl.addEventListener('keydown', (event) => {
|
|
@@ -373,10 +389,22 @@ function mountOrb(controller, options = {}) {
|
|
|
373
389
|
btn.addEventListener('lostpointercapture', onPointerUp);
|
|
374
390
|
// ── Text bubble ─────────────────────────────────────────────────────────────
|
|
375
391
|
let bubbleOpen = false;
|
|
392
|
+
// Reduced (default) size stays exactly as before. Tapping any EMPTY area of
|
|
393
|
+
// the panel (not a message, not the input row, not a button — so text stays
|
|
394
|
+
// selectable/copyable) grows it to the expanded layout; shrinking back is an
|
|
395
|
+
// explicit tap on `unexpandBtn` next to the close button.
|
|
396
|
+
let bubbleExpanded = false;
|
|
376
397
|
// When the mic is denied/unavailable, tapping the orb still puts it in the
|
|
377
398
|
// SAME "active/listening" visual state and reveals the keyboard chip — the
|
|
378
399
|
// user is never blocked, text is always an open avenue (Change 1).
|
|
379
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;
|
|
380
408
|
const bubble = document.createElement('div');
|
|
381
409
|
bubble.className = 'fyodos-bubble';
|
|
382
410
|
bubble.style.display = 'none';
|
|
@@ -409,6 +437,19 @@ function mountOrb(controller, options = {}) {
|
|
|
409
437
|
closeBtn.textContent = '×';
|
|
410
438
|
closeBtn.setAttribute('aria-label', 'Close');
|
|
411
439
|
closeBtn.addEventListener('click', () => setBubble(false));
|
|
440
|
+
// Collapse button — only shown while expanded, right next to the close
|
|
441
|
+
// button (same minimalist style). Tapping an empty area of the panel is
|
|
442
|
+
// what EXPANDS it; shrinking back is always an explicit, discoverable tap.
|
|
443
|
+
const unexpandBtn = document.createElement('button');
|
|
444
|
+
unexpandBtn.type = 'button';
|
|
445
|
+
unexpandBtn.className = 'fyodos-bubble-unexpand';
|
|
446
|
+
unexpandBtn.innerHTML =
|
|
447
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7"></path></svg>';
|
|
448
|
+
unexpandBtn.setAttribute('aria-label', 'Collapse');
|
|
449
|
+
unexpandBtn.addEventListener('click', (e) => {
|
|
450
|
+
e.stopPropagation();
|
|
451
|
+
setBubbleExpanded(false);
|
|
452
|
+
});
|
|
412
453
|
// Loading row shown at the top while an older page of the session history
|
|
413
454
|
// streams in (scroll-to-top pagination).
|
|
414
455
|
const loaderRow = document.createElement('div');
|
|
@@ -432,6 +473,12 @@ function mountOrb(controller, options = {}) {
|
|
|
432
473
|
signature.endsWith(renderedSignature);
|
|
433
474
|
const fromBottom = exchangeScroll.scrollHeight - exchangeScroll.scrollTop;
|
|
434
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 ?? {}) };
|
|
435
482
|
exchangeList.replaceChildren();
|
|
436
483
|
for (const ex of exchanges) {
|
|
437
484
|
const block = document.createElement('div');
|
|
@@ -439,11 +486,13 @@ function mountOrb(controller, options = {}) {
|
|
|
439
486
|
const user = document.createElement('div');
|
|
440
487
|
user.className = 'fyodos-bubble-user';
|
|
441
488
|
user.textContent = ex.userText;
|
|
489
|
+
user.style.color = t.bodyColor;
|
|
442
490
|
block.append(user);
|
|
443
491
|
if (ex.replyText) {
|
|
444
492
|
const reply = document.createElement('div');
|
|
445
493
|
reply.className = 'fyodos-bubble-reply';
|
|
446
494
|
reply.textContent = ex.replyText;
|
|
495
|
+
reply.style.color = t.titleColor;
|
|
447
496
|
block.append(reply);
|
|
448
497
|
}
|
|
449
498
|
exchangeList.append(block);
|
|
@@ -550,7 +599,8 @@ function mountOrb(controller, options = {}) {
|
|
|
550
599
|
if (active) {
|
|
551
600
|
micBtn.style.background = accent;
|
|
552
601
|
micBtn.style.border = 'none';
|
|
553
|
-
|
|
602
|
+
// Ink readable on the (customizable) accent — dark accents need light ink.
|
|
603
|
+
micBtn.style.color = (0, core_1.fiodosInputInkColor)(accent);
|
|
554
604
|
micBtn.style.borderRadius = sendRadius;
|
|
555
605
|
}
|
|
556
606
|
else {
|
|
@@ -562,8 +612,71 @@ function mountOrb(controller, options = {}) {
|
|
|
562
612
|
micBtn.setAttribute('aria-label', active ? ui.micStopLabel : ui.micLabel);
|
|
563
613
|
micBtn.title = active ? ui.micStopLabel : ui.micLabel;
|
|
564
614
|
}
|
|
565
|
-
bubble.append(closeBtn, exchangeScroll, busyRow, inputRow);
|
|
615
|
+
bubble.append(closeBtn, unexpandBtn, exchangeScroll, busyRow, inputRow);
|
|
566
616
|
anchor.appendChild(bubble);
|
|
617
|
+
function setBubbleExpanded(expanded) {
|
|
618
|
+
if (bubbleExpanded === expanded)
|
|
619
|
+
return;
|
|
620
|
+
bubbleExpanded = expanded;
|
|
621
|
+
bubble.classList.toggle('fyodos-bubble--expanded', expanded);
|
|
622
|
+
unexpandBtn.style.display = expanded ? 'flex' : 'none';
|
|
623
|
+
positionBubble();
|
|
624
|
+
}
|
|
625
|
+
// Tap-to-expand: a plain click on the panel grows it. Text selection is
|
|
626
|
+
// never broken: a drag-selection is a move (threshold guard), a double/
|
|
627
|
+
// triple-click word/paragraph selection cancels the pending expand (detail
|
|
628
|
+
// guard + delay), and any active selection blocks it. Controls (input row,
|
|
629
|
+
// buttons) never expand.
|
|
630
|
+
let bubbleTapX = 0;
|
|
631
|
+
let bubbleTapY = 0;
|
|
632
|
+
let pendingExpandTimer = null;
|
|
633
|
+
const TAP_MOVE_THRESHOLD = 8;
|
|
634
|
+
const hasActiveSelection = () => {
|
|
635
|
+
const selection = typeof window !== 'undefined' ? window.getSelection?.() : null;
|
|
636
|
+
return !!selection && !selection.isCollapsed && selection.toString().length > 0;
|
|
637
|
+
};
|
|
638
|
+
const cancelPendingExpand = () => {
|
|
639
|
+
if (pendingExpandTimer != null) {
|
|
640
|
+
window.clearTimeout(pendingExpandTimer);
|
|
641
|
+
pendingExpandTimer = null;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
bubble.addEventListener('pointerdown', (e) => {
|
|
645
|
+
bubbleTapX = e.clientX;
|
|
646
|
+
bubbleTapY = e.clientY;
|
|
647
|
+
});
|
|
648
|
+
bubble.addEventListener('click', (e) => {
|
|
649
|
+
if (bubbleExpanded)
|
|
650
|
+
return;
|
|
651
|
+
const target = e.target;
|
|
652
|
+
if (target?.closest('.fyodos-bubble-row, .fyodos-bubble-close, .fyodos-bubble-unexpand, button, input, a')) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const moved = Math.hypot(e.clientX - bubbleTapX, e.clientY - bubbleTapY);
|
|
656
|
+
if (moved > TAP_MOVE_THRESHOLD)
|
|
657
|
+
return;
|
|
658
|
+
// Double/triple click is a word/paragraph selection gesture, never expand
|
|
659
|
+
// (it also cancels the pending expand from its own first click).
|
|
660
|
+
if (e.detail > 1) {
|
|
661
|
+
cancelPendingExpand();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
cancelPendingExpand();
|
|
665
|
+
const overText = !!target?.closest('.fyodos-bubble-user, .fyodos-bubble-reply');
|
|
666
|
+
if (!overText && !hasActiveSelection()) {
|
|
667
|
+
// Genuinely empty area, nothing selected — expand immediately.
|
|
668
|
+
setBubbleExpanded(true);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
// Over message text, or a selection may still be collapsing: expand after
|
|
672
|
+
// a short delay, re-checking the selection right before — a double-click's
|
|
673
|
+
// second click or a real selection cancels it.
|
|
674
|
+
pendingExpandTimer = window.setTimeout(() => {
|
|
675
|
+
pendingExpandTimer = null;
|
|
676
|
+
if (!hasActiveSelection())
|
|
677
|
+
setBubbleExpanded(true);
|
|
678
|
+
}, 280);
|
|
679
|
+
});
|
|
567
680
|
// Initial placement (after the bubble exists, so applyDeployment can lay it out).
|
|
568
681
|
reposition();
|
|
569
682
|
function applyBubbleTheme() {
|
|
@@ -574,6 +687,7 @@ function mountOrb(controller, options = {}) {
|
|
|
574
687
|
if (modalTheme?.borderRadius != null)
|
|
575
688
|
bubble.style.borderRadius = `${modalTheme.borderRadius}px`;
|
|
576
689
|
closeBtn.style.color = t.bodyColor;
|
|
690
|
+
unexpandBtn.style.color = t.bodyColor;
|
|
577
691
|
exchangeList.querySelectorAll('.fyodos-bubble-user').forEach((node) => {
|
|
578
692
|
node.style.color = t.bodyColor;
|
|
579
693
|
});
|
|
@@ -585,6 +699,9 @@ function mountOrb(controller, options = {}) {
|
|
|
585
699
|
textInput.style.color = (0, core_1.fiodosInputInkColor)(inputBg);
|
|
586
700
|
textInput.style.setProperty('--fyodos-input-placeholder', (0, core_1.fiodosInputMutedInkColor)(inputBg));
|
|
587
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);
|
|
588
705
|
sendBtn.style.borderRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
|
|
589
706
|
syncMicButton();
|
|
590
707
|
busyRow.querySelectorAll('span').forEach((s) => {
|
|
@@ -608,8 +725,18 @@ function mountOrb(controller, options = {}) {
|
|
|
608
725
|
controller.resetBubbleWindow();
|
|
609
726
|
// Closing the panel returns the orb to idle (also clears mic fallback).
|
|
610
727
|
micFallbackActive = false;
|
|
728
|
+
// Always re-open at the reduced size.
|
|
729
|
+
cancelPendingExpand();
|
|
730
|
+
setBubbleExpanded(false);
|
|
611
731
|
}
|
|
612
732
|
syncChip(controller.getState());
|
|
733
|
+
// Re-resolve the thought bubble NOW: closing the panel must bring the
|
|
734
|
+
// screen-aware CTA back immediately. Nothing else is guaranteed to run
|
|
735
|
+
// right after close (controller state does not change on close, the route
|
|
736
|
+
// poll only fires on changes, the config poll every ~12s) — without this
|
|
737
|
+
// the orb sat "naked" until some unrelated event. Opening hides it (the
|
|
738
|
+
// resolver returns null while the panel is open), so one call covers both.
|
|
739
|
+
syncThoughtBubble(controller.getState());
|
|
613
740
|
}
|
|
614
741
|
function submitText() {
|
|
615
742
|
// While a turn is in flight the button is a STOP button; never start a new
|
|
@@ -674,8 +801,22 @@ function mountOrb(controller, options = {}) {
|
|
|
674
801
|
applyDeployment();
|
|
675
802
|
}
|
|
676
803
|
// ── Thought bubble (screen-aware CTA beside the orb) ─────────────────────────
|
|
804
|
+
// LIVE locale: re-read the host app's active language on every resolution so
|
|
805
|
+
// the bubble text (analyzer `bubblePrompts` translations) follows the app's
|
|
806
|
+
// language switcher without remounting. Falls back to the mount-time locale.
|
|
807
|
+
function currentBubbleLocale() {
|
|
808
|
+
try {
|
|
809
|
+
const live = controller.config.getLocale?.();
|
|
810
|
+
if (typeof live === 'string' && live.trim())
|
|
811
|
+
return live.trim();
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
/* live locale read is best-effort */
|
|
815
|
+
}
|
|
816
|
+
return controller.config.locale;
|
|
817
|
+
}
|
|
677
818
|
function resolveCurrentBubblePrompt() {
|
|
678
|
-
return (0, core_1.resolveBubblePrompt)((0, core_1.matchRouteIntent)(currentRoute, controller.config.manifest.routes), controller.messages, { locale:
|
|
819
|
+
return (0, core_1.resolveBubblePrompt)((0, core_1.matchRouteIntent)(currentRoute, controller.config.manifest.routes), controller.messages, { locale: currentBubbleLocale(), actions: controller.config.manifest.actions });
|
|
679
820
|
}
|
|
680
821
|
// Render the CTA on ONE line at its NATURAL size. When the phrase is too long
|
|
681
822
|
// to fit the max width, scroll it (marquee, right-to-left, looping) instead of
|
|
@@ -710,29 +851,17 @@ function mountOrb(controller, options = {}) {
|
|
|
710
851
|
track.appendChild(second);
|
|
711
852
|
thoughtBubbleEl.appendChild(track);
|
|
712
853
|
}
|
|
713
|
-
function applyThoughtBubbleTheme() {
|
|
854
|
+
function applyThoughtBubbleTheme(widthPx) {
|
|
714
855
|
const t = bubbleTheme;
|
|
715
856
|
const { fontPx, padH, approxHeight } = (0, core_1.resolveBubblePadding)(t);
|
|
716
|
-
const shapeCss = (0, core_1.bubbleShapeToCss)(t, approxHeight);
|
|
857
|
+
const shapeCss = (0, core_1.bubbleShapeToCss)(t, approxHeight, 1, widthPx);
|
|
717
858
|
thoughtBubbleEl.style.fontSize = `${fontPx}px`;
|
|
718
859
|
thoughtBubbleEl.style.height = `${approxHeight}px`;
|
|
719
860
|
thoughtBubbleEl.style.padding = `0 ${padH}px`;
|
|
720
861
|
thoughtBubbleEl.style.color = t.textColor;
|
|
721
862
|
thoughtBubbleEl.style.background = t.backgroundColor;
|
|
722
863
|
thoughtBubbleEl.style.border = `${t.borderWidth}px solid ${t.borderColor}`;
|
|
723
|
-
|
|
724
|
-
if (!value) {
|
|
725
|
-
thoughtBubbleEl.style.removeProperty(prop);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
thoughtBubbleEl.style.setProperty(prop, value, 'important');
|
|
729
|
-
};
|
|
730
|
-
setImportant('border-radius', shapeCss.borderRadius);
|
|
731
|
-
setImportant('border-top-left-radius', shapeCss.borderTopLeftRadius);
|
|
732
|
-
setImportant('border-top-right-radius', shapeCss.borderTopRightRadius);
|
|
733
|
-
setImportant('border-bottom-left-radius', shapeCss.borderBottomLeftRadius);
|
|
734
|
-
setImportant('border-bottom-right-radius', shapeCss.borderBottomRightRadius);
|
|
735
|
-
setImportant('clip-path', shapeCss.clipPath);
|
|
864
|
+
(0, core_1.applyBubbleShapeCss)(thoughtBubbleEl, shapeCss);
|
|
736
865
|
}
|
|
737
866
|
function positionThoughtBubble() {
|
|
738
867
|
const dep = (0, core_1.resolveOrbDeployment)(effectivePosition());
|
|
@@ -767,6 +896,11 @@ function mountOrb(controller, options = {}) {
|
|
|
767
896
|
thoughtBubbleEl.style.display = 'flex';
|
|
768
897
|
applyThoughtBubbleTheme();
|
|
769
898
|
layoutThoughtContent(prompt);
|
|
899
|
+
if (bubbleTheme.shape === 'cloud') {
|
|
900
|
+
const widthPx = thoughtBubbleEl.offsetWidth;
|
|
901
|
+
if (widthPx > 0)
|
|
902
|
+
applyThoughtBubbleTheme(widthPx);
|
|
903
|
+
}
|
|
770
904
|
positionThoughtBubble();
|
|
771
905
|
}
|
|
772
906
|
// ── Overlays (confirmation + consent) ────────────────────────────────────────
|
|
@@ -897,6 +1031,7 @@ function mountOrb(controller, options = {}) {
|
|
|
897
1031
|
// denied/blocked, in which case we skip voice and go straight to the keyboard.
|
|
898
1032
|
if (controller.voiceAvailable && voiceInputEnabled) {
|
|
899
1033
|
micFallbackActive = false;
|
|
1034
|
+
userTriedVoice = true;
|
|
900
1035
|
void controller.toggleListening();
|
|
901
1036
|
return;
|
|
902
1037
|
}
|
|
@@ -908,6 +1043,7 @@ function mountOrb(controller, options = {}) {
|
|
|
908
1043
|
openTextPanel();
|
|
909
1044
|
return;
|
|
910
1045
|
}
|
|
1046
|
+
userTriedVoice = true;
|
|
911
1047
|
void controller.toggleListening();
|
|
912
1048
|
};
|
|
913
1049
|
sendBtn.onclick = () => {
|
|
@@ -946,11 +1082,15 @@ function mountOrb(controller, options = {}) {
|
|
|
946
1082
|
// Track phase transitions to fire haptics: start/stop listening + response.
|
|
947
1083
|
let lastPhase = null;
|
|
948
1084
|
function render(state) {
|
|
949
|
-
// Mic just became unusable
|
|
950
|
-
// keyboard so the user always has a working channel.
|
|
1085
|
+
// Mic just became unusable MID-ATTEMPT (no Permissions API): fall back to
|
|
1086
|
+
// the keyboard so the user always has a working channel. Guarded by
|
|
1087
|
+
// userTriedVoice: the upfront permission detection also sets voiceBlocked
|
|
1088
|
+
// right at mount (site-level mic denial), and that must NEVER auto-open
|
|
1089
|
+
// the conversation panel on page load — the user did nothing yet; their
|
|
1090
|
+
// first orb tap will route to the keyboard via the voiceAvailable check.
|
|
951
1091
|
if (state.voiceBlocked && !lastVoiceBlocked) {
|
|
952
1092
|
lastVoiceBlocked = true;
|
|
953
|
-
if (enableTextInput && !bubbleOpen && !state.isBusy) {
|
|
1093
|
+
if (userTriedVoice && enableTextInput && !bubbleOpen && !state.isBusy) {
|
|
954
1094
|
openTextPanel();
|
|
955
1095
|
return;
|
|
956
1096
|
}
|
|
@@ -1019,9 +1159,11 @@ function mountOrb(controller, options = {}) {
|
|
|
1019
1159
|
}
|
|
1020
1160
|
const unsubscribe = controller.subscribe(render);
|
|
1021
1161
|
render(controller.getState());
|
|
1022
|
-
// Track the current route
|
|
1023
|
-
//
|
|
1024
|
-
// popstate / visibility
|
|
1162
|
+
// Track the current route AND the app's live locale so the thought bubble
|
|
1163
|
+
// re-resolves its CTA on navigation and on language switches. No universal
|
|
1164
|
+
// navigation/i18n event exists, so poll + listen to popstate / visibility
|
|
1165
|
+
// for snappier updates.
|
|
1166
|
+
let lastBubbleLocale = currentBubbleLocale();
|
|
1025
1167
|
const readRoute = () => {
|
|
1026
1168
|
let next = null;
|
|
1027
1169
|
try {
|
|
@@ -1030,8 +1172,10 @@ function mountOrb(controller, options = {}) {
|
|
|
1030
1172
|
catch {
|
|
1031
1173
|
/* navigation read is best-effort */
|
|
1032
1174
|
}
|
|
1033
|
-
|
|
1175
|
+
const nextLocale = currentBubbleLocale();
|
|
1176
|
+
if (next !== currentRoute || nextLocale !== lastBubbleLocale) {
|
|
1034
1177
|
currentRoute = next;
|
|
1178
|
+
lastBubbleLocale = nextLocale;
|
|
1035
1179
|
syncThoughtBubble(controller.getState());
|
|
1036
1180
|
}
|
|
1037
1181
|
};
|
package/dist/cjs/orb/orbView.js
CHANGED
|
@@ -78,9 +78,15 @@ function buildDoubleBorder(opts) {
|
|
|
78
78
|
root.appendChild(mid);
|
|
79
79
|
return { root, fill };
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Fiodos brand mark (idle, no logo). Breathes very gently on web — a
|
|
83
|
+
* direct port of the landing hero orbs' waveform easing (see
|
|
84
|
+
* `fiodosOrbMarkIdleHalfHeight` in @fiodos/core) — deliberately subtler than
|
|
85
|
+
* and distinct from the volume-reactive waveform shown while listening.
|
|
86
|
+
* Tinted with the same "audio color" (waveformColor) as that waveform.
|
|
87
|
+
*/
|
|
88
|
+
function buildOrbMark(orbSize, markColor) {
|
|
89
|
+
const { bars, color, radius } = (0, core_1.resolveFiodosOrbMarkAnimatedBars)(orbSize, markColor);
|
|
84
90
|
const svg = svgEl('svg', {
|
|
85
91
|
width: orbSize,
|
|
86
92
|
height: orbSize,
|
|
@@ -88,10 +94,31 @@ function buildOrbMark(orbSize) {
|
|
|
88
94
|
'aria-hidden': 'true',
|
|
89
95
|
});
|
|
90
96
|
svg.style.display = 'block';
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
const rects = bars.map((b) => {
|
|
98
|
+
const rect = svgEl('rect', { x: b.x, y: b.y, width: b.width, height: b.height, rx: radius, ry: radius, fill: color });
|
|
99
|
+
svg.appendChild(rect);
|
|
100
|
+
return rect;
|
|
101
|
+
});
|
|
102
|
+
let raf = 0;
|
|
103
|
+
return {
|
|
104
|
+
el: svg,
|
|
105
|
+
start() {
|
|
106
|
+
const t0 = performance.now();
|
|
107
|
+
const loop = (now) => {
|
|
108
|
+
raf = requestAnimationFrame(loop);
|
|
109
|
+
const t = (now - t0) / 1000;
|
|
110
|
+
bars.forEach((b, i) => {
|
|
111
|
+
const half = (0, core_1.fiodosOrbMarkIdleHalfHeight)(t, i, b.baseHalfHeight, b.maxHalfHeight);
|
|
112
|
+
rects[i].setAttribute('y', (b.centerY - half).toFixed(2));
|
|
113
|
+
rects[i].setAttribute('height', (2 * half).toFixed(2));
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
raf = requestAnimationFrame(loop);
|
|
117
|
+
},
|
|
118
|
+
stop() {
|
|
119
|
+
cancelAnimationFrame(raf);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
95
122
|
}
|
|
96
123
|
// ── Waveform (port of VoiceWaveform: 9 bars, bell profile, rAF) ───────────────
|
|
97
124
|
const WAVE_BARS = 9;
|
|
@@ -382,6 +409,7 @@ function createOrbVisual(initial) {
|
|
|
382
409
|
let volume = 0;
|
|
383
410
|
let fillEl = null;
|
|
384
411
|
let waveform = null;
|
|
412
|
+
let markAnim = null;
|
|
385
413
|
function rebuild() {
|
|
386
414
|
const size = appearance.size ?? exports.DEFAULT_ORB_APPEARANCE.size;
|
|
387
415
|
const fill = (0, core_1.resolveOrbBackgroundColor)({
|
|
@@ -393,6 +421,8 @@ function createOrbVisual(initial) {
|
|
|
393
421
|
const interiorColor = (0, core_1.resolveInnerBorderColor)(appearance.innerBorderColor);
|
|
394
422
|
waveform?.stop();
|
|
395
423
|
waveform = null;
|
|
424
|
+
markAnim?.stop();
|
|
425
|
+
markAnim = null;
|
|
396
426
|
wrapper.replaceChildren();
|
|
397
427
|
const { root, fill: fillNode } = buildDoubleBorder({
|
|
398
428
|
fillDiameter: size,
|
|
@@ -414,6 +444,8 @@ function createOrbVisual(initial) {
|
|
|
414
444
|
const waveColor = appearance.waveformColor ?? '#ffffff';
|
|
415
445
|
waveform?.stop();
|
|
416
446
|
waveform = null;
|
|
447
|
+
markAnim?.stop();
|
|
448
|
+
markAnim = null;
|
|
417
449
|
fillEl.replaceChildren();
|
|
418
450
|
// The "speaking" feedback now lives INSIDE the fill (a pulsing logo, or the
|
|
419
451
|
// rings + dot when there's no photo) to mirror the dashboard editor, so the
|
|
@@ -458,7 +490,9 @@ function createOrbVisual(initial) {
|
|
|
458
490
|
return;
|
|
459
491
|
}
|
|
460
492
|
if (state === 'idle') {
|
|
461
|
-
|
|
493
|
+
markAnim = buildOrbMark(size, appearance.waveformColor);
|
|
494
|
+
fillEl.appendChild(markAnim.el);
|
|
495
|
+
markAnim.start();
|
|
462
496
|
}
|
|
463
497
|
}
|
|
464
498
|
rebuild();
|
|
@@ -60,7 +60,9 @@ export interface WatchPublishedConfigOptions {
|
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* Starts watching the published config. Returns an unsubscribe function.
|
|
63
|
-
* Best-effort: a failed fetch
|
|
64
|
-
*
|
|
63
|
+
* Best-effort: a failed fetch is SILENTLY skipped — the orb keeps its
|
|
64
|
+
* last-known-good (or cached) look instead of reverting to the default — and
|
|
65
|
+
* never throws into the host app. `onChange(null)` is reported ONLY when the
|
|
66
|
+
* backend confirms nothing is published.
|
|
65
67
|
*/
|
|
66
68
|
export declare function watchPublishedConfig(controller: AgentController, options: WatchPublishedConfigOptions): () => void;
|
|
@@ -14,6 +14,38 @@ exports.watchPublishedConfig = watchPublishedConfig;
|
|
|
14
14
|
*/
|
|
15
15
|
const core_1 = require("@fiodos/core");
|
|
16
16
|
const orbView_1 = require("./orbView");
|
|
17
|
+
/**
|
|
18
|
+
* Last-known-good published config, persisted so the customized orb survives
|
|
19
|
+
* page reloads during a backend outage and shows instantly on the next visit.
|
|
20
|
+
* Best-effort: storage failures (private mode, quota) are ignored.
|
|
21
|
+
*/
|
|
22
|
+
const PUBLISHED_CONFIG_CACHE_KEY = 'fyodos:published-config:v1';
|
|
23
|
+
function readCachedPublishedConfig() {
|
|
24
|
+
try {
|
|
25
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
26
|
+
return null;
|
|
27
|
+
const raw = window.localStorage.getItem(PUBLISHED_CONFIG_CACHE_KEY);
|
|
28
|
+
return raw ? JSON.parse(raw) : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function writeCachedPublishedConfig(published) {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
37
|
+
return;
|
|
38
|
+
if (published) {
|
|
39
|
+
window.localStorage.setItem(PUBLISHED_CONFIG_CACHE_KEY, JSON.stringify(published));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
window.localStorage.removeItem(PUBLISHED_CONFIG_CACHE_KEY);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* best-effort cache */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
17
49
|
function toResolved(props, published) {
|
|
18
50
|
const enabled = published.orbEnabled !== false;
|
|
19
51
|
const theme = props.theme;
|
|
@@ -48,8 +80,10 @@ function toResolved(props, published) {
|
|
|
48
80
|
}
|
|
49
81
|
/**
|
|
50
82
|
* Starts watching the published config. Returns an unsubscribe function.
|
|
51
|
-
* Best-effort: a failed fetch
|
|
52
|
-
*
|
|
83
|
+
* Best-effort: a failed fetch is SILENTLY skipped — the orb keeps its
|
|
84
|
+
* last-known-good (or cached) look instead of reverting to the default — and
|
|
85
|
+
* never throws into the host app. `onChange(null)` is reported ONLY when the
|
|
86
|
+
* backend confirms nothing is published.
|
|
53
87
|
*/
|
|
54
88
|
function watchPublishedConfig(controller, options) {
|
|
55
89
|
const fetcher = controller.config.backend.fetchPublishedConfig;
|
|
@@ -58,18 +92,33 @@ function watchPublishedConfig(controller, options) {
|
|
|
58
92
|
return () => { };
|
|
59
93
|
}
|
|
60
94
|
let cancelled = false;
|
|
95
|
+
// Hydrate from the local cache so the customized orb shows instantly (and
|
|
96
|
+
// survives page loads while the backend is unreachable). The first network
|
|
97
|
+
// response below overwrites it as the source of truth.
|
|
98
|
+
const cached = readCachedPublishedConfig();
|
|
99
|
+
if (cached) {
|
|
100
|
+
options.onChange(toResolved((0, core_1.mapPublishedConfigToOrbProps)(cached), cached));
|
|
101
|
+
}
|
|
61
102
|
const load = async () => {
|
|
62
103
|
try {
|
|
63
104
|
const res = await fetcher.call(controller.config.backend);
|
|
64
105
|
if (cancelled)
|
|
65
106
|
return;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
if (res.published) {
|
|
108
|
+
options.onChange(toResolved((0, core_1.mapPublishedConfigToOrbProps)(res.published), res.published));
|
|
109
|
+
writeCachedPublishedConfig(res.published);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// A SUCCESSFUL response with nothing published: the project truly has
|
|
113
|
+
// no custom design, so the defaults are correct.
|
|
114
|
+
options.onChange(null);
|
|
115
|
+
writeCachedPublishedConfig(null);
|
|
116
|
+
}
|
|
69
117
|
}
|
|
70
118
|
catch {
|
|
71
|
-
|
|
72
|
-
|
|
119
|
+
// Transient failure (timeout, network blip after hours open, backend
|
|
120
|
+
// redeploy…): keep the current appearance. The next successful poll
|
|
121
|
+
// refreshes it.
|
|
73
122
|
}
|
|
74
123
|
};
|
|
75
124
|
void load();
|
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.15";
|
|
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.15';
|
|
12
12
|
exports.SDK_PACKAGE = '@fiodos/web-core';
|