symbiote-ui 0.3.0-alpha.13 → 0.3.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -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/chat/voice-settings.js +149 -0
- package/cli.js +12 -1
- package/core/Node.js +1 -1
- package/custom-elements.json +3070 -4085
- package/discover.js +25 -0
- package/display/CodeBlock/CodeBlock.js +20 -0
- package/display/SourceEditor/SourceEditor.js +58 -0
- package/display/SourceViewer/SourceViewer.js +66 -15
- package/display/SourceViewer/SourceViewer.tpl.js +4 -0
- package/display/highlight.js +48 -2
- package/display/source-contract.js +117 -0
- package/docs/agent-ui-principles.md +5 -0
- package/effects/CellBg/CellBg.js +29 -0
- package/index.js +18 -0
- package/layout/LayoutTree.js +18 -0
- package/layout/index.js +7 -0
- package/layout/lifecycle.js +68 -0
- package/manifest/component-registry.js +157 -15
- package/package.json +9 -2
- package/ui/index.js +29 -1
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.
|
|
@@ -154,6 +155,61 @@ let theme = createCascadeTheme({ mode: 'dark', brightness: 8, contrast: 64 });
|
|
|
154
155
|
applyCascadeTheme(document.documentElement, theme.state);
|
|
155
156
|
```
|
|
156
157
|
|
|
158
|
+
Use `cascade-theme-widget` for shell-level quick controls and
|
|
159
|
+
`cascade-theme-editor` for the full layout panel. Keep both components on the
|
|
160
|
+
same `storage-key` and `target-selector`; the widget emits
|
|
161
|
+
`cascade-theme-open-full`, and the host opens a registered `panel-layout`
|
|
162
|
+
panel:
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<section id="app-shell">
|
|
166
|
+
<header>
|
|
167
|
+
<cascade-theme-widget
|
|
168
|
+
storage-key="my-app:cascade-theme"
|
|
169
|
+
target-selector="#app-shell"
|
|
170
|
+
></cascade-theme-widget>
|
|
171
|
+
</header>
|
|
172
|
+
<panel-layout id="workspace-layout"></panel-layout>
|
|
173
|
+
</section>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { defineModule } from 'symbiote-ui/ui';
|
|
178
|
+
|
|
179
|
+
defineModule('cascade-theme-widget');
|
|
180
|
+
defineModule('cascade-theme-editor');
|
|
181
|
+
defineModule('panel-layout');
|
|
182
|
+
|
|
183
|
+
class AppThemePanel extends HTMLElement {
|
|
184
|
+
connectedCallback() {
|
|
185
|
+
if (this.ready) return;
|
|
186
|
+
this.ready = true;
|
|
187
|
+
this.innerHTML = `
|
|
188
|
+
<cascade-theme-editor
|
|
189
|
+
storage-key="my-app:cascade-theme"
|
|
190
|
+
target-selector="#app-shell"
|
|
191
|
+
></cascade-theme-editor>
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
customElements.define('app-theme-panel', AppThemePanel);
|
|
197
|
+
|
|
198
|
+
let layout = document.querySelector('#workspace-layout');
|
|
199
|
+
layout.registerPanelType('theme-editor', {
|
|
200
|
+
title: 'Theme',
|
|
201
|
+
icon: 'palette',
|
|
202
|
+
component: 'app-theme-panel',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
document.addEventListener('cascade-theme-open-full', () => {
|
|
206
|
+
layout.openPanel('theme-editor', {
|
|
207
|
+
uiInvoked: true,
|
|
208
|
+
source: 'cascade-theme-widget',
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
157
213
|
For chat construction, use `chat-workspace` when the host needs the complete
|
|
158
214
|
reusable shell: sidebar, transcript, composer, footer controls, voice-control
|
|
159
215
|
intents, and animated `cell-bg` lifecycle. `chat-sidebar-shell` and
|
|
@@ -179,7 +235,12 @@ chunks and dispatches a `chat-composer-audio-captured` event with the resulting
|
|
|
179
235
|
raw audio blob so the host can process or transcribe it. Use
|
|
180
236
|
`symbiote-ui/chat/voice-input-defaults.js` for shared wake/send/cancel/delete/off
|
|
181
237
|
phrases, command parsing, and matching when hosts need Agent Portal-compatible
|
|
182
|
-
voice command behavior. `
|
|
238
|
+
voice command behavior. `VoiceController` in
|
|
239
|
+
`symbiote-ui/chat/voice-controller.js` coordinates browser wake listening,
|
|
240
|
+
pause/resume while recording or speaking, wake-command matching, and response
|
|
241
|
+
speech lifecycle while leaving settings, transcription providers, persistence,
|
|
242
|
+
and routing to the host. It is also exported through `symbiote-ui/ui` for
|
|
243
|
+
browser hosts. `setFooterHtml()`
|
|
183
244
|
remains available only for trusted host-rendered footer markup.
|
|
184
245
|
`extractChatTitleFromAgentText()` provides a product-neutral parser for
|
|
185
246
|
standalone `<chat-title>...</chat-title>` responses; any prompt instruction that
|
|
@@ -327,8 +388,10 @@ agents that compose dynamic workspaces:
|
|
|
327
388
|
```js
|
|
328
389
|
import {
|
|
329
390
|
LayoutTree,
|
|
391
|
+
resumeLayoutSubtree,
|
|
330
392
|
resolveLayoutMinSize,
|
|
331
393
|
resolveResponsiveLayoutState,
|
|
394
|
+
suspendLayoutSubtree,
|
|
332
395
|
} from 'symbiote-ui/layout';
|
|
333
396
|
|
|
334
397
|
let root = LayoutTree.createSplit(
|
|
@@ -343,6 +406,9 @@ let state = resolveResponsiveLayoutState(
|
|
|
343
406
|
{ collapse: 'auto', overflow: 'scroll-inline', responsiveMode: 'scroll-inline' },
|
|
344
407
|
{ inlineSize: 520, blockSize: 420, layoutMinSize: minSize }
|
|
345
408
|
);
|
|
409
|
+
|
|
410
|
+
suspendLayoutSubtree(workspaceEl, { reason: 'workspace-inactive' });
|
|
411
|
+
resumeLayoutSubtree(workspaceEl, { reason: 'workspace-active' });
|
|
346
412
|
```
|
|
347
413
|
|
|
348
414
|
`panel-layout` uses the same contract at runtime. Root `layoutBehavior`
|
|
@@ -361,6 +427,12 @@ smoke tests, and agents can distinguish requested policy from active fallback.
|
|
|
361
427
|
`layout-sidebar` owns only its sidebar configuration and width persistence; its
|
|
362
428
|
reset control clears that state and emits `layout-sidebar-reset` for host-owned
|
|
363
429
|
layout resets instead of clearing host storage or reloading the page.
|
|
430
|
+
`suspendLayoutSubtree()` and `resumeLayoutSubtree()` call public
|
|
431
|
+
`suspendLayout()`/`resumeLayout()` methods on reusable components and host
|
|
432
|
+
adapters in a hidden layout subtree. `chat-workspace`, `chat-composer`, and
|
|
433
|
+
`cell-bg` implement this contract so hidden workspace groups stop animated
|
|
434
|
+
backgrounds, wake listeners, voice capture, and UI timers without destroying
|
|
435
|
+
host-owned chat, route, or layout state.
|
|
364
436
|
|
|
365
437
|
## Spatial Algorithms
|
|
366
438
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultSendCommandPhrases,
|
|
3
|
+
defaultVoiceActionCommandPhrases,
|
|
4
|
+
defaultWakeCommandPhrases,
|
|
5
|
+
normalizeWakeCommandPhrase,
|
|
6
|
+
parseVoiceCommandList,
|
|
7
|
+
} from './voice-input-defaults.js';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_VOICE_SETTINGS = Object.freeze({
|
|
10
|
+
commandMode: false,
|
|
11
|
+
responseEnabled: false,
|
|
12
|
+
languageMode: 'en',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const VOICE_LOCALES = Object.freeze(['en', 'ru', 'es']);
|
|
16
|
+
const VOICE_ACTIONS = Object.freeze(['cancel', 'delete', 'off']);
|
|
17
|
+
|
|
18
|
+
function asObject(value) {
|
|
19
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeVoiceLanguageMode(mode, fallbackLocale = 'en') {
|
|
23
|
+
let value = String(mode || '').trim().toLowerCase();
|
|
24
|
+
if (['ru', 'es', 'en'].includes(value)) return value;
|
|
25
|
+
return fallbackLocale;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getSafeStorage(customStorage) {
|
|
29
|
+
if (customStorage) return customStorage;
|
|
30
|
+
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
|
|
31
|
+
return window.localStorage;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadVoiceSettings(storage, fallbackLocale = 'en') {
|
|
37
|
+
let safeStorage = getSafeStorage(storage);
|
|
38
|
+
let commandMode = DEFAULT_VOICE_SETTINGS.commandMode;
|
|
39
|
+
let responseEnabled = DEFAULT_VOICE_SETTINGS.responseEnabled;
|
|
40
|
+
let languageMode = normalizeVoiceLanguageMode('', fallbackLocale);
|
|
41
|
+
|
|
42
|
+
if (safeStorage) {
|
|
43
|
+
try {
|
|
44
|
+
let savedCommandMode = safeStorage.getItem('symbiote_voice_command_mode');
|
|
45
|
+
if (savedCommandMode !== null) {
|
|
46
|
+
commandMode = savedCommandMode === 'true';
|
|
47
|
+
}
|
|
48
|
+
let savedResponseEnabled = safeStorage.getItem('symbiote_voice_response_enabled');
|
|
49
|
+
if (savedResponseEnabled !== null) {
|
|
50
|
+
responseEnabled = savedResponseEnabled === 'true';
|
|
51
|
+
}
|
|
52
|
+
let savedLanguageMode = safeStorage.getItem('symbiote_voice_language_mode');
|
|
53
|
+
if (savedLanguageMode !== null) {
|
|
54
|
+
languageMode = normalizeVoiceLanguageMode(savedLanguageMode, fallbackLocale);
|
|
55
|
+
}
|
|
56
|
+
} catch (_) {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
commandMode,
|
|
61
|
+
responseEnabled,
|
|
62
|
+
languageMode,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function saveVoiceSettings(settings, storage) {
|
|
67
|
+
let safeStorage = getSafeStorage(storage);
|
|
68
|
+
if (!safeStorage) return false;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (settings.commandMode !== undefined) {
|
|
72
|
+
safeStorage.setItem('symbiote_voice_command_mode', String(settings.commandMode));
|
|
73
|
+
}
|
|
74
|
+
if (settings.responseEnabled !== undefined) {
|
|
75
|
+
safeStorage.setItem('symbiote_voice_response_enabled', String(settings.responseEnabled));
|
|
76
|
+
}
|
|
77
|
+
if (settings.languageMode !== undefined) {
|
|
78
|
+
safeStorage.setItem('symbiote_voice_language_mode', String(settings.languageMode));
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
} catch (_) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function mergeServerVoiceSettings(local, serverVoiceInput, fallbackLocale = 'en') {
|
|
87
|
+
let result = { ...local };
|
|
88
|
+
if (!serverVoiceInput || typeof serverVoiceInput !== 'object') {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (serverVoiceInput.sendByCommandEnabled !== undefined) {
|
|
93
|
+
result.commandMode = Boolean(serverVoiceInput.sendByCommandEnabled);
|
|
94
|
+
}
|
|
95
|
+
if (serverVoiceInput.voiceResponseEnabled !== undefined) {
|
|
96
|
+
result.responseEnabled = Boolean(serverVoiceInput.voiceResponseEnabled);
|
|
97
|
+
}
|
|
98
|
+
if (serverVoiceInput.languageMode !== undefined) {
|
|
99
|
+
result.languageMode = normalizeVoiceLanguageMode(serverVoiceInput.languageMode, fallbackLocale);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function normalizeVoiceCommandSettings(raw = {}) {
|
|
106
|
+
let data = asObject(raw);
|
|
107
|
+
let sendDefaults = defaultSendCommandPhrases();
|
|
108
|
+
let wakeDefaults = defaultWakeCommandPhrases();
|
|
109
|
+
let actionDefaults = defaultVoiceActionCommandPhrases();
|
|
110
|
+
let savedSend = asObject(data.sendCommands);
|
|
111
|
+
let savedWake = asObject(data.wakeCommands);
|
|
112
|
+
let savedActions = asObject(data.actionCommands);
|
|
113
|
+
let legacySendCommand = String(data.sendCommand || '').trim();
|
|
114
|
+
|
|
115
|
+
let sendCommands = Object.fromEntries(
|
|
116
|
+
VOICE_LOCALES.map((locale) => {
|
|
117
|
+
let value = String(savedSend[locale] || (locale === 'en' ? legacySendCommand : '') || sendDefaults[locale]).trim();
|
|
118
|
+
return [locale, value || sendDefaults[locale]];
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
let wakeCommands = Object.fromEntries(
|
|
123
|
+
VOICE_LOCALES.map((locale) => [
|
|
124
|
+
locale,
|
|
125
|
+
normalizeWakeCommandPhrase(savedWake[locale] || wakeDefaults[locale], locale),
|
|
126
|
+
])
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
let actionCommands = Object.fromEntries(
|
|
130
|
+
VOICE_ACTIONS.map((action) => {
|
|
131
|
+
let values = asObject(savedActions[action]);
|
|
132
|
+
return [
|
|
133
|
+
action,
|
|
134
|
+
Object.fromEntries(
|
|
135
|
+
VOICE_LOCALES.map((locale) => [
|
|
136
|
+
locale,
|
|
137
|
+
parseVoiceCommandList(values[locale], actionDefaults[action][locale]),
|
|
138
|
+
])
|
|
139
|
+
),
|
|
140
|
+
];
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
sendCommands,
|
|
146
|
+
wakeCommands,
|
|
147
|
+
actionCommands,
|
|
148
|
+
};
|
|
149
|
+
}
|
package/cli.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
import { cmdDiscover } from './discover.js';
|
|
6
7
|
|
|
7
8
|
let command = process.argv[2] || 'discover';
|
|
8
|
-
let
|
|
9
|
+
let modulePath = fileURLToPath(import.meta.url);
|
|
10
|
+
let argvPath = process.argv[1] || '';
|
|
11
|
+
let isMain = modulePath === argvPath;
|
|
12
|
+
|
|
13
|
+
if (!isMain && argvPath) {
|
|
14
|
+
try {
|
|
15
|
+
isMain = modulePath === realpathSync(argvPath);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
void error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
9
20
|
|
|
10
21
|
if (isMain && command === 'discover') {
|
|
11
22
|
let result = await cmdDiscover({});
|
package/core/Node.js
CHANGED
|
@@ -16,7 +16,7 @@ export class Node {
|
|
|
16
16
|
* @param {string} [options.id] - Custom ID (default: auto-generated)
|
|
17
17
|
* @param {string} [options.type] - Node type identifier (e.g. 'ai/llm')
|
|
18
18
|
* @param {string} [options.category] - Category for styling (server/instance/control)
|
|
19
|
-
* @param {string} [options.shape] - Shape name (rect/pill/circle/diamond/comment)
|
|
19
|
+
* @param {string} [options.shape] - Shape name (rect/pill/circle/disc/diamond/comment or registered SVG preset)
|
|
20
20
|
* @param {string} [options.icon] - Material icon name for visual rendering
|
|
21
21
|
*/
|
|
22
22
|
constructor(label, options = {}) {
|