@fiodos/web-core 0.1.12 → 0.1.14

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.
Files changed (42) hide show
  1. package/dist/cjs/adapters/webCredentialAutofillAdapter.d.ts +16 -0
  2. package/dist/cjs/adapters/webCredentialAutofillAdapter.js +47 -0
  3. package/dist/cjs/api/backendClient.js +14 -0
  4. package/dist/cjs/config/types.d.ts +15 -2
  5. package/dist/cjs/controller/AgentController.d.ts +14 -0
  6. package/dist/cjs/controller/AgentController.js +63 -0
  7. package/dist/cjs/dropin/createFiodosAgent.d.ts +27 -1
  8. package/dist/cjs/dropin/createFiodosAgent.js +32 -1
  9. package/dist/cjs/embed/dynamics.d.ts +61 -0
  10. package/dist/cjs/embed/dynamics.js +173 -0
  11. package/dist/cjs/embed/global.d.ts +45 -0
  12. package/dist/cjs/embed/global.js +48 -0
  13. package/dist/cjs/index.d.ts +3 -0
  14. package/dist/cjs/index.js +8 -1
  15. package/dist/cjs/orb/mountOrb.js +111 -18
  16. package/dist/cjs/orb/orbView.js +40 -7
  17. package/dist/cjs/orb/publishedConfig.d.ts +4 -2
  18. package/dist/cjs/orb/publishedConfig.js +56 -7
  19. package/dist/cjs/version.d.ts +1 -1
  20. package/dist/cjs/version.js +1 -1
  21. package/dist/embed/fiodos-embed.js +66 -0
  22. package/dist/esm/adapters/webCredentialAutofillAdapter.d.ts +16 -0
  23. package/dist/esm/adapters/webCredentialAutofillAdapter.js +44 -0
  24. package/dist/esm/api/backendClient.js +14 -0
  25. package/dist/esm/config/types.d.ts +15 -2
  26. package/dist/esm/controller/AgentController.d.ts +14 -0
  27. package/dist/esm/controller/AgentController.js +63 -0
  28. package/dist/esm/dropin/createFiodosAgent.d.ts +27 -1
  29. package/dist/esm/dropin/createFiodosAgent.js +32 -1
  30. package/dist/esm/embed/dynamics.d.ts +61 -0
  31. package/dist/esm/embed/dynamics.js +168 -0
  32. package/dist/esm/embed/global.d.ts +45 -0
  33. package/dist/esm/embed/global.js +46 -0
  34. package/dist/esm/index.d.ts +3 -0
  35. package/dist/esm/index.js +3 -0
  36. package/dist/esm/orb/mountOrb.js +112 -19
  37. package/dist/esm/orb/orbView.js +41 -8
  38. package/dist/esm/orb/publishedConfig.d.ts +4 -2
  39. package/dist/esm/orb/publishedConfig.js +56 -7
  40. package/dist/esm/version.d.ts +1 -1
  41. package/dist/esm/version.js +1 -1
  42. package/package.json +5 -3
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Embed entrypoint — single-file IIFE bundle for CLOSED third-party hosts
5
+ * where npm/ESM is not available: Dynamics 365 web resources, WinCC Unified
6
+ * Custom Web Controls, Salesforce LWC iframes, plain <script> tags.
7
+ *
8
+ * Built by `npm run build:embed` into dist/embed/fiodos-embed.js and exposed
9
+ * as the `Fiodos` global:
10
+ *
11
+ * <script src="fiodos-embed.js"></script>
12
+ * <script>
13
+ * const agent = Fiodos.createFiodosAgent({
14
+ * manifest, // e.g. from `fiodos from-odata`
15
+ * registries: Fiodos.buildApiActionRegistries(manifest, {
16
+ * baseUrl: 'https://org.crm.dynamics.com/api/data/v9.2',
17
+ * authProviders: { dataverse: () => ({ Authorization: `Bearer ${token}` }) },
18
+ * }),
19
+ * });
20
+ * </script>
21
+ *
22
+ * The bundle is self-contained (core + web-core inlined) and side-effect-free
23
+ * beyond assigning the global — it never auto-mounts; the host decides when.
24
+ */
25
+ const createFiodosAgent_1 = require("../dropin/createFiodosAgent");
26
+ const mountOrb_1 = require("../orb/mountOrb");
27
+ const AgentController_1 = require("../controller/AgentController");
28
+ const screenContextStore_1 = require("../context/screenContextStore");
29
+ const dynamics_1 = require("./dynamics");
30
+ const version_1 = require("../version");
31
+ const core_1 = require("@fiodos/core");
32
+ const api = {
33
+ version: version_1.SDK_VERSION,
34
+ createFiodosAgent: createFiodosAgent_1.createFiodosAgent,
35
+ mountOrb: mountOrb_1.mountOrb,
36
+ AgentController: AgentController_1.AgentController,
37
+ createScreenContextStore: screenContextStore_1.createScreenContextStore,
38
+ // Closed-software mode: turn ApiExecutionSpec manifests into live handlers,
39
+ // and validate hand-written / from-odata manifests before mounting.
40
+ buildApiActionRegistries: core_1.buildApiActionRegistries,
41
+ validateManifest: core_1.validateManifest,
42
+ // Context bridges: tell the orb what the host is showing. Dynamics gets a
43
+ // dedicated mapper (formContext); any other host calls agent.setScreenContext.
44
+ connectDynamicsFormContext: dynamics_1.connectDynamicsFormContext,
45
+ dynamicsFormSnapshot: dynamics_1.dynamicsFormSnapshot,
46
+ normalizeDynamicsId: dynamics_1.normalizeDynamicsId,
47
+ };
48
+ globalThis.Fiodos = api;
@@ -26,6 +26,7 @@ export { createWebNavigationAdapter, BACK_ROUTE, } from './adapters/webNavigatio
26
26
  export type { WebNavigationAdapter, WebNavigationAdapterOptions, } from './adapters/webNavigationAdapter';
27
27
  export { createWebVoiceAdapter } from './adapters/webVoiceAdapter';
28
28
  export type { WebVoiceAdapterOptions } from './adapters/webVoiceAdapter';
29
+ export { createWebCredentialAutofillAdapter } from './adapters/webCredentialAutofillAdapter';
29
30
  export { createFiodosBackendClient, buildManifestPayload } from './api/backendClient';
30
31
  export type { FiodosBackendClientOptions } from './api/backendClient';
31
32
  export { createFiodosTelemetry } from './api/backendTelemetry';
@@ -36,6 +37,8 @@ export { createBridge } from './bridge/createBridge';
36
37
  export type { Bridge } from './bridge/createBridge';
37
38
  export { createScreenContextStore } from './context/screenContextStore';
38
39
  export type { ScreenContextStore, AgentScreenSnapshot } from './context/screenContextStore';
40
+ export { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId, } from './embed/dynamics';
41
+ export type { DynamicsFormContextLike, DynamicsSnapshotOptions, DynamicsContextBridge, ScreenContextSink, } from './embed/dynamics';
39
42
  export { createSpeechSession } from './speech/speechSession';
40
43
  export type { SpeechSession, SpeechSessionOptions, SpeechSessionState, SpeechSessionSnapshot, } from './speech/speechSession';
41
44
  export { DEFAULT_AGENT_TIMINGS, DEFAULT_AGENT_CHAINING } from './config/types';
package/dist/cjs/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * behaviour and security. No framework imports live here.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.classifyPendingReply = exports.decideAction = exports.resolveUiMessages = exports.DEFAULT_AGENT_CHAINING = exports.DEFAULT_AGENT_TIMINGS = exports.createSpeechSession = exports.createScreenContextStore = exports.createBridge = exports.isAgentApiError = exports.AgentApiError = exports.createFiodosTelemetry = exports.buildManifestPayload = exports.createFiodosBackendClient = exports.createWebVoiceAdapter = exports.BACK_ROUTE = exports.createWebNavigationAdapter = exports.createWebStorageAdapter = exports.watchPublishedConfig = exports.DEFAULT_ORB_APPEARANCE = exports.buildKeyboardChip = exports.createOrbVisual = exports.mountOrb = exports.sendOrbSeen = exports.fetchClientManifest = exports.DEFAULT_WEB_API_URL = exports.createFiodosAgent = exports.AgentController = exports.SDK_PACKAGE = exports.SDK_VERSION = void 0;
12
+ exports.classifyPendingReply = exports.decideAction = exports.resolveUiMessages = exports.DEFAULT_AGENT_CHAINING = exports.DEFAULT_AGENT_TIMINGS = exports.createSpeechSession = exports.normalizeDynamicsId = exports.dynamicsFormSnapshot = exports.connectDynamicsFormContext = exports.createScreenContextStore = exports.createBridge = exports.isAgentApiError = exports.AgentApiError = exports.createFiodosTelemetry = exports.buildManifestPayload = exports.createFiodosBackendClient = exports.createWebCredentialAutofillAdapter = exports.createWebVoiceAdapter = exports.BACK_ROUTE = exports.createWebNavigationAdapter = exports.createWebStorageAdapter = exports.watchPublishedConfig = exports.DEFAULT_ORB_APPEARANCE = exports.buildKeyboardChip = exports.createOrbVisual = exports.mountOrb = exports.sendOrbSeen = exports.fetchClientManifest = exports.DEFAULT_WEB_API_URL = exports.createFiodosAgent = exports.AgentController = exports.SDK_PACKAGE = exports.SDK_VERSION = void 0;
13
13
  // ── Version (reported by the orb heartbeat) ───────────────────────────────────
14
14
  var version_1 = require("./version");
15
15
  Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_1.SDK_VERSION; } });
@@ -42,6 +42,8 @@ Object.defineProperty(exports, "createWebNavigationAdapter", { enumerable: true,
42
42
  Object.defineProperty(exports, "BACK_ROUTE", { enumerable: true, get: function () { return webNavigationAdapter_1.BACK_ROUTE; } });
43
43
  var webVoiceAdapter_1 = require("./adapters/webVoiceAdapter");
44
44
  Object.defineProperty(exports, "createWebVoiceAdapter", { enumerable: true, get: function () { return webVoiceAdapter_1.createWebVoiceAdapter; } });
45
+ var webCredentialAutofillAdapter_1 = require("./adapters/webCredentialAutofillAdapter");
46
+ Object.defineProperty(exports, "createWebCredentialAutofillAdapter", { enumerable: true, get: function () { return webCredentialAutofillAdapter_1.createWebCredentialAutofillAdapter; } });
45
47
  // ── Backend + telemetry (same wire contract as every platform) ────────────────
46
48
  var backendClient_1 = require("./api/backendClient");
47
49
  Object.defineProperty(exports, "createFiodosBackendClient", { enumerable: true, get: function () { return backendClient_1.createFiodosBackendClient; } });
@@ -57,6 +59,11 @@ Object.defineProperty(exports, "createBridge", { enumerable: true, get: function
57
59
  // ── Screen context ────────────────────────────────────────────────────────────
58
60
  var screenContextStore_1 = require("./context/screenContextStore");
59
61
  Object.defineProperty(exports, "createScreenContextStore", { enumerable: true, get: function () { return screenContextStore_1.createScreenContextStore; } });
62
+ // ── Closed-host context adapters (embed mode) ─────────────────────────────────
63
+ var dynamics_1 = require("./embed/dynamics");
64
+ Object.defineProperty(exports, "connectDynamicsFormContext", { enumerable: true, get: function () { return dynamics_1.connectDynamicsFormContext; } });
65
+ Object.defineProperty(exports, "dynamicsFormSnapshot", { enumerable: true, get: function () { return dynamics_1.dynamicsFormSnapshot; } });
66
+ Object.defineProperty(exports, "normalizeDynamicsId", { enumerable: true, get: function () { return dynamics_1.normalizeDynamicsId; } });
60
67
  // ── Speech session ──────────────────────────────────────────────────────────────
61
68
  var speechSession_1 = require("./speech/speechSession");
62
69
  Object.defineProperty(exports, "createSpeechSession", { enumerable: true, get: function () { return speechSession_1.createSpeechSession; } });
@@ -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-close{position:absolute;top:4px;right:6px;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;}
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;}
@@ -373,6 +388,11 @@ function mountOrb(controller, options = {}) {
373
388
  btn.addEventListener('lostpointercapture', onPointerUp);
374
389
  // ── Text bubble ─────────────────────────────────────────────────────────────
375
390
  let bubbleOpen = false;
391
+ // Reduced (default) size stays exactly as before. Tapping any EMPTY area of
392
+ // the panel (not a message, not the input row, not a button — so text stays
393
+ // selectable/copyable) grows it to the expanded layout; shrinking back is an
394
+ // explicit tap on `unexpandBtn` next to the close button.
395
+ let bubbleExpanded = false;
376
396
  // When the mic is denied/unavailable, tapping the orb still puts it in the
377
397
  // SAME "active/listening" visual state and reveals the keyboard chip — the
378
398
  // user is never blocked, text is always an open avenue (Change 1).
@@ -409,6 +429,19 @@ function mountOrb(controller, options = {}) {
409
429
  closeBtn.textContent = '×';
410
430
  closeBtn.setAttribute('aria-label', 'Close');
411
431
  closeBtn.addEventListener('click', () => setBubble(false));
432
+ // Collapse button — only shown while expanded, right next to the close
433
+ // button (same minimalist style). Tapping an empty area of the panel is
434
+ // what EXPANDS it; shrinking back is always an explicit, discoverable tap.
435
+ const unexpandBtn = document.createElement('button');
436
+ unexpandBtn.type = 'button';
437
+ unexpandBtn.className = 'fyodos-bubble-unexpand';
438
+ unexpandBtn.innerHTML =
439
+ '<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>';
440
+ unexpandBtn.setAttribute('aria-label', 'Collapse');
441
+ unexpandBtn.addEventListener('click', (e) => {
442
+ e.stopPropagation();
443
+ setBubbleExpanded(false);
444
+ });
412
445
  // Loading row shown at the top while an older page of the session history
413
446
  // streams in (scroll-to-top pagination).
414
447
  const loaderRow = document.createElement('div');
@@ -562,8 +595,71 @@ function mountOrb(controller, options = {}) {
562
595
  micBtn.setAttribute('aria-label', active ? ui.micStopLabel : ui.micLabel);
563
596
  micBtn.title = active ? ui.micStopLabel : ui.micLabel;
564
597
  }
565
- bubble.append(closeBtn, exchangeScroll, busyRow, inputRow);
598
+ bubble.append(closeBtn, unexpandBtn, exchangeScroll, busyRow, inputRow);
566
599
  anchor.appendChild(bubble);
600
+ function setBubbleExpanded(expanded) {
601
+ if (bubbleExpanded === expanded)
602
+ return;
603
+ bubbleExpanded = expanded;
604
+ bubble.classList.toggle('fyodos-bubble--expanded', expanded);
605
+ unexpandBtn.style.display = expanded ? 'flex' : 'none';
606
+ positionBubble();
607
+ }
608
+ // Tap-to-expand: a plain click on the panel grows it. Text selection is
609
+ // never broken: a drag-selection is a move (threshold guard), a double/
610
+ // triple-click word/paragraph selection cancels the pending expand (detail
611
+ // guard + delay), and any active selection blocks it. Controls (input row,
612
+ // buttons) never expand.
613
+ let bubbleTapX = 0;
614
+ let bubbleTapY = 0;
615
+ let pendingExpandTimer = null;
616
+ const TAP_MOVE_THRESHOLD = 8;
617
+ const hasActiveSelection = () => {
618
+ const selection = typeof window !== 'undefined' ? window.getSelection?.() : null;
619
+ return !!selection && !selection.isCollapsed && selection.toString().length > 0;
620
+ };
621
+ const cancelPendingExpand = () => {
622
+ if (pendingExpandTimer != null) {
623
+ window.clearTimeout(pendingExpandTimer);
624
+ pendingExpandTimer = null;
625
+ }
626
+ };
627
+ bubble.addEventListener('pointerdown', (e) => {
628
+ bubbleTapX = e.clientX;
629
+ bubbleTapY = e.clientY;
630
+ });
631
+ bubble.addEventListener('click', (e) => {
632
+ if (bubbleExpanded)
633
+ return;
634
+ const target = e.target;
635
+ if (target?.closest('.fyodos-bubble-row, .fyodos-bubble-close, .fyodos-bubble-unexpand, button, input, a')) {
636
+ return;
637
+ }
638
+ const moved = Math.hypot(e.clientX - bubbleTapX, e.clientY - bubbleTapY);
639
+ if (moved > TAP_MOVE_THRESHOLD)
640
+ return;
641
+ // Double/triple click is a word/paragraph selection gesture, never expand
642
+ // (it also cancels the pending expand from its own first click).
643
+ if (e.detail > 1) {
644
+ cancelPendingExpand();
645
+ return;
646
+ }
647
+ cancelPendingExpand();
648
+ const overText = !!target?.closest('.fyodos-bubble-user, .fyodos-bubble-reply');
649
+ if (!overText && !hasActiveSelection()) {
650
+ // Genuinely empty area, nothing selected — expand immediately.
651
+ setBubbleExpanded(true);
652
+ return;
653
+ }
654
+ // Over message text, or a selection may still be collapsing: expand after
655
+ // a short delay, re-checking the selection right before — a double-click's
656
+ // second click or a real selection cancels it.
657
+ pendingExpandTimer = window.setTimeout(() => {
658
+ pendingExpandTimer = null;
659
+ if (!hasActiveSelection())
660
+ setBubbleExpanded(true);
661
+ }, 280);
662
+ });
567
663
  // Initial placement (after the bubble exists, so applyDeployment can lay it out).
568
664
  reposition();
569
665
  function applyBubbleTheme() {
@@ -574,6 +670,7 @@ function mountOrb(controller, options = {}) {
574
670
  if (modalTheme?.borderRadius != null)
575
671
  bubble.style.borderRadius = `${modalTheme.borderRadius}px`;
576
672
  closeBtn.style.color = t.bodyColor;
673
+ unexpandBtn.style.color = t.bodyColor;
577
674
  exchangeList.querySelectorAll('.fyodos-bubble-user').forEach((node) => {
578
675
  node.style.color = t.bodyColor;
579
676
  });
@@ -608,6 +705,9 @@ function mountOrb(controller, options = {}) {
608
705
  controller.resetBubbleWindow();
609
706
  // Closing the panel returns the orb to idle (also clears mic fallback).
610
707
  micFallbackActive = false;
708
+ // Always re-open at the reduced size.
709
+ cancelPendingExpand();
710
+ setBubbleExpanded(false);
611
711
  }
612
712
  syncChip(controller.getState());
613
713
  }
@@ -710,29 +810,17 @@ function mountOrb(controller, options = {}) {
710
810
  track.appendChild(second);
711
811
  thoughtBubbleEl.appendChild(track);
712
812
  }
713
- function applyThoughtBubbleTheme() {
813
+ function applyThoughtBubbleTheme(widthPx) {
714
814
  const t = bubbleTheme;
715
815
  const { fontPx, padH, approxHeight } = (0, core_1.resolveBubblePadding)(t);
716
- const shapeCss = (0, core_1.bubbleShapeToCss)(t, approxHeight);
816
+ const shapeCss = (0, core_1.bubbleShapeToCss)(t, approxHeight, 1, widthPx);
717
817
  thoughtBubbleEl.style.fontSize = `${fontPx}px`;
718
818
  thoughtBubbleEl.style.height = `${approxHeight}px`;
719
819
  thoughtBubbleEl.style.padding = `0 ${padH}px`;
720
820
  thoughtBubbleEl.style.color = t.textColor;
721
821
  thoughtBubbleEl.style.background = t.backgroundColor;
722
822
  thoughtBubbleEl.style.border = `${t.borderWidth}px solid ${t.borderColor}`;
723
- const setImportant = (prop, value) => {
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);
823
+ (0, core_1.applyBubbleShapeCss)(thoughtBubbleEl, shapeCss);
736
824
  }
737
825
  function positionThoughtBubble() {
738
826
  const dep = (0, core_1.resolveOrbDeployment)(effectivePosition());
@@ -767,6 +855,11 @@ function mountOrb(controller, options = {}) {
767
855
  thoughtBubbleEl.style.display = 'flex';
768
856
  applyThoughtBubbleTheme();
769
857
  layoutThoughtContent(prompt);
858
+ if (bubbleTheme.shape === 'cloud') {
859
+ const widthPx = thoughtBubbleEl.offsetWidth;
860
+ if (widthPx > 0)
861
+ applyThoughtBubbleTheme(widthPx);
862
+ }
770
863
  positionThoughtBubble();
771
864
  }
772
865
  // ── Overlays (confirmation + consent) ────────────────────────────────────────
@@ -78,9 +78,14 @@ function buildDoubleBorder(opts) {
78
78
  root.appendChild(mid);
79
79
  return { root, fill };
80
80
  }
81
- // ── Brand mark (port of FiodosOrbMark) ───────────────────────────────────────
81
+ /**
82
+ * Neutral 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
+ */
82
87
  function buildOrbMark(orbSize) {
83
- const { bars, color, radius } = (0, core_1.resolveFiodosOrbMarkBars)(orbSize);
88
+ const { bars, color, radius } = (0, core_1.resolveFiodosOrbMarkAnimatedBars)(orbSize);
84
89
  const svg = svgEl('svg', {
85
90
  width: orbSize,
86
91
  height: orbSize,
@@ -88,10 +93,31 @@ function buildOrbMark(orbSize) {
88
93
  'aria-hidden': 'true',
89
94
  });
90
95
  svg.style.display = 'block';
91
- for (const b of bars) {
92
- svg.appendChild(svgEl('rect', { x: b.x, y: b.y, width: b.width, height: b.height, rx: radius, ry: radius, fill: color }));
93
- }
94
- return svg;
96
+ const rects = bars.map((b) => {
97
+ const rect = svgEl('rect', { x: b.x, y: b.y, width: b.width, height: b.height, rx: radius, ry: radius, fill: color });
98
+ svg.appendChild(rect);
99
+ return rect;
100
+ });
101
+ let raf = 0;
102
+ return {
103
+ el: svg,
104
+ start() {
105
+ const t0 = performance.now();
106
+ const loop = (now) => {
107
+ raf = requestAnimationFrame(loop);
108
+ const t = (now - t0) / 1000;
109
+ bars.forEach((b, i) => {
110
+ const half = (0, core_1.fiodosOrbMarkIdleHalfHeight)(t, i, b.baseHalfHeight, b.maxHalfHeight);
111
+ rects[i].setAttribute('y', (b.centerY - half).toFixed(2));
112
+ rects[i].setAttribute('height', (2 * half).toFixed(2));
113
+ });
114
+ };
115
+ raf = requestAnimationFrame(loop);
116
+ },
117
+ stop() {
118
+ cancelAnimationFrame(raf);
119
+ },
120
+ };
95
121
  }
96
122
  // ── Waveform (port of VoiceWaveform: 9 bars, bell profile, rAF) ───────────────
97
123
  const WAVE_BARS = 9;
@@ -382,6 +408,7 @@ function createOrbVisual(initial) {
382
408
  let volume = 0;
383
409
  let fillEl = null;
384
410
  let waveform = null;
411
+ let markAnim = null;
385
412
  function rebuild() {
386
413
  const size = appearance.size ?? exports.DEFAULT_ORB_APPEARANCE.size;
387
414
  const fill = (0, core_1.resolveOrbBackgroundColor)({
@@ -393,6 +420,8 @@ function createOrbVisual(initial) {
393
420
  const interiorColor = (0, core_1.resolveInnerBorderColor)(appearance.innerBorderColor);
394
421
  waveform?.stop();
395
422
  waveform = null;
423
+ markAnim?.stop();
424
+ markAnim = null;
396
425
  wrapper.replaceChildren();
397
426
  const { root, fill: fillNode } = buildDoubleBorder({
398
427
  fillDiameter: size,
@@ -414,6 +443,8 @@ function createOrbVisual(initial) {
414
443
  const waveColor = appearance.waveformColor ?? '#ffffff';
415
444
  waveform?.stop();
416
445
  waveform = null;
446
+ markAnim?.stop();
447
+ markAnim = null;
417
448
  fillEl.replaceChildren();
418
449
  // The "speaking" feedback now lives INSIDE the fill (a pulsing logo, or the
419
450
  // rings + dot when there's no photo) to mirror the dashboard editor, so the
@@ -458,7 +489,9 @@ function createOrbVisual(initial) {
458
489
  return;
459
490
  }
460
491
  if (state === 'idle') {
461
- fillEl.appendChild(buildOrbMark(size));
492
+ markAnim = buildOrbMark(size);
493
+ fillEl.appendChild(markAnim.el);
494
+ markAnim.start();
462
495
  }
463
496
  }
464
497
  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 reports `null` (the orb keeps its current/default
64
- * look) and never throws into the host app.
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 reports `null` (the orb keeps its current/default
52
- * look) and never throws into the host app.
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
- options.onChange(res.published
67
- ? toResolved((0, core_1.mapPublishedConfigToOrbProps)(res.published), res.published)
68
- : null);
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
- if (!cancelled)
72
- options.onChange(null);
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();
@@ -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.12";
8
+ export declare const SDK_VERSION = "0.1.14";
9
9
  export declare const SDK_PACKAGE = "@fiodos/web-core";
@@ -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.12';
11
+ exports.SDK_VERSION = '0.1.14';
12
12
  exports.SDK_PACKAGE = '@fiodos/web-core';