@fiodos/web-core 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/api/backendClient.js +20 -0
- package/dist/cjs/config/types.d.ts +18 -0
- package/dist/cjs/config/types.js +6 -1
- package/dist/cjs/controller/AgentController.d.ts +84 -1
- package/dist/cjs/controller/AgentController.js +515 -75
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/orb/mountOrb.d.ts +6 -0
- package/dist/cjs/orb/mountOrb.js +307 -53
- package/dist/cjs/orb/orbView.d.ts +2 -0
- package/dist/cjs/orb/publishedConfig.d.ts +18 -0
- package/dist/cjs/orb/publishedConfig.js +6 -0
- package/dist/cjs/speech/bubbleDictation.d.ts +11 -0
- package/dist/cjs/speech/bubbleDictation.js +88 -0
- package/dist/cjs/ui/messages.d.ts +15 -0
- package/dist/cjs/ui/messages.js +12 -0
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/api/backendClient.js +20 -0
- package/dist/esm/config/types.d.ts +18 -0
- package/dist/esm/config/types.js +5 -0
- package/dist/esm/controller/AgentController.d.ts +84 -1
- package/dist/esm/controller/AgentController.js +517 -77
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/orb/mountOrb.d.ts +6 -0
- package/dist/esm/orb/mountOrb.js +308 -54
- package/dist/esm/orb/orbView.d.ts +2 -0
- package/dist/esm/orb/publishedConfig.d.ts +18 -0
- package/dist/esm/orb/publishedConfig.js +6 -0
- package/dist/esm/speech/bubbleDictation.d.ts +11 -0
- package/dist/esm/speech/bubbleDictation.js +84 -0
- package/dist/esm/ui/messages.d.ts +15 -0
- package/dist/esm/ui/messages.js +12 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export { SDK_VERSION, SDK_PACKAGE } from './version.js';
|
|
11
11
|
export { AgentController } from './controller/AgentController.js';
|
|
12
|
-
export type { AgentState, AgentExchange } from './controller/AgentController.js';
|
|
12
|
+
export type { AgentState, AgentExchange, AgentActivity } from './controller/AgentController.js';
|
|
13
13
|
export { createFiodosAgent } from './dropin/createFiodosAgent.js';
|
|
14
14
|
export type { CreateFiodosAgentOptions, FiodosAgentHandle } from './dropin/createFiodosAgent.js';
|
|
15
15
|
export { DEFAULT_WEB_API_URL, fetchClientManifest, sendOrbSeen, } from './api/clientBootstrap.js';
|
|
@@ -38,8 +38,8 @@ export { createScreenContextStore } from './context/screenContextStore.js';
|
|
|
38
38
|
export type { ScreenContextStore, AgentScreenSnapshot } from './context/screenContextStore.js';
|
|
39
39
|
export { createSpeechSession } from './speech/speechSession.js';
|
|
40
40
|
export type { SpeechSession, SpeechSessionOptions, SpeechSessionState, SpeechSessionSnapshot, } from './speech/speechSession.js';
|
|
41
|
-
export { DEFAULT_AGENT_TIMINGS } from './config/types.js';
|
|
42
|
-
export type { FiodosWebAgentConfig, AgentPhase, AgentTimings, TurnGateVerdict, } from './config/types.js';
|
|
41
|
+
export { DEFAULT_AGENT_TIMINGS, DEFAULT_AGENT_CHAINING } from './config/types.js';
|
|
42
|
+
export type { FiodosWebAgentConfig, AgentPhase, AgentTimings, AgentChaining, TurnGateVerdict, } from './config/types.js';
|
|
43
43
|
export { resolveUiMessages } from './ui/messages.js';
|
|
44
44
|
export type { WebUiMessages, ResolveUiMessagesOptions } from './ui/messages.js';
|
|
45
45
|
export { decideAction, classifyPendingReply } from './core/turnEngine.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export { createScreenContextStore } from './context/screenContextStore.js';
|
|
|
34
34
|
// ── Speech session ──────────────────────────────────────────────────────────────
|
|
35
35
|
export { createSpeechSession } from './speech/speechSession.js';
|
|
36
36
|
// ── Config / i18n ─────────────────────────────────────────────────────────────
|
|
37
|
-
export { DEFAULT_AGENT_TIMINGS } from './config/types.js';
|
|
37
|
+
export { DEFAULT_AGENT_TIMINGS, DEFAULT_AGENT_CHAINING } from './config/types.js';
|
|
38
38
|
export { resolveUiMessages } from './ui/messages.js';
|
|
39
39
|
// ── Decision engine (shared, framework-free; advanced use / testing) ──────────
|
|
40
40
|
export { decideAction, classifyPendingReply } from './core/turnEngine.js';
|
|
@@ -17,6 +17,12 @@ export interface MountOrbOptions {
|
|
|
17
17
|
appearancePollMs?: number;
|
|
18
18
|
/** Honour the dashboard's published screen position. Default: true. */
|
|
19
19
|
usePublishedPosition?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Internal-profile orbs only render on development hosts (localhost, LAN
|
|
22
|
+
* IPs, *.local). Set true to assert this page IS internal software (e.g. a
|
|
23
|
+
* hosted admin panel) so an internal orb may render here. Default: false.
|
|
24
|
+
*/
|
|
25
|
+
allowInternalOrb?: boolean;
|
|
20
26
|
}
|
|
21
27
|
export interface MountedOrb {
|
|
22
28
|
unmount(): void;
|
package/dist/esm/orb/mountOrb.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
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, matchRouteIntent, resolveBubblePrompt, resolveBubbleTheme, resolveBubblePadding,
|
|
19
|
+
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, matchRouteIntent, resolveBubblePrompt, bubblePromptToAffirmative, resolveBubbleTheme, resolveBubblePadding, bubbleShapeToCss, resolveOrbAnchorPx, resolveOrbDeployment, snapOrbScreenPositionToSide, viewportPixelsToOrbScreenPosition, fiodosInputInkColor, fiodosInputMutedInkColor, shouldHideInternalOrb, } from '@fiodos/core';
|
|
20
20
|
import { buildKeyboardChip, createOrbVisual, DEFAULT_ORB_APPEARANCE, } from './orbView.js';
|
|
21
21
|
import { watchPublishedConfig } from './publishedConfig.js';
|
|
22
|
+
import { createBubbleDictation, scrollTextInputToEnd } from '../speech/bubbleDictation.js';
|
|
22
23
|
const STYLE_ID = 'fyodos-orb-styles';
|
|
23
24
|
const CSS = `
|
|
24
25
|
.fyodos-orb-root{position:fixed;display:inline-flex;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;touch-action:none;}
|
|
@@ -27,20 +28,32 @@ const CSS = `
|
|
|
27
28
|
.fyodos-orb-btn:focus-visible{outline:2px solid #8ab6ff;outline-offset:4px;border-radius:50%;}
|
|
28
29
|
.fyodos-orb-anchor{position:relative;display:inline-flex;}
|
|
29
30
|
.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;}
|
|
30
|
-
.fyodos-bubble-close{
|
|
31
|
+
.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;}
|
|
31
32
|
.fyodos-bubble-user-row{display:flex;flex-direction:row;align-items:flex-start;gap:6px;}
|
|
32
|
-
.fyodos-bubble-scroll{overflow-y:auto;padding:12px 14px 4px;display:flex;flex-direction:column;gap:6px;}
|
|
33
|
+
.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;}
|
|
34
|
+
.fyodos-bubble-scroll::-webkit-scrollbar{display:none;width:0;height:0;}
|
|
35
|
+
.fyodos-bubble-loader{display:flex;justify-content:center;align-items:center;padding:2px 0 6px;flex-shrink:0;}
|
|
36
|
+
.fyodos-bubble-loader span{width:14px;height:14px;border-radius:50%;border:2px solid rgba(148,163,184,0.35);border-top-color:rgba(148,163,184,0.9);animation:fy-spin .7s linear infinite;}
|
|
37
|
+
.fyodos-bubble-exchanges{display:flex;flex-direction:column;gap:12px;}
|
|
38
|
+
.fyodos-bubble-exchange{display:flex;flex-direction:column;gap:6px;}
|
|
39
|
+
.fyodos-bubble-exchange:first-child .fyodos-bubble-user{padding-right:22px;}
|
|
33
40
|
.fyodos-bubble-user{flex:1;min-width:0;font-size:13px;line-height:18px;opacity:.75;}
|
|
34
41
|
.fyodos-bubble-reply{font-size:15px;line-height:21px;white-space:pre-wrap;}
|
|
35
42
|
.fyodos-bubble-busy{display:flex;gap:5px;padding:8px 14px;align-items:center;}
|
|
36
43
|
.fyodos-bubble-busy span{width:6px;height:6px;border-radius:50%;opacity:.5;animation:fyodos-blink 1s infinite;}
|
|
37
44
|
.fyodos-bubble-busy span:nth-child(2){animation-delay:.18s;}
|
|
38
45
|
.fyodos-bubble-busy span:nth-child(3){animation-delay:.36s;}
|
|
46
|
+
.fyodos-busy-step{margin-right:6px;font-size:12.5px;line-height:18px;font-style:italic;animation:fyodos-fade-in .28s ease-out;}
|
|
39
47
|
.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
|
-
.fyodos-bubble-row input{flex:1;
|
|
48
|
+
.fyodos-bubble-row input{flex:1;min-width:0;border:none;outline:none;border-radius:10px;padding:9px 11px;font-size:15px;}
|
|
49
|
+
.fyodos-bubble-row input::placeholder{color:var(--fyodos-input-placeholder,#94a3b8);opacity:1;}
|
|
50
|
+
.fyodos-bubble-mic{appearance:none;flex-shrink:0;display:flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:32px;padding:0;border:1px solid rgba(148,163,184,0.4);border-radius:999px;background:rgba(255,255,255,0.06);color:inherit;cursor:pointer;}
|
|
41
51
|
.fyodos-bubble-row button{appearance:none;border:none;flex-shrink:0;display:flex;align-items:center;justify-content:center;width:32px;height:32px;min-width:32px;padding:0;font-size:17px;font-weight:700;line-height:1;color:#000;cursor:pointer;}
|
|
42
|
-
.fyodos-thought{position:absolute;appearance:none;display:
|
|
52
|
+
.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
53
|
@keyframes fyodos-thought-in{from{opacity:0;transform:translateY(4px) scale(0.96)}to{opacity:1;transform:none}}
|
|
54
|
+
@keyframes fyodos-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
|
|
55
|
+
.fyodos-thought-track{display:flex;flex-shrink:0;width:max-content;will-change:transform;}
|
|
56
|
+
.fyodos-thought-track>span{flex-shrink:0;padding-right:48px;white-space:nowrap;}
|
|
44
57
|
@keyframes fyodos-fade-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
45
58
|
@keyframes fyodos-blink{0%,100%{opacity:.25}50%{opacity:.9}}
|
|
46
59
|
@keyframes fy-spin{to{transform:rotate(360deg)}}
|
|
@@ -124,7 +137,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
124
137
|
}
|
|
125
138
|
// Subtle haptic feedback on every relevant orb interaction (silent no-op on
|
|
126
139
|
// browsers without the Vibration API — desktop, iOS Safari).
|
|
127
|
-
const haptics = createWebOrbHaptics();
|
|
140
|
+
const haptics = createWebOrbHaptics(() => effectiveAppearance().hapticIntensity);
|
|
128
141
|
// ── Effective appearance: defaults ← published ← explicit option overrides ──
|
|
129
142
|
let publishedAppearance = null;
|
|
130
143
|
let chipTheme = null;
|
|
@@ -134,6 +147,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
134
147
|
// Dashboard VOICE switch. Default true (voice on). When false the orb never
|
|
135
148
|
// listens/speaks: a tap opens the keyboard directly and text turns are silent.
|
|
136
149
|
let voiceInputEnabled = true;
|
|
150
|
+
// Dashboard "thinking steps" switch. When true the busy row walks through
|
|
151
|
+
// what the agent is doing ("Analyzing your request…") instead of only dots.
|
|
152
|
+
let thinkingStepsEnabled = false;
|
|
137
153
|
let currentRoute = null;
|
|
138
154
|
let screenPosition = options.corner
|
|
139
155
|
? cornerToPosition(options.corner)
|
|
@@ -167,19 +183,32 @@ export function mountOrb(controller, options = {}) {
|
|
|
167
183
|
let chipEl = null;
|
|
168
184
|
// Thought bubble: a short, screen-aware CTA beside the orb. Created once and
|
|
169
185
|
// shown/hidden by syncThoughtBubble; tapping it opens the keyboard.
|
|
170
|
-
const thoughtBubbleEl = document.createElement('
|
|
171
|
-
thoughtBubbleEl.type = 'button';
|
|
186
|
+
const thoughtBubbleEl = document.createElement('div');
|
|
172
187
|
thoughtBubbleEl.className = 'fyodos-thought';
|
|
188
|
+
thoughtBubbleEl.setAttribute('role', 'button');
|
|
189
|
+
thoughtBubbleEl.tabIndex = 0;
|
|
173
190
|
thoughtBubbleEl.style.display = 'none';
|
|
174
191
|
thoughtBubbleEl.addEventListener('click', () => {
|
|
175
192
|
haptics.trigger('orbTap');
|
|
176
193
|
controller.primeAudio();
|
|
177
194
|
if (enableTextInput) {
|
|
178
|
-
|
|
195
|
+
const prompt = resolveCurrentBubblePrompt();
|
|
196
|
+
openTextPanel(prompt
|
|
197
|
+
? bubblePromptToAffirmative(prompt, {
|
|
198
|
+
locale: controller.config.locale,
|
|
199
|
+
messages: controller.messages,
|
|
200
|
+
})
|
|
201
|
+
: undefined);
|
|
179
202
|
return;
|
|
180
203
|
}
|
|
181
204
|
void controller.toggleListening();
|
|
182
205
|
});
|
|
206
|
+
thoughtBubbleEl.addEventListener('keydown', (event) => {
|
|
207
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
thoughtBubbleEl.click();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
183
212
|
anchor.appendChild(thoughtBubbleEl);
|
|
184
213
|
container.appendChild(root);
|
|
185
214
|
// ── Position (dashboard position; user drag is SESSION-only, never persisted) ─
|
|
@@ -369,35 +398,168 @@ export function mountOrb(controller, options = {}) {
|
|
|
369
398
|
}
|
|
370
399
|
const exchangeScroll = document.createElement('div');
|
|
371
400
|
exchangeScroll.className = 'fyodos-bubble-scroll';
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const userLine = document.createElement('div');
|
|
375
|
-
userLine.className = 'fyodos-bubble-user';
|
|
401
|
+
// Close button overlaid at the bubble corner — in line with the first message,
|
|
402
|
+
// exactly like the dashboard preview (no extra header row/space).
|
|
376
403
|
const closeBtn = document.createElement('button');
|
|
377
404
|
closeBtn.type = 'button';
|
|
378
405
|
closeBtn.className = 'fyodos-bubble-close';
|
|
379
406
|
closeBtn.textContent = '×';
|
|
380
407
|
closeBtn.setAttribute('aria-label', 'Close');
|
|
381
408
|
closeBtn.addEventListener('click', () => setBubble(false));
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
409
|
+
// Loading row shown at the top while an older page of the session history
|
|
410
|
+
// streams in (scroll-to-top pagination).
|
|
411
|
+
const loaderRow = document.createElement('div');
|
|
412
|
+
loaderRow.className = 'fyodos-bubble-loader';
|
|
413
|
+
loaderRow.append(document.createElement('span'));
|
|
414
|
+
loaderRow.style.display = 'none';
|
|
415
|
+
const exchangeList = document.createElement('div');
|
|
416
|
+
exchangeList.className = 'fyodos-bubble-exchanges';
|
|
417
|
+
exchangeScroll.append(loaderRow, exchangeList);
|
|
418
|
+
// Signature of the rendered list so state emits that do not change the
|
|
419
|
+
// transcript (volume, phase…) never re-render it nor fight the user's scroll.
|
|
420
|
+
let renderedSignature = '';
|
|
421
|
+
function renderBubbleExchanges(exchanges) {
|
|
422
|
+
const signature = exchanges.map((ex) => `${ex.userText}\u0000${ex.replyText}`).join('\u0001');
|
|
423
|
+
if (signature === renderedSignature)
|
|
424
|
+
return;
|
|
425
|
+
// Prepend (older page loaded): the previous tail is intact at the end.
|
|
426
|
+
// Keep the user's reading position anchored instead of jumping to bottom.
|
|
427
|
+
const prepended = renderedSignature.length > 0 &&
|
|
428
|
+
signature.length > renderedSignature.length &&
|
|
429
|
+
signature.endsWith(renderedSignature);
|
|
430
|
+
const fromBottom = exchangeScroll.scrollHeight - exchangeScroll.scrollTop;
|
|
431
|
+
renderedSignature = signature;
|
|
432
|
+
exchangeList.replaceChildren();
|
|
433
|
+
for (const ex of exchanges) {
|
|
434
|
+
const block = document.createElement('div');
|
|
435
|
+
block.className = 'fyodos-bubble-exchange';
|
|
436
|
+
const user = document.createElement('div');
|
|
437
|
+
user.className = 'fyodos-bubble-user';
|
|
438
|
+
user.textContent = ex.userText;
|
|
439
|
+
block.append(user);
|
|
440
|
+
if (ex.replyText) {
|
|
441
|
+
const reply = document.createElement('div');
|
|
442
|
+
reply.className = 'fyodos-bubble-reply';
|
|
443
|
+
reply.textContent = ex.replyText;
|
|
444
|
+
block.append(reply);
|
|
445
|
+
}
|
|
446
|
+
exchangeList.append(block);
|
|
447
|
+
}
|
|
448
|
+
exchangeScroll.scrollTop = prepended
|
|
449
|
+
? exchangeScroll.scrollHeight - fromBottom
|
|
450
|
+
: exchangeScroll.scrollHeight;
|
|
451
|
+
}
|
|
452
|
+
// Scroll-to-top pagination: reaching the top with older messages available
|
|
453
|
+
// pages the next block in (the controller shows the loader while it mounts).
|
|
454
|
+
exchangeScroll.addEventListener('scroll', () => {
|
|
455
|
+
if (!bubbleOpen || exchangeScroll.scrollTop > 12)
|
|
456
|
+
return;
|
|
457
|
+
const state = controller.getState();
|
|
458
|
+
if (state.hasOlderExchanges && !state.loadingOlderExchanges) {
|
|
459
|
+
controller.loadOlderExchanges();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
386
462
|
const busyRow = document.createElement('div');
|
|
387
463
|
busyRow.className = 'fyodos-bubble-busy';
|
|
464
|
+
// REAL live activity FIRST in the row — flush left, aligned with the
|
|
465
|
+
// messages above (an <em> so the dot styling, which targets the spans,
|
|
466
|
+
// never applies to it). Hidden unless the dashboard advanced option is on.
|
|
467
|
+
const busyStep = document.createElement('em');
|
|
468
|
+
busyStep.className = 'fyodos-busy-step';
|
|
469
|
+
busyStep.style.display = 'none';
|
|
470
|
+
busyRow.appendChild(busyStep);
|
|
388
471
|
busyRow.append(document.createElement('span'), document.createElement('span'), document.createElement('span'));
|
|
472
|
+
// ── REAL activity line (dashboard advanced option) ─────────────────────────
|
|
473
|
+
// Replaces the old canned walk-through: the controller reports what the
|
|
474
|
+
// agent is ACTUALLY doing (thinking / navigating / running an action /
|
|
475
|
+
// waiting for confirmation) and the line updates only on real changes.
|
|
476
|
+
let lastActivityText = null;
|
|
477
|
+
function activityText(state) {
|
|
478
|
+
if (!thinkingStepsEnabled || !state.liveActivity)
|
|
479
|
+
return null;
|
|
480
|
+
const { kind, label } = state.liveActivity;
|
|
481
|
+
switch (kind) {
|
|
482
|
+
case 'navigating':
|
|
483
|
+
return ui.activityNavigating.replace('{label}', label ?? '');
|
|
484
|
+
case 'executing':
|
|
485
|
+
return ui.activityExecuting.replace('{label}', label ?? '');
|
|
486
|
+
case 'waiting_confirmation':
|
|
487
|
+
return ui.activityWaitingConfirmation;
|
|
488
|
+
default:
|
|
489
|
+
return ui.activityThinking;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function syncActivityStep(show, state) {
|
|
493
|
+
const text = show ? activityText(state) : null;
|
|
494
|
+
if (text === lastActivityText)
|
|
495
|
+
return;
|
|
496
|
+
lastActivityText = text;
|
|
497
|
+
if (!text) {
|
|
498
|
+
busyStep.textContent = '';
|
|
499
|
+
busyStep.style.display = 'none';
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
busyStep.style.display = '';
|
|
503
|
+
busyStep.textContent = text;
|
|
504
|
+
// Restart the fade-in for each new real step.
|
|
505
|
+
busyStep.style.animation = 'none';
|
|
506
|
+
void busyStep.offsetWidth;
|
|
507
|
+
busyStep.style.animation = '';
|
|
508
|
+
}
|
|
389
509
|
const inputRow = document.createElement('div');
|
|
390
510
|
inputRow.className = 'fyodos-bubble-row';
|
|
391
511
|
const textInput = document.createElement('input');
|
|
392
512
|
textInput.type = 'text';
|
|
393
513
|
textInput.placeholder = ui.textPlaceholder;
|
|
514
|
+
const micBtn = document.createElement('button');
|
|
515
|
+
micBtn.type = 'button';
|
|
516
|
+
micBtn.className = 'fyodos-bubble-mic';
|
|
517
|
+
micBtn.innerHTML =
|
|
518
|
+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="3" width="6" height="11" rx="3"></rect><path d="M5 11a7 7 0 0014 0 M12 18v3"></path></svg>';
|
|
519
|
+
micBtn.style.display = 'none';
|
|
394
520
|
const sendBtn = document.createElement('button');
|
|
395
521
|
// Unified send affordance: an up-arrow glyph on the accent square (parity
|
|
396
522
|
// with the dashboard preview and every native SDK), not a text label.
|
|
397
523
|
sendBtn.textContent = '↑';
|
|
398
524
|
sendBtn.setAttribute('aria-label', ui.sendLabel);
|
|
399
|
-
inputRow.append(textInput, sendBtn);
|
|
400
|
-
|
|
525
|
+
inputRow.append(textInput, micBtn, sendBtn);
|
|
526
|
+
const bubbleDictation = createBubbleDictation(controller.config.locale);
|
|
527
|
+
if (bubbleDictation.isSupported()) {
|
|
528
|
+
micBtn.style.display = 'flex';
|
|
529
|
+
micBtn.setAttribute('aria-label', ui.micLabel);
|
|
530
|
+
micBtn.title = ui.micLabel;
|
|
531
|
+
micBtn.addEventListener('click', () => {
|
|
532
|
+
if (controller.getState().isBusy)
|
|
533
|
+
return;
|
|
534
|
+
bubbleDictation.toggle(() => textInput.value, (next) => {
|
|
535
|
+
textInput.value = next;
|
|
536
|
+
scrollTextInputToEnd(textInput);
|
|
537
|
+
});
|
|
538
|
+
syncMicButton();
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function syncMicButton() {
|
|
542
|
+
const accent = modalTheme?.primaryButtonColor ?? effectiveAppearance().accentColor;
|
|
543
|
+
const sendRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
|
|
544
|
+
const bodyColor = { ...DEFAULT_MODAL_THEME, ...(modalTheme ?? {}) }.bodyColor;
|
|
545
|
+
const active = bubbleDictation.isListening();
|
|
546
|
+
micBtn.classList.toggle('is-listening', active);
|
|
547
|
+
if (active) {
|
|
548
|
+
micBtn.style.background = accent;
|
|
549
|
+
micBtn.style.border = 'none';
|
|
550
|
+
micBtn.style.color = '#000';
|
|
551
|
+
micBtn.style.borderRadius = sendRadius;
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
micBtn.style.background = 'rgba(255,255,255,0.06)';
|
|
555
|
+
micBtn.style.border = '1px solid rgba(148,163,184,0.4)';
|
|
556
|
+
micBtn.style.color = bodyColor;
|
|
557
|
+
micBtn.style.borderRadius = '999px';
|
|
558
|
+
}
|
|
559
|
+
micBtn.setAttribute('aria-label', active ? ui.micStopLabel : ui.micLabel);
|
|
560
|
+
micBtn.title = active ? ui.micStopLabel : ui.micLabel;
|
|
561
|
+
}
|
|
562
|
+
bubble.append(closeBtn, exchangeScroll, busyRow, inputRow);
|
|
401
563
|
anchor.appendChild(bubble);
|
|
402
564
|
// Initial placement (after the bubble exists, so applyDeployment can lay it out).
|
|
403
565
|
reposition();
|
|
@@ -408,16 +570,25 @@ export function mountOrb(controller, options = {}) {
|
|
|
408
570
|
bubble.style.borderColor = t.cardBorderColor;
|
|
409
571
|
if (modalTheme?.borderRadius != null)
|
|
410
572
|
bubble.style.borderRadius = `${modalTheme.borderRadius}px`;
|
|
411
|
-
userLine.style.color = t.bodyColor;
|
|
412
|
-
replyLine.style.color = t.titleColor;
|
|
413
573
|
closeBtn.style.color = t.bodyColor;
|
|
414
|
-
|
|
415
|
-
|
|
574
|
+
exchangeList.querySelectorAll('.fyodos-bubble-user').forEach((node) => {
|
|
575
|
+
node.style.color = t.bodyColor;
|
|
576
|
+
});
|
|
577
|
+
exchangeList.querySelectorAll('.fyodos-bubble-reply').forEach((node) => {
|
|
578
|
+
node.style.color = t.titleColor;
|
|
579
|
+
});
|
|
580
|
+
const inputBg = modalTheme?.inputBackgroundColor ?? '#ffffff';
|
|
581
|
+
textInput.style.background = inputBg;
|
|
582
|
+
textInput.style.color = fiodosInputInkColor(inputBg);
|
|
583
|
+
textInput.style.setProperty('--fyodos-input-placeholder', fiodosInputMutedInkColor(inputBg));
|
|
416
584
|
sendBtn.style.background = accent;
|
|
417
585
|
sendBtn.style.borderRadius = `${modalTheme?.sendButtonRadius ?? 10}px`;
|
|
586
|
+
syncMicButton();
|
|
418
587
|
busyRow.querySelectorAll('span').forEach((s) => {
|
|
419
588
|
s.style.background = accent;
|
|
420
589
|
});
|
|
590
|
+
// Live-activity line: white (reply color), italic — not the faint gray.
|
|
591
|
+
busyStep.style.color = t.titleColor;
|
|
421
592
|
}
|
|
422
593
|
function setBubble(open) {
|
|
423
594
|
bubbleOpen = open;
|
|
@@ -428,6 +599,10 @@ export function mountOrb(controller, options = {}) {
|
|
|
428
599
|
textInput.focus();
|
|
429
600
|
}
|
|
430
601
|
else {
|
|
602
|
+
bubbleDictation.stop();
|
|
603
|
+
syncMicButton();
|
|
604
|
+
// Collapse the paged history back to the initial page for the next open.
|
|
605
|
+
controller.resetBubbleWindow();
|
|
431
606
|
// Closing the panel returns the orb to idle (also clears mic fallback).
|
|
432
607
|
micFallbackActive = false;
|
|
433
608
|
}
|
|
@@ -441,15 +616,27 @@ export function mountOrb(controller, options = {}) {
|
|
|
441
616
|
const value = textInput.value.trim();
|
|
442
617
|
if (!value)
|
|
443
618
|
return;
|
|
619
|
+
bubbleDictation.stop();
|
|
620
|
+
syncMicButton();
|
|
444
621
|
haptics.trigger('send');
|
|
445
622
|
textInput.value = '';
|
|
446
623
|
// Silent turn: the reply is SHOWN in the bubble (not spoken), and the bubble
|
|
447
624
|
// STAYS OPEN. This is the unified, reference-matching behaviour.
|
|
448
625
|
void controller.submitTextTurn(value);
|
|
449
626
|
}
|
|
450
|
-
function openTextPanel() {
|
|
451
|
-
|
|
627
|
+
function openTextPanel(seed, autoSend = false) {
|
|
628
|
+
// Opening the keyboard cancels any in-flight turn; a cancelled message
|
|
629
|
+
// becomes the editable input content unless an explicit seed was given.
|
|
630
|
+
const cancelled = controller.cancelAll();
|
|
631
|
+
const explicitSeed = seed?.trim() ?? '';
|
|
632
|
+
const draft = explicitSeed || cancelled || '';
|
|
633
|
+
textInput.value = draft;
|
|
452
634
|
setBubble(true);
|
|
635
|
+
// Only an explicit seed may auto-send; a restored cancelled message must
|
|
636
|
+
// wait for the user to edit/resend it.
|
|
637
|
+
if (autoSend && explicitSeed) {
|
|
638
|
+
queueMicrotask(() => submitText());
|
|
639
|
+
}
|
|
453
640
|
}
|
|
454
641
|
// ── Keyboard chip (rebuilt when theme changes / visibility toggles) ──────────
|
|
455
642
|
// Appears beside the ACTIVE orb — whether listening for real (voice granted)
|
|
@@ -487,26 +674,62 @@ export function mountOrb(controller, options = {}) {
|
|
|
487
674
|
function resolveCurrentBubblePrompt() {
|
|
488
675
|
return resolveBubblePrompt(matchRouteIntent(currentRoute, controller.config.manifest.routes), controller.messages, { locale: controller.config.locale, actions: controller.config.manifest.actions });
|
|
489
676
|
}
|
|
677
|
+
// Render the CTA on ONE line at its NATURAL size. When the phrase is too long
|
|
678
|
+
// to fit the max width, scroll it (marquee, right-to-left, looping) instead of
|
|
679
|
+
// shrinking the font or truncating — the bubble keeps a constant height.
|
|
680
|
+
function layoutThoughtContent(prompt) {
|
|
681
|
+
const t = bubbleTheme;
|
|
682
|
+
const { padH } = resolveBubblePadding(t);
|
|
683
|
+
// Measure the natural (single-copy) text width at the base font size.
|
|
684
|
+
thoughtBubbleEl.style.width = '';
|
|
685
|
+
thoughtBubbleEl.textContent = prompt;
|
|
686
|
+
const measured = thoughtBubbleEl.scrollWidth;
|
|
687
|
+
const viewport = typeof window !== 'undefined' ? window.innerWidth : 280;
|
|
688
|
+
const maxW = Math.min(280, viewport * 0.7);
|
|
689
|
+
const overflow = measured > maxW - t.borderWidth * 2;
|
|
690
|
+
if (!overflow) {
|
|
691
|
+
thoughtBubbleEl.style.width = '';
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const textWidth = Math.max(0, measured - padH * 2);
|
|
695
|
+
const durationSec = Math.max(4, (textWidth + 48) / 30);
|
|
696
|
+
thoughtBubbleEl.style.width = `${maxW}px`;
|
|
697
|
+
thoughtBubbleEl.textContent = '';
|
|
698
|
+
const track = document.createElement('div');
|
|
699
|
+
track.className = 'fyodos-thought-track';
|
|
700
|
+
track.style.animation = `fyodos-marquee ${durationSec}s linear infinite`;
|
|
701
|
+
const first = document.createElement('span');
|
|
702
|
+
first.textContent = prompt;
|
|
703
|
+
const second = document.createElement('span');
|
|
704
|
+
second.textContent = prompt;
|
|
705
|
+
second.setAttribute('aria-hidden', 'true');
|
|
706
|
+
track.appendChild(first);
|
|
707
|
+
track.appendChild(second);
|
|
708
|
+
thoughtBubbleEl.appendChild(track);
|
|
709
|
+
}
|
|
490
710
|
function applyThoughtBubbleTheme() {
|
|
491
711
|
const t = bubbleTheme;
|
|
492
|
-
const { fontPx,
|
|
493
|
-
const
|
|
712
|
+
const { fontPx, padH, approxHeight } = resolveBubblePadding(t);
|
|
713
|
+
const shapeCss = bubbleShapeToCss(t, approxHeight);
|
|
494
714
|
thoughtBubbleEl.style.fontSize = `${fontPx}px`;
|
|
495
|
-
thoughtBubbleEl.style.
|
|
715
|
+
thoughtBubbleEl.style.height = `${approxHeight}px`;
|
|
716
|
+
thoughtBubbleEl.style.padding = `0 ${padH}px`;
|
|
496
717
|
thoughtBubbleEl.style.color = t.textColor;
|
|
497
718
|
thoughtBubbleEl.style.background = t.backgroundColor;
|
|
498
719
|
thoughtBubbleEl.style.border = `${t.borderWidth}px solid ${t.borderColor}`;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
720
|
+
const setImportant = (prop, value) => {
|
|
721
|
+
if (!value) {
|
|
722
|
+
thoughtBubbleEl.style.removeProperty(prop);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
thoughtBubbleEl.style.setProperty(prop, value, 'important');
|
|
726
|
+
};
|
|
727
|
+
setImportant('border-radius', shapeCss.borderRadius);
|
|
728
|
+
setImportant('border-top-left-radius', shapeCss.borderTopLeftRadius);
|
|
729
|
+
setImportant('border-top-right-radius', shapeCss.borderTopRightRadius);
|
|
730
|
+
setImportant('border-bottom-left-radius', shapeCss.borderBottomLeftRadius);
|
|
731
|
+
setImportant('border-bottom-right-radius', shapeCss.borderBottomRightRadius);
|
|
732
|
+
setImportant('clip-path', shapeCss.clipPath);
|
|
510
733
|
}
|
|
511
734
|
function positionThoughtBubble() {
|
|
512
735
|
const dep = resolveOrbDeployment(effectivePosition());
|
|
@@ -536,11 +759,12 @@ export function mountOrb(controller, options = {}) {
|
|
|
536
759
|
thoughtBubbleEl.style.display = 'none';
|
|
537
760
|
return;
|
|
538
761
|
}
|
|
539
|
-
thoughtBubbleEl.
|
|
540
|
-
|
|
762
|
+
thoughtBubbleEl.setAttribute('aria-label', prompt);
|
|
763
|
+
// Display before measuring so `scrollWidth` (used to detect overflow) is real.
|
|
764
|
+
thoughtBubbleEl.style.display = 'flex';
|
|
541
765
|
applyThoughtBubbleTheme();
|
|
766
|
+
layoutThoughtContent(prompt);
|
|
542
767
|
positionThoughtBubble();
|
|
543
|
-
thoughtBubbleEl.style.display = 'block';
|
|
544
768
|
}
|
|
545
769
|
// ── Overlays (confirmation + consent) ────────────────────────────────────────
|
|
546
770
|
let overlayEl = null;
|
|
@@ -646,7 +870,12 @@ export function mountOrb(controller, options = {}) {
|
|
|
646
870
|
// (aborts the request, no reply is delivered) and returns the orb to idle.
|
|
647
871
|
if (state.showThinking) {
|
|
648
872
|
haptics.trigger('orbTap');
|
|
649
|
-
controller.cancelAll();
|
|
873
|
+
const draft = controller.cancelAll();
|
|
874
|
+
// The cancelled message goes back into the input for editing.
|
|
875
|
+
if (draft && bubbleOpen) {
|
|
876
|
+
textInput.value = draft;
|
|
877
|
+
scrollTextInputToEnd(textInput);
|
|
878
|
+
}
|
|
650
879
|
return;
|
|
651
880
|
}
|
|
652
881
|
haptics.trigger('orbTap');
|
|
@@ -680,10 +909,17 @@ export function mountOrb(controller, options = {}) {
|
|
|
680
909
|
};
|
|
681
910
|
sendBtn.onclick = () => {
|
|
682
911
|
// Send (arrow) while idle; Stop (square) while a turn is processing — tapping
|
|
683
|
-
// it cancels the in-flight turn so the user can type
|
|
684
|
-
|
|
912
|
+
// it cancels the in-flight turn (or running chain) so the user can type again.
|
|
913
|
+
const st = controller.getState();
|
|
914
|
+
if (st.isBusy || st.chainActive) {
|
|
685
915
|
haptics.trigger('orbTap');
|
|
686
|
-
|
|
916
|
+
// STOP: the cancelled message leaves the panel and goes back into the
|
|
917
|
+
// input so the user can edit and resend it.
|
|
918
|
+
const draft = controller.cancelAll();
|
|
919
|
+
if (draft) {
|
|
920
|
+
textInput.value = draft;
|
|
921
|
+
scrollTextInputToEnd(textInput);
|
|
922
|
+
}
|
|
687
923
|
textInput.focus();
|
|
688
924
|
return;
|
|
689
925
|
}
|
|
@@ -695,6 +931,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
695
931
|
if (e.key === 'Escape')
|
|
696
932
|
setBubble(false);
|
|
697
933
|
};
|
|
934
|
+
textInput.addEventListener('input', () => scrollTextInputToEnd(textInput));
|
|
698
935
|
if (!enableTextInput) {
|
|
699
936
|
if (chipEl)
|
|
700
937
|
chipEl.style.display = 'none';
|
|
@@ -737,20 +974,28 @@ export function mountOrb(controller, options = {}) {
|
|
|
737
974
|
? 'speaking'
|
|
738
975
|
: 'idle';
|
|
739
976
|
orbVisual.applyState(visual, state.volumeLevel);
|
|
740
|
-
//
|
|
977
|
+
// Busy through the WHOLE task: single turns and every autonomous chain
|
|
978
|
+
// step (so the real narration never flickers between steps).
|
|
979
|
+
const rowBusy = state.showThinking || state.chainActive;
|
|
980
|
+
// REAL activity line: updates when the agent actually does something new,
|
|
981
|
+
// and clears when the reply lands.
|
|
982
|
+
syncActivityStep(rowBusy, state);
|
|
983
|
+
// Text bubble: show recent exchanges + busy state.
|
|
741
984
|
if (bubbleOpen) {
|
|
742
|
-
|
|
985
|
+
loaderRow.style.display = state.loadingOlderExchanges ? 'flex' : 'none';
|
|
986
|
+
renderBubbleExchanges(state.recentExchanges);
|
|
743
987
|
exchangeScroll.style.display = 'flex';
|
|
744
|
-
|
|
745
|
-
replyLine.textContent = ex?.replyText ?? '';
|
|
746
|
-
replyLine.style.display = ex?.replyText ? 'block' : 'none';
|
|
747
|
-
busyRow.style.display = state.showThinking ? 'flex' : 'none';
|
|
988
|
+
busyRow.style.display = rowBusy ? 'flex' : 'none';
|
|
748
989
|
// Send (arrow) ↔ Stop (square): while busy the action becomes "cancel".
|
|
749
|
-
|
|
990
|
+
// A running autonomous chain counts as busy so the user can interrupt it.
|
|
991
|
+
const busy = state.isBusy || state.chainActive;
|
|
750
992
|
sendBtn.textContent = busy ? '◼' : '↑';
|
|
751
993
|
sendBtn.setAttribute('aria-label', busy ? ui.cancelLabel : ui.sendLabel);
|
|
752
994
|
sendBtn.style.opacity = '1';
|
|
753
995
|
sendBtn.disabled = false;
|
|
996
|
+
micBtn.style.opacity = busy ? '0.45' : '1';
|
|
997
|
+
micBtn.disabled = busy;
|
|
998
|
+
syncMicButton();
|
|
754
999
|
}
|
|
755
1000
|
syncChip(state);
|
|
756
1001
|
syncThoughtBubble(state);
|
|
@@ -798,7 +1043,11 @@ export function mountOrb(controller, options = {}) {
|
|
|
798
1043
|
// Dashboard master switch: hide the whole orb (button + chip + bubble live
|
|
799
1044
|
// inside `root`) when explicitly turned off, show it otherwise. No host
|
|
800
1045
|
// code change — it flips on the next poll and reappears when re-enabled.
|
|
801
|
-
|
|
1046
|
+
// Internal-profile orbs additionally hide on public (non-development)
|
|
1047
|
+
// hosts: they are for the developer/company, never for end users.
|
|
1048
|
+
const hiddenByProfile = config != null &&
|
|
1049
|
+
shouldHideInternalOrb(config.internalProfile, options.allowInternalOrb === true);
|
|
1050
|
+
root.style.display = (config && config.enabled === false) || hiddenByProfile ? 'none' : '';
|
|
802
1051
|
if (config) {
|
|
803
1052
|
publishedAppearance = config.appearance;
|
|
804
1053
|
chipTheme = config.keyboardChip;
|
|
@@ -806,6 +1055,10 @@ export function mountOrb(controller, options = {}) {
|
|
|
806
1055
|
bubbleTheme = config.bubbleTheme;
|
|
807
1056
|
bubbleEnabled = config.bubbleEnabled;
|
|
808
1057
|
voiceInputEnabled = config.voiceInputEnabled;
|
|
1058
|
+
thinkingStepsEnabled = config.thinkingStepsEnabled;
|
|
1059
|
+
// Idle context retention chosen in the dashboard (0 = never clear) +
|
|
1060
|
+
// expanded memory window when the project's orb profile is 'internal'.
|
|
1061
|
+
controller.setConversationRetention(config.conversationRetentionMinutes, config.internalProfile);
|
|
809
1062
|
if (usePublishedPosition && config.screenPosition)
|
|
810
1063
|
screenPosition = config.screenPosition;
|
|
811
1064
|
}
|
|
@@ -823,6 +1076,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
823
1076
|
});
|
|
824
1077
|
return {
|
|
825
1078
|
unmount() {
|
|
1079
|
+
bubbleDictation.dispose();
|
|
826
1080
|
unsubscribe();
|
|
827
1081
|
stopWatch();
|
|
828
1082
|
window.clearInterval(routeTimer);
|
|
@@ -16,6 +16,8 @@ export interface OrbAppearance {
|
|
|
16
16
|
glowColor?: string;
|
|
17
17
|
colorKey?: string;
|
|
18
18
|
glowIntensity?: number;
|
|
19
|
+
/** 0–100 orb vibration/haptic strength (dashboard advanced option). */
|
|
20
|
+
hapticIntensity?: number;
|
|
19
21
|
innerBorderColor?: string;
|
|
20
22
|
innerBorderWidth?: number;
|
|
21
23
|
size: number;
|
|
@@ -27,12 +27,30 @@ export interface ResolvedOrbConfig {
|
|
|
27
27
|
* directly on tap and never plays TTS. Default true (absent = voice on).
|
|
28
28
|
*/
|
|
29
29
|
voiceInputEnabled: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Dashboard on/off for the agent's live "thinking steps" in the chat panel
|
|
32
|
+
* (`agentThinkingStepsEnabled`). True ONLY when the developer turned it on;
|
|
33
|
+
* then the busy row shows what the agent is doing instead of only the dots.
|
|
34
|
+
* Default false (absent = dots only).
|
|
35
|
+
*/
|
|
36
|
+
thinkingStepsEnabled: boolean;
|
|
30
37
|
/**
|
|
31
38
|
* Dashboard master visibility switch (`orbEnabled`). False ONLY when the
|
|
32
39
|
* developer explicitly turned the orb off. The orb is hidden when false and
|
|
33
40
|
* shown otherwise (including when nothing is published / on error).
|
|
34
41
|
*/
|
|
35
42
|
enabled: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Idle context retention in minutes (`conversationRetentionMinutes`).
|
|
45
|
+
* 0 = never clear; null = not published (the SDK's 30-minute default).
|
|
46
|
+
*/
|
|
47
|
+
conversationRetentionMinutes: number | null;
|
|
48
|
+
/**
|
|
49
|
+
* True when the project's orb profile is 'internal' (internal/developer
|
|
50
|
+
* software): the agent uses the EXPANDED conversation memory window.
|
|
51
|
+
* Default false ('public' — today's behavior unchanged).
|
|
52
|
+
*/
|
|
53
|
+
internalProfile: boolean;
|
|
36
54
|
}
|
|
37
55
|
export interface WatchPublishedConfigOptions {
|
|
38
56
|
/** Poll interval (ms) to pick up dashboard changes in real time. Default 12000. */
|