symbiote-ui 0.3.0-alpha.13 → 0.3.0-alpha.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.
- package/README.md +19 -2
- package/chat/ChatComposer/ChatComposer.js +23 -0
- package/chat/ChatWorkspace/ChatWorkspace.js +13 -0
- package/chat/chat-title.js +1 -1
- package/chat/voice-controller.js +196 -0
- package/custom-elements.json +2461 -4045
- package/discover.js +25 -0
- package/docs/agent-ui-principles.md +5 -0
- package/effects/CellBg/CellBg.js +29 -0
- package/index.js +9 -0
- package/layout/index.js +5 -0
- package/layout/lifecycle.js +68 -0
- package/manifest/component-registry.js +11 -2
- package/package.json +8 -2
- package/ui/index.js +15 -0
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ npm install jsda-kit linkedom
|
|
|
25
25
|
|
|
26
26
|
- `symbiote-ui` - Node-safe core primitives.
|
|
27
27
|
- `symbiote-ui/core` - graph editor data primitives.
|
|
28
|
-
- `symbiote-ui/layout` - SSR-safe layout helpers.
|
|
28
|
+
- `symbiote-ui/layout` - SSR-safe layout, behavior, and lifecycle helpers.
|
|
29
29
|
- `symbiote-ui/graph` - provider graph normalization and projection helpers.
|
|
30
30
|
- `symbiote-ui/manifest` - component, schema, rule, theme, and provider catalogs.
|
|
31
31
|
- `symbiote-ui/runtime` - Node-safe agent UI construction helpers.
|
|
@@ -35,6 +35,7 @@ npm install jsda-kit linkedom
|
|
|
35
35
|
- `symbiote-ui/locale` - Node-safe locale catalogs and translation helpers.
|
|
36
36
|
- `symbiote-ui/discover` - provider discovery JSON API used by the CLI.
|
|
37
37
|
- `symbiote-ui/chat/voice-input-defaults.js` - Node-safe wake/send/action voice command defaults and matching helpers.
|
|
38
|
+
- `symbiote-ui/chat/voice-controller.js` - browser wake listening and response speech orchestration for chat hosts.
|
|
38
39
|
- `symbiote-ui/custom-elements.json` - Custom Elements manifest.
|
|
39
40
|
- `symbiote-ui/schemas/*`, `symbiote-ui/tokens/*`, `symbiote-ui/rules/*` - machine-readable provider contracts.
|
|
40
41
|
- `symbiote-ui/display/*` - reusable display utilities exposed by package export map.
|
|
@@ -179,7 +180,12 @@ chunks and dispatches a `chat-composer-audio-captured` event with the resulting
|
|
|
179
180
|
raw audio blob so the host can process or transcribe it. Use
|
|
180
181
|
`symbiote-ui/chat/voice-input-defaults.js` for shared wake/send/cancel/delete/off
|
|
181
182
|
phrases, command parsing, and matching when hosts need Agent Portal-compatible
|
|
182
|
-
voice command behavior. `
|
|
183
|
+
voice command behavior. `VoiceController` in
|
|
184
|
+
`symbiote-ui/chat/voice-controller.js` coordinates browser wake listening,
|
|
185
|
+
pause/resume while recording or speaking, wake-command matching, and response
|
|
186
|
+
speech lifecycle while leaving settings, transcription providers, persistence,
|
|
187
|
+
and routing to the host. It is also exported through `symbiote-ui/ui` for
|
|
188
|
+
browser hosts. `setFooterHtml()`
|
|
183
189
|
remains available only for trusted host-rendered footer markup.
|
|
184
190
|
`extractChatTitleFromAgentText()` provides a product-neutral parser for
|
|
185
191
|
standalone `<chat-title>...</chat-title>` responses; any prompt instruction that
|
|
@@ -327,8 +333,10 @@ agents that compose dynamic workspaces:
|
|
|
327
333
|
```js
|
|
328
334
|
import {
|
|
329
335
|
LayoutTree,
|
|
336
|
+
resumeLayoutSubtree,
|
|
330
337
|
resolveLayoutMinSize,
|
|
331
338
|
resolveResponsiveLayoutState,
|
|
339
|
+
suspendLayoutSubtree,
|
|
332
340
|
} from 'symbiote-ui/layout';
|
|
333
341
|
|
|
334
342
|
let root = LayoutTree.createSplit(
|
|
@@ -343,6 +351,9 @@ let state = resolveResponsiveLayoutState(
|
|
|
343
351
|
{ collapse: 'auto', overflow: 'scroll-inline', responsiveMode: 'scroll-inline' },
|
|
344
352
|
{ inlineSize: 520, blockSize: 420, layoutMinSize: minSize }
|
|
345
353
|
);
|
|
354
|
+
|
|
355
|
+
suspendLayoutSubtree(workspaceEl, { reason: 'workspace-inactive' });
|
|
356
|
+
resumeLayoutSubtree(workspaceEl, { reason: 'workspace-active' });
|
|
346
357
|
```
|
|
347
358
|
|
|
348
359
|
`panel-layout` uses the same contract at runtime. Root `layoutBehavior`
|
|
@@ -361,6 +372,12 @@ smoke tests, and agents can distinguish requested policy from active fallback.
|
|
|
361
372
|
`layout-sidebar` owns only its sidebar configuration and width persistence; its
|
|
362
373
|
reset control clears that state and emits `layout-sidebar-reset` for host-owned
|
|
363
374
|
layout resets instead of clearing host storage or reloading the page.
|
|
375
|
+
`suspendLayoutSubtree()` and `resumeLayoutSubtree()` call public
|
|
376
|
+
`suspendLayout()`/`resumeLayout()` methods on reusable components and host
|
|
377
|
+
adapters in a hidden layout subtree. `chat-workspace`, `chat-composer`, and
|
|
378
|
+
`cell-bg` implement this contract so hidden workspace groups stop animated
|
|
379
|
+
backgrounds, wake listeners, voice capture, and UI timers without destroying
|
|
380
|
+
host-owned chat, route, or layout state.
|
|
364
381
|
|
|
365
382
|
## Spatial Algorithms
|
|
366
383
|
|
|
@@ -340,6 +340,29 @@ export class ChatComposer extends Symbiote {
|
|
|
340
340
|
super.disconnectedCallback?.();
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
suspendLayout() {
|
|
344
|
+
this._resumeLocalWakeAfterSuspend = this._localVoiceActiveMode === 'wake';
|
|
345
|
+
this._stopLocalWakeRecognition();
|
|
346
|
+
this._voiceRuntime?.cancel?.();
|
|
347
|
+
this._localVoiceState = this._resumeLocalWakeAfterSuspend ? 'listening' : 'idle';
|
|
348
|
+
this._localVoiceActiveMode = this._resumeLocalWakeAfterSuspend ? 'wake' : 'idle';
|
|
349
|
+
this._localVoiceWakeMatched = false;
|
|
350
|
+
this._localWakeTriggering = false;
|
|
351
|
+
this.clearVoicePreview();
|
|
352
|
+
this._syncLocalVoiceControls();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
resumeLayout() {
|
|
356
|
+
if (!this._resumeLocalWakeAfterSuspend) return;
|
|
357
|
+
this._resumeLocalWakeAfterSuspend = false;
|
|
358
|
+
this._localVoiceActiveMode = 'wake';
|
|
359
|
+
this._localVoiceState = 'listening';
|
|
360
|
+
this._localVoiceWakeMatched = false;
|
|
361
|
+
this._localWakeTriggering = false;
|
|
362
|
+
this._syncLocalVoiceControls();
|
|
363
|
+
this._startLocalWakeRecognition();
|
|
364
|
+
}
|
|
365
|
+
|
|
343
366
|
_voiceCommandLocale() {
|
|
344
367
|
let locale = this._voiceControls?.language?.mode || 'en';
|
|
345
368
|
return ['ru', 'es', 'en'].includes(locale) ? locale : 'en';
|
|
@@ -200,6 +200,19 @@ export class ChatWorkspace extends Symbiote {
|
|
|
200
200
|
this.setBackgroundState({ state: 'stop', active: false });
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
suspendLayout() {
|
|
204
|
+
this._layoutSuspendedBackgroundState = this.dataset.backgroundState || '';
|
|
205
|
+
this.getBackground()?.stop?.();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
resumeLayout() {
|
|
209
|
+
let state = this._layoutSuspendedBackgroundState || this.dataset.backgroundState || '';
|
|
210
|
+
this._layoutSuspendedBackgroundState = '';
|
|
211
|
+
if (BACKGROUND_ACTIVE_STATES.has(state)) {
|
|
212
|
+
this.setBackgroundState({ state, active: true, reason: 'layout-resume' });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
203
216
|
_bindWorkspaceEvents() {
|
|
204
217
|
let route = (sourceEvent, eventName, detailFactory = (event) => event.detail || {}) => {
|
|
205
218
|
this.addEventListener(sourceEvent, (event) => {
|
package/chat/chat-title.js
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { matchVoiceCommandInText } from './voice-input-defaults.js';
|
|
2
|
+
|
|
3
|
+
function createWakeError(error, message, cause = null) {
|
|
4
|
+
return { error, message, cause };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class VoiceController {
|
|
8
|
+
constructor({
|
|
9
|
+
getLanguage = () => 'en-US',
|
|
10
|
+
getWakeCandidates = () => [],
|
|
11
|
+
onWakeTriggered = () => {},
|
|
12
|
+
onSpeechStart = () => {},
|
|
13
|
+
onSpeechEnd = () => {},
|
|
14
|
+
onWakeError = () => {},
|
|
15
|
+
} = {}) {
|
|
16
|
+
this.getLanguage = getLanguage;
|
|
17
|
+
this.getWakeCandidates = getWakeCandidates;
|
|
18
|
+
this.onWakeTriggered = onWakeTriggered;
|
|
19
|
+
this.onSpeechStart = onSpeechStart;
|
|
20
|
+
this.onSpeechEnd = onSpeechEnd;
|
|
21
|
+
this.onWakeError = onWakeError;
|
|
22
|
+
|
|
23
|
+
this.wakeEnabled = false;
|
|
24
|
+
this.wakePaused = false;
|
|
25
|
+
this.speaking = false;
|
|
26
|
+
|
|
27
|
+
this._wakeRecognition = null;
|
|
28
|
+
this._speechUtterance = null;
|
|
29
|
+
this._wakeTimeout = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static get hasSpeechRecognition() {
|
|
33
|
+
if (typeof window === 'undefined') return false;
|
|
34
|
+
return Boolean(window.SpeechRecognition || window.webkitSpeechRecognition);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static get hasSpeechSynthesis() {
|
|
38
|
+
if (typeof globalThis === 'undefined') return false;
|
|
39
|
+
return Boolean(globalThis.speechSynthesis && globalThis.SpeechSynthesisUtterance);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get isWakeActive() {
|
|
43
|
+
return this.wakeEnabled && !this.wakePaused;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
startWake() {
|
|
47
|
+
this.wakeEnabled = true;
|
|
48
|
+
this.wakePaused = false;
|
|
49
|
+
this._startWakeRecognition();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
stopWake({ disableMode = false } = {}) {
|
|
53
|
+
if (disableMode) {
|
|
54
|
+
this.wakeEnabled = false;
|
|
55
|
+
this.cancelSpeech();
|
|
56
|
+
}
|
|
57
|
+
this.wakePaused = false;
|
|
58
|
+
this._stopWakeRecognition();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pauseWake() {
|
|
62
|
+
if (!this.wakeEnabled) return;
|
|
63
|
+
this.wakePaused = true;
|
|
64
|
+
this._stopWakeRecognition();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resumeWake() {
|
|
68
|
+
if (!this.wakeEnabled) return;
|
|
69
|
+
this.wakePaused = false;
|
|
70
|
+
this._startWakeRecognition();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_startWakeRecognition() {
|
|
74
|
+
if (!this.wakeEnabled || this.wakePaused || this._wakeRecognition) return;
|
|
75
|
+
if (this._wakeTimeout) {
|
|
76
|
+
clearTimeout(this._wakeTimeout);
|
|
77
|
+
this._wakeTimeout = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const SpeechRecognition = typeof window !== 'undefined'
|
|
81
|
+
? (window.SpeechRecognition || window.webkitSpeechRecognition)
|
|
82
|
+
: null;
|
|
83
|
+
|
|
84
|
+
if (!SpeechRecognition) {
|
|
85
|
+
this.wakeEnabled = false;
|
|
86
|
+
this.onWakeError(createWakeError(
|
|
87
|
+
'not-supported',
|
|
88
|
+
'Speech recognition is not supported by this browser.'
|
|
89
|
+
));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const recognition = new SpeechRecognition();
|
|
95
|
+
recognition.lang = this.getLanguage();
|
|
96
|
+
recognition.interimResults = true;
|
|
97
|
+
recognition.continuous = true;
|
|
98
|
+
this._wakeRecognition = recognition;
|
|
99
|
+
|
|
100
|
+
recognition.onresult = (event) => {
|
|
101
|
+
let transcript = '';
|
|
102
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
103
|
+
transcript += event.results[i][0].transcript;
|
|
104
|
+
}
|
|
105
|
+
const candidates = this.getWakeCandidates();
|
|
106
|
+
if (matchVoiceCommandInText(transcript, candidates).matched) {
|
|
107
|
+
this.onWakeTriggered();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
recognition.onerror = (event) => {
|
|
112
|
+
this._wakeRecognition = null;
|
|
113
|
+
this.onWakeError(event);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
recognition.onend = () => {
|
|
117
|
+
this._wakeRecognition = null;
|
|
118
|
+
if (this.wakeEnabled && !this.wakePaused) {
|
|
119
|
+
this._wakeTimeout = setTimeout(() => this._startWakeRecognition(), 250);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
recognition.start();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
this.wakeEnabled = false;
|
|
126
|
+
this.wakePaused = false;
|
|
127
|
+
this._wakeRecognition = null;
|
|
128
|
+
this.onWakeError(createWakeError(
|
|
129
|
+
'start-failed',
|
|
130
|
+
'Speech recognition failed to start.',
|
|
131
|
+
err
|
|
132
|
+
));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_stopWakeRecognition() {
|
|
137
|
+
if (this._wakeTimeout) {
|
|
138
|
+
clearTimeout(this._wakeTimeout);
|
|
139
|
+
this._wakeTimeout = null;
|
|
140
|
+
}
|
|
141
|
+
if (this._wakeRecognition) {
|
|
142
|
+
const rec = this._wakeRecognition;
|
|
143
|
+
this._wakeRecognition = null;
|
|
144
|
+
rec.onresult = null;
|
|
145
|
+
rec.onerror = null;
|
|
146
|
+
rec.onend = null;
|
|
147
|
+
try {
|
|
148
|
+
rec.abort();
|
|
149
|
+
} catch (_) {}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
speak(text) {
|
|
154
|
+
if (!VoiceController.hasSpeechSynthesis) return;
|
|
155
|
+
this.cancelSpeech();
|
|
156
|
+
|
|
157
|
+
this.pauseWake();
|
|
158
|
+
this.speaking = true;
|
|
159
|
+
this.onSpeechStart();
|
|
160
|
+
|
|
161
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
162
|
+
utterance.lang = this.getLanguage();
|
|
163
|
+
this._speechUtterance = utterance;
|
|
164
|
+
|
|
165
|
+
const cleanup = () => {
|
|
166
|
+
if (this._speechUtterance === utterance) {
|
|
167
|
+
this.speaking = false;
|
|
168
|
+
this._speechUtterance = null;
|
|
169
|
+
this.onSpeechEnd();
|
|
170
|
+
this.resumeWake();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
utterance.onend = cleanup;
|
|
175
|
+
utterance.onerror = cleanup;
|
|
176
|
+
|
|
177
|
+
globalThis.speechSynthesis.cancel();
|
|
178
|
+
globalThis.speechSynthesis.speak(utterance);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
cancelSpeech() {
|
|
182
|
+
if (!VoiceController.hasSpeechSynthesis) return;
|
|
183
|
+
if (this.speaking) {
|
|
184
|
+
globalThis.speechSynthesis.cancel();
|
|
185
|
+
this.speaking = false;
|
|
186
|
+
this._speechUtterance = null;
|
|
187
|
+
this.onSpeechEnd();
|
|
188
|
+
this.resumeWake();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
destroy() {
|
|
193
|
+
this.stopWake({ disableMode: true });
|
|
194
|
+
this.cancelSpeech();
|
|
195
|
+
}
|
|
196
|
+
}
|