@fiodos/web-core 0.1.2 → 0.1.4
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/formatTurnError.d.ts +2 -0
- package/dist/cjs/api/formatTurnError.js +45 -0
- package/dist/cjs/controller/AgentController.d.ts +2 -0
- package/dist/cjs/controller/AgentController.js +27 -1
- package/dist/cjs/orb/mountOrb.js +34 -7
- package/dist/cjs/orb/publishedConfig.d.ts +6 -0
- package/dist/cjs/orb/publishedConfig.js +5 -2
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/api/formatTurnError.d.ts +2 -0
- package/dist/esm/api/formatTurnError.js +42 -0
- package/dist/esm/controller/AgentController.d.ts +2 -0
- package/dist/esm/controller/AgentController.js +27 -1
- package/dist/esm/orb/mountOrb.js +35 -8
- package/dist/esm/orb/publishedConfig.d.ts +6 -0
- package/dist/esm/orb/publishedConfig.js +5 -2
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatAgentTurnError = formatAgentTurnError;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
/** User-visible chat error — shown IN the text bubble, not only in the console. */
|
|
6
|
+
function formatAgentTurnError(err, locale) {
|
|
7
|
+
const es = locale.startsWith('es');
|
|
8
|
+
if (err instanceof errors_1.AgentApiError) {
|
|
9
|
+
switch (err.code) {
|
|
10
|
+
case 'timeout':
|
|
11
|
+
return es
|
|
12
|
+
? 'La respuesta tardó demasiado. Comprueba tu conexión e inténtalo de nuevo.'
|
|
13
|
+
: 'The response took too long. Check your connection and try again.';
|
|
14
|
+
case 'network':
|
|
15
|
+
return es
|
|
16
|
+
? 'No pude conectar con el servidor de Fiodos. ¿Está el backend en marcha y la URL correcta?'
|
|
17
|
+
: 'Could not reach the Fiodos server. Is the backend running and the URL correct?';
|
|
18
|
+
case 'unauthorized':
|
|
19
|
+
return es
|
|
20
|
+
? 'La API key fue rechazada. Comprueba que coincide con la del proyecto en el dashboard.'
|
|
21
|
+
: 'The API key was rejected. Check it matches the project key in your dashboard.';
|
|
22
|
+
case 'quota_exceeded':
|
|
23
|
+
return es
|
|
24
|
+
? 'Has alcanzado el límite de uso de IA de tu plan. Mejóralo en el dashboard para seguir.'
|
|
25
|
+
: 'You reached your plan\u2019s AI usage limit. Upgrade in the dashboard to continue.';
|
|
26
|
+
case 'rate_limited':
|
|
27
|
+
return es
|
|
28
|
+
? 'Demasiadas peticiones seguidas. Espera un momento e inténtalo de nuevo.'
|
|
29
|
+
: 'Too many requests. Wait a moment and try again.';
|
|
30
|
+
case 'server_error':
|
|
31
|
+
return es
|
|
32
|
+
? 'El servidor no pudo responder. Revisa que la IA esté configurada en el backend.'
|
|
33
|
+
: 'The server could not respond. Check that the AI is configured on the backend.';
|
|
34
|
+
case 'cancelled':
|
|
35
|
+
return es ? 'Cancelado.' : 'Cancelled.';
|
|
36
|
+
default:
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (err.message && err.message !== err.code)
|
|
40
|
+
return err.message;
|
|
41
|
+
}
|
|
42
|
+
if (err instanceof Error && err.message)
|
|
43
|
+
return err.message;
|
|
44
|
+
return es ? 'No pude responder. Inténtalo de nuevo.' : 'I could not reply. Please try again.';
|
|
45
|
+
}
|
|
@@ -58,6 +58,8 @@ export declare class AgentController {
|
|
|
58
58
|
private enabled;
|
|
59
59
|
private phase;
|
|
60
60
|
private lastExchange;
|
|
61
|
+
/** User text for the turn currently in flight (for error/timeout display). */
|
|
62
|
+
private pendingTurnText;
|
|
61
63
|
private confirmationMessage;
|
|
62
64
|
private confirmationVoiceHint;
|
|
63
65
|
private consentModalVisible;
|
|
@@ -20,6 +20,7 @@ exports.AgentController = void 0;
|
|
|
20
20
|
*/
|
|
21
21
|
const core_1 = require("@fiodos/core");
|
|
22
22
|
const errors_1 = require("../api/errors");
|
|
23
|
+
const formatTurnError_1 = require("../api/formatTurnError");
|
|
23
24
|
const backendClient_1 = require("../api/backendClient");
|
|
24
25
|
const screenContextStore_1 = require("../context/screenContextStore");
|
|
25
26
|
const speechSession_1 = require("../speech/speechSession");
|
|
@@ -34,6 +35,8 @@ class AgentController {
|
|
|
34
35
|
this.enabled = true;
|
|
35
36
|
this.phase = 'idle';
|
|
36
37
|
this.lastExchange = null;
|
|
38
|
+
/** User text for the turn currently in flight (for error/timeout display). */
|
|
39
|
+
this.pendingTurnText = null;
|
|
37
40
|
this.confirmationMessage = null;
|
|
38
41
|
this.confirmationVoiceHint = null;
|
|
39
42
|
this.consentModalVisible = false;
|
|
@@ -323,11 +326,21 @@ class AgentController {
|
|
|
323
326
|
return;
|
|
324
327
|
}
|
|
325
328
|
this.setPhase('thinking');
|
|
329
|
+
this.pendingTurnText = text;
|
|
330
|
+
this.lastExchange = { userText: text, replyText: '' };
|
|
331
|
+
this.emit();
|
|
326
332
|
this.clearWatchdog();
|
|
327
333
|
this.watchdog = setTimeout(() => {
|
|
328
334
|
if (this.phase === 'thinking') {
|
|
329
335
|
this.telemetry.logEvent({ eventType: 'agent_watchdog_reset', sessionId: this.sessionId });
|
|
336
|
+
const userText = this.pendingTurnText ?? text;
|
|
337
|
+
const replyText = this.config.locale.startsWith('es')
|
|
338
|
+
? 'La respuesta tardó demasiado. Inténtalo de nuevo.'
|
|
339
|
+
: 'The response took too long. Please try again.';
|
|
340
|
+
this.lastExchange = { userText, replyText };
|
|
341
|
+
this.pendingTurnText = null;
|
|
330
342
|
this.setPhase('idle');
|
|
343
|
+
this.emit();
|
|
331
344
|
}
|
|
332
345
|
}, this.timings.thinkingWatchdogMs);
|
|
333
346
|
const currentRoute = this.config.navigation.getCurrentRoute();
|
|
@@ -435,23 +448,36 @@ class AgentController {
|
|
|
435
448
|
if (e instanceof errors_1.AgentApiError && e.code === 'cancelled') {
|
|
436
449
|
if (this.phase === 'thinking')
|
|
437
450
|
this.setPhase('idle');
|
|
451
|
+
this.pendingTurnText = null;
|
|
438
452
|
return;
|
|
439
453
|
}
|
|
440
454
|
if (e instanceof errors_1.AgentApiError && e.code === 'quota_exceeded') {
|
|
441
455
|
this.config.onQuotaExceeded?.();
|
|
442
456
|
}
|
|
457
|
+
const errMsg = (0, formatTurnError_1.formatAgentTurnError)(e, this.config.locale);
|
|
458
|
+
this.lastExchange = { userText: text, replyText: errMsg };
|
|
459
|
+
this.pendingTurnText = null;
|
|
443
460
|
if (this.phase === 'thinking')
|
|
444
461
|
this.setPhase('idle');
|
|
445
|
-
this.
|
|
462
|
+
this.emit();
|
|
463
|
+
this.notifyError(errMsg);
|
|
446
464
|
return;
|
|
447
465
|
}
|
|
448
466
|
this.clearWatchdog();
|
|
449
467
|
this.abort = null;
|
|
468
|
+
this.pendingTurnText = null;
|
|
450
469
|
if (reply) {
|
|
451
470
|
this.lastExchange = { userText: text, replyText: reply };
|
|
452
471
|
this.emit();
|
|
453
472
|
(0, core_1.recordConversationExchange)(this.conversation, text, reply);
|
|
454
473
|
}
|
|
474
|
+
else {
|
|
475
|
+
const fallback = this.config.locale.startsWith('es')
|
|
476
|
+
? 'No recibí respuesta del servidor. Inténtalo de nuevo.'
|
|
477
|
+
: 'No reply from the server. Please try again.';
|
|
478
|
+
this.lastExchange = { userText: text, replyText: fallback };
|
|
479
|
+
this.emit();
|
|
480
|
+
}
|
|
455
481
|
const hasPending = this.pending != null;
|
|
456
482
|
await this.speakReply(reply, audioBase64);
|
|
457
483
|
if (hasPending) {
|
package/dist/cjs/orb/mountOrb.js
CHANGED
|
@@ -65,6 +65,18 @@ function ensureStyles() {
|
|
|
65
65
|
el.textContent = CSS;
|
|
66
66
|
document.head.appendChild(el);
|
|
67
67
|
}
|
|
68
|
+
// Content box of the layout viewport. `documentElement.clientWidth/Height`
|
|
69
|
+
// EXCLUDE a classic scrollbar that occupies real width/height (Windows, some
|
|
70
|
+
// Linux/Chromium); overlay scrollbars (macOS, mobile) report the same value as
|
|
71
|
+
// the layout viewport. Anchoring against this — not `innerWidth` — keeps a
|
|
72
|
+
// right/bottom-edge orb just INSIDE the scrollbar instead of under/over it.
|
|
73
|
+
function viewportContentSize() {
|
|
74
|
+
const de = typeof document !== 'undefined' ? document.documentElement : null;
|
|
75
|
+
return {
|
|
76
|
+
width: de?.clientWidth || window.innerWidth,
|
|
77
|
+
height: de?.clientHeight || window.innerHeight,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
68
80
|
function cornerToPosition(corner) {
|
|
69
81
|
switch (corner) {
|
|
70
82
|
case 'bottom-left':
|
|
@@ -161,14 +173,15 @@ function mountOrb(controller, options = {}) {
|
|
|
161
173
|
}
|
|
162
174
|
function reposition() {
|
|
163
175
|
const size = orbDiameterPx();
|
|
164
|
-
const
|
|
176
|
+
const content = viewportContentSize();
|
|
177
|
+
const { left, top } = (0, core_1.resolveOrbAnchorPx)({
|
|
165
178
|
viewportWidth: window.innerWidth,
|
|
166
179
|
viewportHeight: window.innerHeight,
|
|
180
|
+
contentWidth: content.width,
|
|
181
|
+
contentHeight: content.height,
|
|
167
182
|
orbDiameter: size,
|
|
168
183
|
position: effectivePosition(),
|
|
169
184
|
});
|
|
170
|
-
const left = Math.max(8, window.innerWidth - insets.right - size);
|
|
171
|
-
const top = Math.max(8, window.innerHeight - insets.bottom - size);
|
|
172
185
|
root.style.left = `${left}px`;
|
|
173
186
|
root.style.top = `${top}px`;
|
|
174
187
|
root.style.right = '';
|
|
@@ -197,6 +210,14 @@ function mountOrb(controller, options = {}) {
|
|
|
197
210
|
}
|
|
198
211
|
}
|
|
199
212
|
window.addEventListener('resize', reposition);
|
|
213
|
+
// Recalc when a scrollbar appears/disappears as the page content changes (the
|
|
214
|
+
// content box shrinks/grows even when the window size doesn't), so the orb
|
|
215
|
+
// never ends up overlapped by — or detached from — the scrollbar edge.
|
|
216
|
+
let contentObserver;
|
|
217
|
+
if (typeof ResizeObserver !== 'undefined' && document.documentElement) {
|
|
218
|
+
contentObserver = new ResizeObserver(() => reposition());
|
|
219
|
+
contentObserver.observe(document.documentElement);
|
|
220
|
+
}
|
|
200
221
|
// Keep the conversation bar above the keyboard as the visual viewport resizes
|
|
201
222
|
// (keyboard open/close) or scrolls on mobile browsers.
|
|
202
223
|
const visualViewport = window.visualViewport;
|
|
@@ -240,9 +261,10 @@ function mountOrb(controller, options = {}) {
|
|
|
240
261
|
dragging = true;
|
|
241
262
|
e.preventDefault();
|
|
242
263
|
const size = orbDiameterPx();
|
|
264
|
+
const content = viewportContentSize();
|
|
243
265
|
const margin = 8;
|
|
244
|
-
const maxLeft =
|
|
245
|
-
const maxTop =
|
|
266
|
+
const maxLeft = content.width - margin - size;
|
|
267
|
+
const maxTop = content.height - margin - size;
|
|
246
268
|
const left = Math.min(maxLeft, Math.max(margin, originLeft + dx));
|
|
247
269
|
const top = Math.min(maxTop, Math.max(margin, originTop + dy));
|
|
248
270
|
root.style.left = `${left}px`;
|
|
@@ -252,8 +274,8 @@ function mountOrb(controller, options = {}) {
|
|
|
252
274
|
draggedPosition = (0, core_1.viewportPixelsToOrbScreenPosition)({
|
|
253
275
|
centerX: left + size / 2,
|
|
254
276
|
centerY: top + size / 2,
|
|
255
|
-
viewportWidth:
|
|
256
|
-
viewportHeight:
|
|
277
|
+
viewportWidth: content.width,
|
|
278
|
+
viewportHeight: content.height,
|
|
257
279
|
orbDiameter: size,
|
|
258
280
|
});
|
|
259
281
|
applyDeployment();
|
|
@@ -661,6 +683,10 @@ function mountOrb(controller, options = {}) {
|
|
|
661
683
|
const stopWatch = (0, publishedConfig_1.watchPublishedConfig)(controller, {
|
|
662
684
|
pollMs: options.appearancePollMs,
|
|
663
685
|
onChange: (config) => {
|
|
686
|
+
// Dashboard master switch: hide the whole orb (button + chip + bubble live
|
|
687
|
+
// inside `root`) when explicitly turned off, show it otherwise. No host
|
|
688
|
+
// code change — it flips on the next poll and reappears when re-enabled.
|
|
689
|
+
root.style.display = config && config.enabled === false ? 'none' : '';
|
|
664
690
|
if (config) {
|
|
665
691
|
publishedAppearance = config.appearance;
|
|
666
692
|
chipTheme = config.keyboardChip;
|
|
@@ -684,6 +710,7 @@ function mountOrb(controller, options = {}) {
|
|
|
684
710
|
unsubscribe();
|
|
685
711
|
stopWatch();
|
|
686
712
|
window.removeEventListener('resize', reposition);
|
|
713
|
+
contentObserver?.disconnect();
|
|
687
714
|
visualViewport?.removeEventListener('resize', onViewportChange);
|
|
688
715
|
visualViewport?.removeEventListener('scroll', onViewportChange);
|
|
689
716
|
clearOverlay();
|
|
@@ -17,6 +17,12 @@ export interface ResolvedOrbConfig {
|
|
|
17
17
|
keyboardChip: ChipTheme | null;
|
|
18
18
|
modalTheme: PublishedModalTheme | undefined;
|
|
19
19
|
screenPosition: OrbScreenPosition | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Dashboard master visibility switch (`orbEnabled`). False ONLY when the
|
|
22
|
+
* developer explicitly turned the orb off. The orb is hidden when false and
|
|
23
|
+
* shown otherwise (including when nothing is published / on error).
|
|
24
|
+
*/
|
|
25
|
+
enabled: boolean;
|
|
20
26
|
}
|
|
21
27
|
export interface WatchPublishedConfigOptions {
|
|
22
28
|
/** Poll interval (ms) to pick up dashboard changes in real time. Default 12000. */
|
|
@@ -14,7 +14,7 @@ exports.watchPublishedConfig = watchPublishedConfig;
|
|
|
14
14
|
*/
|
|
15
15
|
const core_1 = require("@fiodos/core");
|
|
16
16
|
const orbView_1 = require("./orbView");
|
|
17
|
-
function toResolved(props) {
|
|
17
|
+
function toResolved(props, enabled) {
|
|
18
18
|
const theme = props.theme;
|
|
19
19
|
const appearance = {
|
|
20
20
|
accentColor: theme.accentColor ?? orbView_1.DEFAULT_ORB_APPEARANCE.accentColor,
|
|
@@ -33,6 +33,7 @@ function toResolved(props) {
|
|
|
33
33
|
keyboardChip: theme.keyboardChip ?? null,
|
|
34
34
|
modalTheme: props.modalTheme,
|
|
35
35
|
screenPosition: props.screenPosition,
|
|
36
|
+
enabled,
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
@@ -52,7 +53,9 @@ function watchPublishedConfig(controller, options) {
|
|
|
52
53
|
const res = await fetcher.call(controller.config.backend);
|
|
53
54
|
if (cancelled)
|
|
54
55
|
return;
|
|
55
|
-
options.onChange(res.published
|
|
56
|
+
options.onChange(res.published
|
|
57
|
+
? toResolved((0, core_1.mapPublishedConfigToOrbProps)(res.published), res.published.orbEnabled !== false)
|
|
58
|
+
: null);
|
|
56
59
|
}
|
|
57
60
|
catch {
|
|
58
61
|
if (!cancelled)
|
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.3";
|
|
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.3';
|
|
12
12
|
exports.SDK_PACKAGE = '@fiodos/web-core';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AgentApiError } from './errors.js';
|
|
2
|
+
/** User-visible chat error — shown IN the text bubble, not only in the console. */
|
|
3
|
+
export function formatAgentTurnError(err, locale) {
|
|
4
|
+
const es = locale.startsWith('es');
|
|
5
|
+
if (err instanceof AgentApiError) {
|
|
6
|
+
switch (err.code) {
|
|
7
|
+
case 'timeout':
|
|
8
|
+
return es
|
|
9
|
+
? 'La respuesta tardó demasiado. Comprueba tu conexión e inténtalo de nuevo.'
|
|
10
|
+
: 'The response took too long. Check your connection and try again.';
|
|
11
|
+
case 'network':
|
|
12
|
+
return es
|
|
13
|
+
? 'No pude conectar con el servidor de Fiodos. ¿Está el backend en marcha y la URL correcta?'
|
|
14
|
+
: 'Could not reach the Fiodos server. Is the backend running and the URL correct?';
|
|
15
|
+
case 'unauthorized':
|
|
16
|
+
return es
|
|
17
|
+
? 'La API key fue rechazada. Comprueba que coincide con la del proyecto en el dashboard.'
|
|
18
|
+
: 'The API key was rejected. Check it matches the project key in your dashboard.';
|
|
19
|
+
case 'quota_exceeded':
|
|
20
|
+
return es
|
|
21
|
+
? 'Has alcanzado el límite de uso de IA de tu plan. Mejóralo en el dashboard para seguir.'
|
|
22
|
+
: 'You reached your plan\u2019s AI usage limit. Upgrade in the dashboard to continue.';
|
|
23
|
+
case 'rate_limited':
|
|
24
|
+
return es
|
|
25
|
+
? 'Demasiadas peticiones seguidas. Espera un momento e inténtalo de nuevo.'
|
|
26
|
+
: 'Too many requests. Wait a moment and try again.';
|
|
27
|
+
case 'server_error':
|
|
28
|
+
return es
|
|
29
|
+
? 'El servidor no pudo responder. Revisa que la IA esté configurada en el backend.'
|
|
30
|
+
: 'The server could not respond. Check that the AI is configured on the backend.';
|
|
31
|
+
case 'cancelled':
|
|
32
|
+
return es ? 'Cancelado.' : 'Cancelled.';
|
|
33
|
+
default:
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (err.message && err.message !== err.code)
|
|
37
|
+
return err.message;
|
|
38
|
+
}
|
|
39
|
+
if (err instanceof Error && err.message)
|
|
40
|
+
return err.message;
|
|
41
|
+
return es ? 'No pude responder. Inténtalo de nuevo.' : 'I could not reply. Please try again.';
|
|
42
|
+
}
|
|
@@ -58,6 +58,8 @@ export declare class AgentController {
|
|
|
58
58
|
private enabled;
|
|
59
59
|
private phase;
|
|
60
60
|
private lastExchange;
|
|
61
|
+
/** User text for the turn currently in flight (for error/timeout display). */
|
|
62
|
+
private pendingTurnText;
|
|
61
63
|
private confirmationMessage;
|
|
62
64
|
private confirmationVoiceHint;
|
|
63
65
|
private consentModalVisible;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, createConversationSession, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, validateManifest, } from '@fiodos/core';
|
|
19
19
|
import { AgentApiError } from '../api/errors.js';
|
|
20
|
+
import { formatAgentTurnError } from '../api/formatTurnError.js';
|
|
20
21
|
import { buildManifestPayload } from '../api/backendClient.js';
|
|
21
22
|
import { createScreenContextStore, } from '../context/screenContextStore.js';
|
|
22
23
|
import { createSpeechSession } from '../speech/speechSession.js';
|
|
@@ -31,6 +32,8 @@ export class AgentController {
|
|
|
31
32
|
this.enabled = true;
|
|
32
33
|
this.phase = 'idle';
|
|
33
34
|
this.lastExchange = null;
|
|
35
|
+
/** User text for the turn currently in flight (for error/timeout display). */
|
|
36
|
+
this.pendingTurnText = null;
|
|
34
37
|
this.confirmationMessage = null;
|
|
35
38
|
this.confirmationVoiceHint = null;
|
|
36
39
|
this.consentModalVisible = false;
|
|
@@ -320,11 +323,21 @@ export class AgentController {
|
|
|
320
323
|
return;
|
|
321
324
|
}
|
|
322
325
|
this.setPhase('thinking');
|
|
326
|
+
this.pendingTurnText = text;
|
|
327
|
+
this.lastExchange = { userText: text, replyText: '' };
|
|
328
|
+
this.emit();
|
|
323
329
|
this.clearWatchdog();
|
|
324
330
|
this.watchdog = setTimeout(() => {
|
|
325
331
|
if (this.phase === 'thinking') {
|
|
326
332
|
this.telemetry.logEvent({ eventType: 'agent_watchdog_reset', sessionId: this.sessionId });
|
|
333
|
+
const userText = this.pendingTurnText ?? text;
|
|
334
|
+
const replyText = this.config.locale.startsWith('es')
|
|
335
|
+
? 'La respuesta tardó demasiado. Inténtalo de nuevo.'
|
|
336
|
+
: 'The response took too long. Please try again.';
|
|
337
|
+
this.lastExchange = { userText, replyText };
|
|
338
|
+
this.pendingTurnText = null;
|
|
327
339
|
this.setPhase('idle');
|
|
340
|
+
this.emit();
|
|
328
341
|
}
|
|
329
342
|
}, this.timings.thinkingWatchdogMs);
|
|
330
343
|
const currentRoute = this.config.navigation.getCurrentRoute();
|
|
@@ -432,23 +445,36 @@ export class AgentController {
|
|
|
432
445
|
if (e instanceof AgentApiError && e.code === 'cancelled') {
|
|
433
446
|
if (this.phase === 'thinking')
|
|
434
447
|
this.setPhase('idle');
|
|
448
|
+
this.pendingTurnText = null;
|
|
435
449
|
return;
|
|
436
450
|
}
|
|
437
451
|
if (e instanceof AgentApiError && e.code === 'quota_exceeded') {
|
|
438
452
|
this.config.onQuotaExceeded?.();
|
|
439
453
|
}
|
|
454
|
+
const errMsg = formatAgentTurnError(e, this.config.locale);
|
|
455
|
+
this.lastExchange = { userText: text, replyText: errMsg };
|
|
456
|
+
this.pendingTurnText = null;
|
|
440
457
|
if (this.phase === 'thinking')
|
|
441
458
|
this.setPhase('idle');
|
|
442
|
-
this.
|
|
459
|
+
this.emit();
|
|
460
|
+
this.notifyError(errMsg);
|
|
443
461
|
return;
|
|
444
462
|
}
|
|
445
463
|
this.clearWatchdog();
|
|
446
464
|
this.abort = null;
|
|
465
|
+
this.pendingTurnText = null;
|
|
447
466
|
if (reply) {
|
|
448
467
|
this.lastExchange = { userText: text, replyText: reply };
|
|
449
468
|
this.emit();
|
|
450
469
|
recordConversationExchange(this.conversation, text, reply);
|
|
451
470
|
}
|
|
471
|
+
else {
|
|
472
|
+
const fallback = this.config.locale.startsWith('es')
|
|
473
|
+
? 'No recibí respuesta del servidor. Inténtalo de nuevo.'
|
|
474
|
+
: 'No reply from the server. Please try again.';
|
|
475
|
+
this.lastExchange = { userText: text, replyText: fallback };
|
|
476
|
+
this.emit();
|
|
477
|
+
}
|
|
452
478
|
const hasPending = this.pending != null;
|
|
453
479
|
await this.speakReply(reply, audioBase64);
|
|
454
480
|
if (hasPending) {
|
package/dist/esm/orb/mountOrb.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - CONFIRMATIONS: the same security model (manifest decides; strict phrase /
|
|
17
17
|
* deliberate tap for payment-grade; loose "yes" never resolves strict).
|
|
18
18
|
*/
|
|
19
|
-
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics,
|
|
19
|
+
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, resolveOrbAnchorPx, resolveOrbDeployment, snapOrbScreenPositionToSide, viewportPixelsToOrbScreenPosition, } from '@fiodos/core';
|
|
20
20
|
import { buildKeyboardChip, createOrbVisual, DEFAULT_ORB_APPEARANCE, } from './orbView.js';
|
|
21
21
|
import { watchPublishedConfig } from './publishedConfig.js';
|
|
22
22
|
const STYLE_ID = 'fyodos-orb-styles';
|
|
@@ -62,6 +62,18 @@ function ensureStyles() {
|
|
|
62
62
|
el.textContent = CSS;
|
|
63
63
|
document.head.appendChild(el);
|
|
64
64
|
}
|
|
65
|
+
// Content box of the layout viewport. `documentElement.clientWidth/Height`
|
|
66
|
+
// EXCLUDE a classic scrollbar that occupies real width/height (Windows, some
|
|
67
|
+
// Linux/Chromium); overlay scrollbars (macOS, mobile) report the same value as
|
|
68
|
+
// the layout viewport. Anchoring against this — not `innerWidth` — keeps a
|
|
69
|
+
// right/bottom-edge orb just INSIDE the scrollbar instead of under/over it.
|
|
70
|
+
function viewportContentSize() {
|
|
71
|
+
const de = typeof document !== 'undefined' ? document.documentElement : null;
|
|
72
|
+
return {
|
|
73
|
+
width: de?.clientWidth || window.innerWidth,
|
|
74
|
+
height: de?.clientHeight || window.innerHeight,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
65
77
|
function cornerToPosition(corner) {
|
|
66
78
|
switch (corner) {
|
|
67
79
|
case 'bottom-left':
|
|
@@ -158,14 +170,15 @@ export function mountOrb(controller, options = {}) {
|
|
|
158
170
|
}
|
|
159
171
|
function reposition() {
|
|
160
172
|
const size = orbDiameterPx();
|
|
161
|
-
const
|
|
173
|
+
const content = viewportContentSize();
|
|
174
|
+
const { left, top } = resolveOrbAnchorPx({
|
|
162
175
|
viewportWidth: window.innerWidth,
|
|
163
176
|
viewportHeight: window.innerHeight,
|
|
177
|
+
contentWidth: content.width,
|
|
178
|
+
contentHeight: content.height,
|
|
164
179
|
orbDiameter: size,
|
|
165
180
|
position: effectivePosition(),
|
|
166
181
|
});
|
|
167
|
-
const left = Math.max(8, window.innerWidth - insets.right - size);
|
|
168
|
-
const top = Math.max(8, window.innerHeight - insets.bottom - size);
|
|
169
182
|
root.style.left = `${left}px`;
|
|
170
183
|
root.style.top = `${top}px`;
|
|
171
184
|
root.style.right = '';
|
|
@@ -194,6 +207,14 @@ export function mountOrb(controller, options = {}) {
|
|
|
194
207
|
}
|
|
195
208
|
}
|
|
196
209
|
window.addEventListener('resize', reposition);
|
|
210
|
+
// Recalc when a scrollbar appears/disappears as the page content changes (the
|
|
211
|
+
// content box shrinks/grows even when the window size doesn't), so the orb
|
|
212
|
+
// never ends up overlapped by — or detached from — the scrollbar edge.
|
|
213
|
+
let contentObserver;
|
|
214
|
+
if (typeof ResizeObserver !== 'undefined' && document.documentElement) {
|
|
215
|
+
contentObserver = new ResizeObserver(() => reposition());
|
|
216
|
+
contentObserver.observe(document.documentElement);
|
|
217
|
+
}
|
|
197
218
|
// Keep the conversation bar above the keyboard as the visual viewport resizes
|
|
198
219
|
// (keyboard open/close) or scrolls on mobile browsers.
|
|
199
220
|
const visualViewport = window.visualViewport;
|
|
@@ -237,9 +258,10 @@ export function mountOrb(controller, options = {}) {
|
|
|
237
258
|
dragging = true;
|
|
238
259
|
e.preventDefault();
|
|
239
260
|
const size = orbDiameterPx();
|
|
261
|
+
const content = viewportContentSize();
|
|
240
262
|
const margin = 8;
|
|
241
|
-
const maxLeft =
|
|
242
|
-
const maxTop =
|
|
263
|
+
const maxLeft = content.width - margin - size;
|
|
264
|
+
const maxTop = content.height - margin - size;
|
|
243
265
|
const left = Math.min(maxLeft, Math.max(margin, originLeft + dx));
|
|
244
266
|
const top = Math.min(maxTop, Math.max(margin, originTop + dy));
|
|
245
267
|
root.style.left = `${left}px`;
|
|
@@ -249,8 +271,8 @@ export function mountOrb(controller, options = {}) {
|
|
|
249
271
|
draggedPosition = viewportPixelsToOrbScreenPosition({
|
|
250
272
|
centerX: left + size / 2,
|
|
251
273
|
centerY: top + size / 2,
|
|
252
|
-
viewportWidth:
|
|
253
|
-
viewportHeight:
|
|
274
|
+
viewportWidth: content.width,
|
|
275
|
+
viewportHeight: content.height,
|
|
254
276
|
orbDiameter: size,
|
|
255
277
|
});
|
|
256
278
|
applyDeployment();
|
|
@@ -658,6 +680,10 @@ export function mountOrb(controller, options = {}) {
|
|
|
658
680
|
const stopWatch = watchPublishedConfig(controller, {
|
|
659
681
|
pollMs: options.appearancePollMs,
|
|
660
682
|
onChange: (config) => {
|
|
683
|
+
// Dashboard master switch: hide the whole orb (button + chip + bubble live
|
|
684
|
+
// inside `root`) when explicitly turned off, show it otherwise. No host
|
|
685
|
+
// code change — it flips on the next poll and reappears when re-enabled.
|
|
686
|
+
root.style.display = config && config.enabled === false ? 'none' : '';
|
|
661
687
|
if (config) {
|
|
662
688
|
publishedAppearance = config.appearance;
|
|
663
689
|
chipTheme = config.keyboardChip;
|
|
@@ -681,6 +707,7 @@ export function mountOrb(controller, options = {}) {
|
|
|
681
707
|
unsubscribe();
|
|
682
708
|
stopWatch();
|
|
683
709
|
window.removeEventListener('resize', reposition);
|
|
710
|
+
contentObserver?.disconnect();
|
|
684
711
|
visualViewport?.removeEventListener('resize', onViewportChange);
|
|
685
712
|
visualViewport?.removeEventListener('scroll', onViewportChange);
|
|
686
713
|
clearOverlay();
|
|
@@ -17,6 +17,12 @@ export interface ResolvedOrbConfig {
|
|
|
17
17
|
keyboardChip: ChipTheme | null;
|
|
18
18
|
modalTheme: PublishedModalTheme | undefined;
|
|
19
19
|
screenPosition: OrbScreenPosition | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Dashboard master visibility switch (`orbEnabled`). False ONLY when the
|
|
22
|
+
* developer explicitly turned the orb off. The orb is hidden when false and
|
|
23
|
+
* shown otherwise (including when nothing is published / on error).
|
|
24
|
+
*/
|
|
25
|
+
enabled: boolean;
|
|
20
26
|
}
|
|
21
27
|
export interface WatchPublishedConfigOptions {
|
|
22
28
|
/** Poll interval (ms) to pick up dashboard changes in real time. Default 12000. */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { mapPublishedConfigToOrbProps, } from '@fiodos/core';
|
|
13
13
|
import { DEFAULT_ORB_APPEARANCE } from './orbView.js';
|
|
14
|
-
function toResolved(props) {
|
|
14
|
+
function toResolved(props, enabled) {
|
|
15
15
|
const theme = props.theme;
|
|
16
16
|
const appearance = {
|
|
17
17
|
accentColor: theme.accentColor ?? DEFAULT_ORB_APPEARANCE.accentColor,
|
|
@@ -30,6 +30,7 @@ function toResolved(props) {
|
|
|
30
30
|
keyboardChip: theme.keyboardChip ?? null,
|
|
31
31
|
modalTheme: props.modalTheme,
|
|
32
32
|
screenPosition: props.screenPosition,
|
|
33
|
+
enabled,
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
@@ -49,7 +50,9 @@ export function watchPublishedConfig(controller, options) {
|
|
|
49
50
|
const res = await fetcher.call(controller.config.backend);
|
|
50
51
|
if (cancelled)
|
|
51
52
|
return;
|
|
52
|
-
options.onChange(res.published
|
|
53
|
+
options.onChange(res.published
|
|
54
|
+
? toResolved(mapPublishedConfigToOrbProps(res.published), res.published.orbEnabled !== false)
|
|
55
|
+
: null);
|
|
53
56
|
}
|
|
54
57
|
catch {
|
|
55
58
|
if (!cancelled)
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export declare const SDK_VERSION = "0.1.
|
|
8
|
+
export declare const SDK_VERSION = "0.1.3";
|
|
9
9
|
export declare const SDK_PACKAGE = "@fiodos/web-core";
|
package/dist/esm/version.js
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export const SDK_VERSION = '0.1.
|
|
8
|
+
export const SDK_VERSION = '0.1.3';
|
|
9
9
|
export const SDK_PACKAGE = '@fiodos/web-core';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/web-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Framework-agnostic browser layer for the Fiodos agent: a vanilla AgentController (orchestrator), DOM adapters (navigation/voice/storage), HTTP client and decision engine over @fiodos/core. Shared base for @fiodos/vue, @fiodos/svelte and @fiodos/angular (no framework imports).",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": {
|