bosun 0.37.1 → 0.37.2
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/.env.example +4 -1
- package/agent-tool-config.mjs +14 -3
- package/bosun-skills.mjs +59 -4
- package/desktop/launch.mjs +18 -0
- package/desktop/main.mjs +52 -13
- package/fleet-coordinator.mjs +34 -1
- package/kanban-adapter.mjs +30 -3
- package/library-manager.mjs +48 -0
- package/maintenance.mjs +30 -5
- package/monitor.mjs +56 -0
- package/package.json +2 -1
- package/setup-web-server.mjs +71 -10
- package/ui/app.js +40 -3
- package/ui/components/session-list.js +25 -7
- package/ui/components/workspace-switcher.js +48 -1
- package/ui/demo.html +110 -0
- package/ui/modules/mic-track-registry.js +83 -0
- package/ui/modules/settings-schema.js +3 -0
- package/ui/modules/state.js +25 -0
- package/ui/modules/streaming.js +1 -1
- package/ui/modules/voice-barge-in.js +27 -0
- package/ui/modules/voice-client-sdk.js +260 -38
- package/ui/modules/voice-client.js +662 -58
- package/ui/modules/voice-overlay.js +829 -47
- package/ui/setup.html +151 -9
- package/ui/styles.css +258 -0
- package/ui/tabs/chat.js +11 -0
- package/ui/tabs/library.js +219 -9
- package/ui/tabs/settings.js +51 -11
- package/ui/tabs/telemetry.js +327 -105
- package/ui/tabs/workflows.js +22 -5
- package/ui-server.mjs +961 -103
- package/voice-relay.mjs +119 -11
- package/workflow-engine.mjs +54 -0
- package/workflow-nodes.mjs +177 -28
- package/workflow-templates/github.mjs +205 -94
- package/workflow-templates/task-batch.mjs +247 -0
- package/workflow-templates.mjs +15 -0
package/ui/tabs/library.js
CHANGED
|
@@ -349,12 +349,12 @@ async function configureMcpEnv(id, env) {
|
|
|
349
349
|
|
|
350
350
|
async function fetchAvailableTools() {
|
|
351
351
|
const res = await apiFetch("/api/agent-tools/available");
|
|
352
|
-
return res?.data || { builtinTools: [], mcpServers: [] };
|
|
352
|
+
return res?.data || { builtinTools: [], bosunTools: [], mcpServers: [] };
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
async function fetchAgentToolConfig(agentId) {
|
|
356
356
|
const res = await apiFetch(`/api/agent-tools/config?agentId=${encodeURIComponent(agentId)}`);
|
|
357
|
-
return res?.data || { builtinTools: [], mcpServers: [] };
|
|
357
|
+
return res?.data || { builtinTools: [], bosunTools: [], mcpServers: [], enabledTools: null };
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
async function saveAgentToolConfig(agentId, config) {
|
|
@@ -377,6 +377,73 @@ async function fetchBuiltinToolDefaults() {
|
|
|
377
377
|
const TYPE_ICONS = { prompt: ":edit:", agent: ":bot:", skill: ":cpu:", mcp: ":plug:" };
|
|
378
378
|
const TYPE_LABELS = { prompt: "Prompt", agent: "Agent Profile", skill: "Skill", mcp: "MCP Server" };
|
|
379
379
|
const TYPE_COLORS = { prompt: "#58a6ff", agent: "#af7bff", skill: "#3fb950", mcp: "#f59e0b" };
|
|
380
|
+
const AGENT_TYPE_OPTIONS = Object.freeze([
|
|
381
|
+
{ value: "voice", label: "Voice" },
|
|
382
|
+
{ value: "task", label: "Task" },
|
|
383
|
+
{ value: "chat", label: "Chat" },
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
function normalizeAgentType(rawType) {
|
|
387
|
+
const value = String(rawType || "").trim().toLowerCase();
|
|
388
|
+
if (value === "voice" || value === "task" || value === "chat") return value;
|
|
389
|
+
return "task";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function inferAgentTypeFromEntry(entry, parsedContent) {
|
|
393
|
+
const explicit = normalizeAgentType(parsedContent?.agentType);
|
|
394
|
+
if (parsedContent?.agentType) return explicit;
|
|
395
|
+
if (parsedContent?.voiceAgent === true) return "voice";
|
|
396
|
+
const id = String(entry?.id || "").trim().toLowerCase();
|
|
397
|
+
const tags = Array.isArray(entry?.tags)
|
|
398
|
+
? entry.tags.map((tag) => String(tag || "").trim().toLowerCase())
|
|
399
|
+
: [];
|
|
400
|
+
if (id.startsWith("voice-agent")) return "voice";
|
|
401
|
+
if (tags.includes("voice") || tags.includes("audio-agent") || tags.includes("realtime")) return "voice";
|
|
402
|
+
return "task";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const AUDIO_AGENT_TEMPLATES = Object.freeze({
|
|
406
|
+
female: {
|
|
407
|
+
type: "agent",
|
|
408
|
+
name: "Voice Agent (Female)",
|
|
409
|
+
description: "Conversational voice specialist with concise guidance and call-friendly pacing.",
|
|
410
|
+
tags: "voice,audio-agent,female,realtime",
|
|
411
|
+
scope: "global",
|
|
412
|
+
content: JSON.stringify({
|
|
413
|
+
name: "Voice Agent (Female)",
|
|
414
|
+
description: "Conversational voice specialist with concise guidance and call-friendly pacing.",
|
|
415
|
+
titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
|
|
416
|
+
scopes: ["voice", "assistant"],
|
|
417
|
+
model: null,
|
|
418
|
+
promptOverride: null,
|
|
419
|
+
skills: ["concise-voice-guidance", "conversation-memory"],
|
|
420
|
+
agentType: "voice",
|
|
421
|
+
voiceAgent: true,
|
|
422
|
+
voicePersona: "female",
|
|
423
|
+
voiceInstructions: "You are Nova, a female voice agent. Be concise, warm, and practical. Use tools for facts and execution. Keep spoken responses short and clear.",
|
|
424
|
+
}, null, 2),
|
|
425
|
+
},
|
|
426
|
+
male: {
|
|
427
|
+
type: "agent",
|
|
428
|
+
name: "Voice Agent (Male)",
|
|
429
|
+
description: "Operational voice specialist focused on diagnostics and execution.",
|
|
430
|
+
tags: "voice,audio-agent,male,realtime",
|
|
431
|
+
scope: "global",
|
|
432
|
+
content: JSON.stringify({
|
|
433
|
+
name: "Voice Agent (Male)",
|
|
434
|
+
description: "Operational voice specialist focused on diagnostics and execution.",
|
|
435
|
+
titlePatterns: ["\\bvoice\\b", "\\bcall\\b", "\\bmeeting\\b", "\\bassistant\\b"],
|
|
436
|
+
scopes: ["voice", "assistant"],
|
|
437
|
+
model: null,
|
|
438
|
+
promptOverride: null,
|
|
439
|
+
skills: ["ops-diagnostics", "task-execution"],
|
|
440
|
+
agentType: "voice",
|
|
441
|
+
voiceAgent: true,
|
|
442
|
+
voicePersona: "male",
|
|
443
|
+
voiceInstructions: "You are Atlas, a male voice agent. Be direct and execution-oriented. Prefer actionable status updates. Use tools proactively for diagnostics.",
|
|
444
|
+
}, null, 2),
|
|
445
|
+
},
|
|
446
|
+
});
|
|
380
447
|
|
|
381
448
|
/* ═══════════════════════════════════════════════════════════════
|
|
382
449
|
* Sub-components
|
|
@@ -454,6 +521,9 @@ function LibraryCard({ entry, onSelect }) {
|
|
|
454
521
|
<div class="library-card-desc">${entry.description}</div>
|
|
455
522
|
`}
|
|
456
523
|
<div class="library-card-meta">
|
|
524
|
+
${entry.type === "agent" && entry.agentType && html`
|
|
525
|
+
<span class="library-card-tag">${String(entry.agentType).toUpperCase()}</span>
|
|
526
|
+
`}
|
|
457
527
|
${(entry.tags || []).slice(0, 5).map((tag) => html`
|
|
458
528
|
<span class="library-card-tag" key=${tag}>${tag}</span>
|
|
459
529
|
`)}
|
|
@@ -476,7 +546,8 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
476
546
|
description: entry?.description || "",
|
|
477
547
|
tags: (entry?.tags || []).join(", "),
|
|
478
548
|
scope: entry?.scope || "global",
|
|
479
|
-
|
|
549
|
+
agentType: inferAgentTypeFromEntry(entry, null),
|
|
550
|
+
content: typeof entry?.content === "string" ? entry.content : "",
|
|
480
551
|
};
|
|
481
552
|
const [form, setForm] = useState(initialFormSnapshot);
|
|
482
553
|
const [baseline, setBaseline] = useState(initialFormSnapshot);
|
|
@@ -496,6 +567,7 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
496
567
|
description: entry?.description || "",
|
|
497
568
|
tags: (entry?.tags || []).join(", "),
|
|
498
569
|
scope: entry?.scope || "global",
|
|
570
|
+
agentType: inferAgentTypeFromEntry(entry, null),
|
|
499
571
|
content: "",
|
|
500
572
|
};
|
|
501
573
|
setForm(next);
|
|
@@ -513,8 +585,13 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
513
585
|
if (cancelled) return;
|
|
514
586
|
let contentStr = detail?.content ?? "";
|
|
515
587
|
if (typeof contentStr === "object") contentStr = JSON.stringify(contentStr, null, 2);
|
|
588
|
+
const parsed = detail?.content && typeof detail.content === "object" ? detail.content : null;
|
|
516
589
|
setForm((f) => {
|
|
517
|
-
const next = {
|
|
590
|
+
const next = {
|
|
591
|
+
...f,
|
|
592
|
+
content: contentStr,
|
|
593
|
+
agentType: inferAgentTypeFromEntry(detail || entry, parsed),
|
|
594
|
+
};
|
|
518
595
|
setBaseline(next);
|
|
519
596
|
return next;
|
|
520
597
|
});
|
|
@@ -551,7 +628,19 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
551
628
|
const tags = form.tags.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
552
629
|
let content = form.content;
|
|
553
630
|
if (form.type === "agent") {
|
|
554
|
-
try {
|
|
631
|
+
try {
|
|
632
|
+
content = JSON.parse(content);
|
|
633
|
+
} catch {
|
|
634
|
+
showToast("Agent profile content must be valid JSON", "error");
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
const agentType = normalizeAgentType(form.agentType);
|
|
638
|
+
content.agentType = agentType;
|
|
639
|
+
if (agentType === "voice") {
|
|
640
|
+
content.voiceAgent = true;
|
|
641
|
+
} else if (content.voiceAgent === true) {
|
|
642
|
+
content.voiceAgent = false;
|
|
643
|
+
}
|
|
555
644
|
}
|
|
556
645
|
const res = await saveEntry({
|
|
557
646
|
id: form.id || undefined,
|
|
@@ -611,6 +700,7 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
611
700
|
model: null,
|
|
612
701
|
promptOverride: null,
|
|
613
702
|
skills: [],
|
|
703
|
+
agentType: "task",
|
|
614
704
|
tags: [],
|
|
615
705
|
}, null, 2)
|
|
616
706
|
: "# Skill Title\n\n## Purpose\nDescribe what this skill teaches agents.\n\n## Instructions\n...";
|
|
@@ -661,6 +751,16 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
661
751
|
<option value="workspace">Workspace</option>
|
|
662
752
|
</select>
|
|
663
753
|
</label>
|
|
754
|
+
${form.type === "agent" && html`
|
|
755
|
+
<label>
|
|
756
|
+
Agent Type
|
|
757
|
+
<select value=${normalizeAgentType(form.agentType)} onChange=${updateField("agentType")}>
|
|
758
|
+
${AGENT_TYPE_OPTIONS.map((opt) => html`
|
|
759
|
+
<option key=${opt.value} value=${opt.value}>${opt.label}</option>
|
|
760
|
+
`)}
|
|
761
|
+
</select>
|
|
762
|
+
</label>
|
|
763
|
+
`}
|
|
664
764
|
<label>
|
|
665
765
|
Content
|
|
666
766
|
${loadingContent
|
|
@@ -672,7 +772,7 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
672
772
|
</label>
|
|
673
773
|
<div style="font-size:0.78em;color:var(--text-tertiary,#666);margin-top:-8px;">
|
|
674
774
|
${form.type === "prompt" ? "Use {{VARIABLE_NAME}} for template variables. Reference in workflows as {{prompt:name}}."
|
|
675
|
-
|
|
775
|
+
: form.type === "agent" ? "JSON format. Referenced in workflows as {{agent:name}}."
|
|
676
776
|
: form.type === "mcp" ? "MCP server configuration. Managed via the MCP Servers panel."
|
|
677
777
|
: "Markdown format. Referenced in workflows as {{skill:name}}."}
|
|
678
778
|
</div>
|
|
@@ -727,8 +827,8 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
|
|
|
727
827
|
/* ─ Agent Tool Configurator ─────────────────────────────── */
|
|
728
828
|
|
|
729
829
|
function AgentToolConfigurator({ agentId, agentName }) {
|
|
730
|
-
const [toolsTab, setToolsTab] = useState("builtin"); // "builtin" | "mcp"
|
|
731
|
-
const [tools, setTools] = useState({ builtinTools: [], mcpServers: [] });
|
|
830
|
+
const [toolsTab, setToolsTab] = useState("builtin"); // "builtin" | "bosun" | "mcp"
|
|
831
|
+
const [tools, setTools] = useState({ builtinTools: [], bosunTools: [], mcpServers: [], enabledTools: null });
|
|
732
832
|
const [installed, setInstalled] = useState([]);
|
|
733
833
|
const [loading, setLoading] = useState(true);
|
|
734
834
|
const [saving, setSaving] = useState(false);
|
|
@@ -756,11 +856,26 @@ function AgentToolConfigurator({ agentId, agentName }) {
|
|
|
756
856
|
const newDisabled = enabled
|
|
757
857
|
? disabledList.filter((id) => id !== toolId)
|
|
758
858
|
: [...disabledList, toolId];
|
|
859
|
+
const currentEnabledTools = Array.isArray(tools.enabledTools)
|
|
860
|
+
? tools.enabledTools.map((id) => String(id || "").trim()).filter(Boolean)
|
|
861
|
+
: null;
|
|
862
|
+
const nextEnabledTools = currentEnabledTools
|
|
863
|
+
? (() => {
|
|
864
|
+
const set = new Set(currentEnabledTools);
|
|
865
|
+
if (enabled) set.add(toolId);
|
|
866
|
+
else set.delete(toolId);
|
|
867
|
+
return [...set];
|
|
868
|
+
})()
|
|
869
|
+
: undefined;
|
|
759
870
|
setSaving(true);
|
|
760
871
|
try {
|
|
761
|
-
await saveAgentToolConfig(agentId, {
|
|
872
|
+
await saveAgentToolConfig(agentId, {
|
|
873
|
+
disabledBuiltinTools: newDisabled,
|
|
874
|
+
...(nextEnabledTools !== undefined ? { enabledTools: nextEnabledTools } : {}),
|
|
875
|
+
});
|
|
762
876
|
setTools((prev) => ({
|
|
763
877
|
...prev,
|
|
878
|
+
...(nextEnabledTools !== undefined ? { enabledTools: nextEnabledTools } : {}),
|
|
764
879
|
builtinTools: prev.builtinTools.map((t) =>
|
|
765
880
|
t.id === toolId ? { ...t, enabled } : t
|
|
766
881
|
),
|
|
@@ -771,6 +886,34 @@ function AgentToolConfigurator({ agentId, agentName }) {
|
|
|
771
886
|
setSaving(false);
|
|
772
887
|
}, [agentId, tools]);
|
|
773
888
|
|
|
889
|
+
const toggleBosunTool = useCallback(async (toolId, enabled) => {
|
|
890
|
+
const bosunIds = (tools.bosunTools || []).map((tool) => String(tool?.id || "").trim()).filter(Boolean);
|
|
891
|
+
const bosunIdSet = new Set(bosunIds);
|
|
892
|
+
const currentEnabledTools = Array.isArray(tools.enabledTools)
|
|
893
|
+
? tools.enabledTools.map((id) => String(id || "").trim()).filter(Boolean)
|
|
894
|
+
: [];
|
|
895
|
+
const currentSet = new Set(currentEnabledTools);
|
|
896
|
+
const hasBosunAllowlist = bosunIds.some((id) => currentSet.has(id));
|
|
897
|
+
const nextBosunSet = hasBosunAllowlist
|
|
898
|
+
? new Set(currentEnabledTools.filter((id) => bosunIdSet.has(id)))
|
|
899
|
+
: new Set(bosunIds);
|
|
900
|
+
if (enabled) nextBosunSet.add(toolId);
|
|
901
|
+
else nextBosunSet.delete(toolId);
|
|
902
|
+
const preserved = currentEnabledTools.filter((id) => !bosunIdSet.has(id));
|
|
903
|
+
const nextEnabledTools = [...new Set([...preserved, ...nextBosunSet])];
|
|
904
|
+
setSaving(true);
|
|
905
|
+
try {
|
|
906
|
+
await saveAgentToolConfig(agentId, { enabledTools: nextEnabledTools });
|
|
907
|
+
setTools((prev) => ({
|
|
908
|
+
...prev,
|
|
909
|
+
enabledTools: nextEnabledTools,
|
|
910
|
+
}));
|
|
911
|
+
} catch (err) {
|
|
912
|
+
showToast("Failed to save: " + err.message, "error");
|
|
913
|
+
}
|
|
914
|
+
setSaving(false);
|
|
915
|
+
}, [agentId, tools]);
|
|
916
|
+
|
|
774
917
|
const toggleMcpServer = useCallback(async (serverId, enabled) => {
|
|
775
918
|
const currentMcp = tools.mcpServers || [];
|
|
776
919
|
const newMcp = enabled
|
|
@@ -787,6 +930,17 @@ function AgentToolConfigurator({ agentId, agentName }) {
|
|
|
787
930
|
}, [agentId, tools]);
|
|
788
931
|
|
|
789
932
|
const enabledMcpSet = new Set(tools.mcpServers || []);
|
|
933
|
+
const bosunTools = Array.isArray(tools.bosunTools) ? tools.bosunTools : [];
|
|
934
|
+
const bosunToolIds = bosunTools.map((tool) => String(tool?.id || "").trim()).filter(Boolean);
|
|
935
|
+
const rawEnabledTools = Array.isArray(tools.enabledTools)
|
|
936
|
+
? tools.enabledTools.map((id) => String(id || "").trim()).filter(Boolean)
|
|
937
|
+
: null;
|
|
938
|
+
const hasBosunAllowlist = Boolean(rawEnabledTools && rawEnabledTools.some((id) => bosunToolIds.includes(id)));
|
|
939
|
+
const enabledBosunSet = new Set(
|
|
940
|
+
hasBosunAllowlist
|
|
941
|
+
? rawEnabledTools.filter((id) => bosunToolIds.includes(id))
|
|
942
|
+
: bosunToolIds,
|
|
943
|
+
);
|
|
790
944
|
|
|
791
945
|
if (loading) {
|
|
792
946
|
return html`<div class="agent-tools-section">
|
|
@@ -806,6 +960,10 @@ function AgentToolConfigurator({ agentId, agentName }) {
|
|
|
806
960
|
onClick=${() => setToolsTab("builtin")}>
|
|
807
961
|
${iconText(":cpu: Built-in Tools")} (${(tools.builtinTools || []).filter((t) => t.enabled).length}/${(tools.builtinTools || []).length})
|
|
808
962
|
</button>
|
|
963
|
+
<button class=${`agent-tools-tab ${toolsTab === "bosun" ? "active" : ""}`}
|
|
964
|
+
onClick=${() => setToolsTab("bosun")}>
|
|
965
|
+
${iconText(":zap: Bosun Tools")} (${enabledBosunSet.size}/${bosunTools.length})
|
|
966
|
+
</button>
|
|
809
967
|
<button class=${`agent-tools-tab ${toolsTab === "mcp" ? "active" : ""}`}
|
|
810
968
|
onClick=${() => setToolsTab("mcp")}>
|
|
811
969
|
${iconText(":plug: MCP Servers")} (${enabledMcpSet.size}/${installed.length})
|
|
@@ -832,6 +990,34 @@ function AgentToolConfigurator({ agentId, agentName }) {
|
|
|
832
990
|
</div>
|
|
833
991
|
`}
|
|
834
992
|
|
|
993
|
+
${toolsTab === "bosun" && html`
|
|
994
|
+
<details open class="tool-config-group">
|
|
995
|
+
<summary class="tool-config-group-label">
|
|
996
|
+
Runtime Voice Tools (collapsible)
|
|
997
|
+
</summary>
|
|
998
|
+
${bosunTools.length === 0 && html`
|
|
999
|
+
<div style="padding:12px;text-align:center;color:var(--text-secondary);font-size:0.85em;">
|
|
1000
|
+
No Bosun runtime tools were discovered.
|
|
1001
|
+
</div>
|
|
1002
|
+
`}
|
|
1003
|
+
${bosunTools.map((tool) => html`
|
|
1004
|
+
<div class="tool-config-item" key=${tool.id}>
|
|
1005
|
+
<span class="tool-config-item-icon">${resolveIcon(":zap:") || iconText(":zap:")}</span>
|
|
1006
|
+
<div class="tool-config-item-info">
|
|
1007
|
+
<div class="tool-config-item-name">${tool.name}</div>
|
|
1008
|
+
<div class="tool-config-item-desc">${tool.description || "Bosun runtime tool"}</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
<div class="tool-config-toggle">
|
|
1011
|
+
<${Toggle}
|
|
1012
|
+
checked=${enabledBosunSet.has(tool.id)}
|
|
1013
|
+
onChange=${(val) => toggleBosunTool(tool.id, val)}
|
|
1014
|
+
/>
|
|
1015
|
+
</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
`)}
|
|
1018
|
+
</details>
|
|
1019
|
+
`}
|
|
1020
|
+
|
|
835
1021
|
${toolsTab === "mcp" && html`
|
|
836
1022
|
<div class="tool-config-group">
|
|
837
1023
|
${installed.length === 0 && html`
|
|
@@ -1350,6 +1536,17 @@ export function LibraryTab() {
|
|
|
1350
1536
|
|
|
1351
1537
|
useEffect(() => { loadEntries(); }, [filterType.value]);
|
|
1352
1538
|
|
|
1539
|
+
useEffect(() => {
|
|
1540
|
+
const onWorkspaceSwitched = () => {
|
|
1541
|
+
setEditing(null);
|
|
1542
|
+
loadEntries();
|
|
1543
|
+
};
|
|
1544
|
+
window.addEventListener("ve:workspace-switched", onWorkspaceSwitched);
|
|
1545
|
+
return () => {
|
|
1546
|
+
window.removeEventListener("ve:workspace-switched", onWorkspaceSwitched);
|
|
1547
|
+
};
|
|
1548
|
+
}, [loadEntries]);
|
|
1549
|
+
|
|
1353
1550
|
// Debounced search
|
|
1354
1551
|
const searchTimer = useRef(null);
|
|
1355
1552
|
const handleSearch = useCallback((value) => {
|
|
@@ -1391,6 +1588,13 @@ export function LibraryTab() {
|
|
|
1391
1588
|
setEditing(entry);
|
|
1392
1589
|
}, []);
|
|
1393
1590
|
|
|
1591
|
+
const handleCreateAudioAgent = useCallback((templateKey) => {
|
|
1592
|
+
const template = AUDIO_AGENT_TEMPLATES[templateKey];
|
|
1593
|
+
if (!template) return;
|
|
1594
|
+
haptic("light");
|
|
1595
|
+
setEditing({ ...template });
|
|
1596
|
+
}, []);
|
|
1597
|
+
|
|
1394
1598
|
const handleSaved = useCallback(() => {
|
|
1395
1599
|
setEditing(null);
|
|
1396
1600
|
loadEntries();
|
|
@@ -1414,6 +1618,12 @@ export function LibraryTab() {
|
|
|
1414
1618
|
<div class="library-root">
|
|
1415
1619
|
<div class="library-header">
|
|
1416
1620
|
<h2>${iconText(":book: Library")}</h2>
|
|
1621
|
+
<button class="library-type-pill" onClick=${() => handleCreateAudioAgent("female")}>
|
|
1622
|
+
${iconText(":mic: New Female Audio Agent")}
|
|
1623
|
+
</button>
|
|
1624
|
+
<button class="library-type-pill" onClick=${() => handleCreateAudioAgent("male")}>
|
|
1625
|
+
${iconText(":mic: New Male Audio Agent")}
|
|
1626
|
+
</button>
|
|
1417
1627
|
<button class="library-type-pill" onClick=${handleRebuild}
|
|
1418
1628
|
title="Rescan directories and rebuild manifest">
|
|
1419
1629
|
${iconText(":refresh: Rebuild")}
|
package/ui/tabs/settings.js
CHANGED
|
@@ -2114,25 +2114,36 @@ function VoiceEndpointsEditor() {
|
|
|
2114
2114
|
];
|
|
2115
2115
|
}, []);
|
|
2116
2116
|
|
|
2117
|
-
const normalizeEp = useCallback((ep = {}, idx = 0) =>
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2117
|
+
const normalizeEp = useCallback((ep = {}, idx = 0) => {
|
|
2118
|
+
const provider = ["azure", "openai", "claude", "gemini", "custom"].includes(ep.provider)
|
|
2119
|
+
? ep.provider
|
|
2120
|
+
: "azure";
|
|
2121
|
+
const transcriptionEnabled = ep.transcriptionEnabled == null
|
|
2122
|
+
? provider !== "azure"
|
|
2123
|
+
: ep.transcriptionEnabled !== false;
|
|
2124
|
+
return {
|
|
2125
|
+
_id: ep._id ?? `ep-${idx}-${Date.now()}`,
|
|
2126
|
+
name: String(ep.name || `endpoint-${idx + 1}`),
|
|
2127
|
+
provider,
|
|
2121
2128
|
endpoint: (() => {
|
|
2122
|
-
const p = ["azure", "openai", "claude", "gemini", "custom"].includes(ep.provider) ? ep.provider : "azure";
|
|
2123
2129
|
const raw = String(ep.endpoint || "");
|
|
2124
|
-
return (
|
|
2130
|
+
return (provider === "azure" || provider === "custom")
|
|
2131
|
+
? raw
|
|
2132
|
+
: (raw || getDefaultEndpointUrl(provider, ep.authSource));
|
|
2125
2133
|
})(),
|
|
2126
2134
|
deployment: String(ep.deployment || ""),
|
|
2127
2135
|
model: String(ep.model || ""),
|
|
2128
2136
|
visionModel: String(ep.visionModel || ""),
|
|
2137
|
+
transcriptionModel: String(ep.transcriptionModel || ""),
|
|
2138
|
+
transcriptionEnabled,
|
|
2129
2139
|
apiKey: String(ep.apiKey || ""),
|
|
2130
2140
|
voiceId: String(ep.voiceId || ""),
|
|
2131
2141
|
role: ["primary", "backup"].includes(ep.role) ? ep.role : "primary",
|
|
2132
2142
|
weight: Number(ep.weight) > 0 ? Number(ep.weight) : 1,
|
|
2133
2143
|
enabled: ep.enabled !== false,
|
|
2134
2144
|
authSource: ["apiKey", "oauth"].includes(ep.authSource) ? ep.authSource : "apiKey",
|
|
2135
|
-
|
|
2145
|
+
};
|
|
2146
|
+
}, [getDefaultEndpointUrl]);
|
|
2136
2147
|
|
|
2137
2148
|
// Fetch OAuth status for all providers
|
|
2138
2149
|
const fetchOAuthStatuses = useCallback(async () => {
|
|
@@ -2359,12 +2370,20 @@ function VoiceEndpointsEditor() {
|
|
|
2359
2370
|
onInput=${(e) => updateEndpoint(ep._id, "endpoint", e.target.value)} />
|
|
2360
2371
|
</div>
|
|
2361
2372
|
<div style="grid-column:1/-1">
|
|
2362
|
-
<div class="setting-row-label">
|
|
2363
|
-
<input type="text" value=${ep.deployment} placeholder="gpt-realtime
|
|
2373
|
+
<div class="setting-row-label">Deployment Name</div>
|
|
2374
|
+
<input type="text" value=${ep.deployment} placeholder="my-gpt-4o-realtime"
|
|
2364
2375
|
onInput=${(e) => updateEndpoint(ep._id, "deployment", e.target.value)} />
|
|
2365
2376
|
<div class="meta-text" style="margin-top:3px">
|
|
2366
|
-
|
|
2367
|
-
|
|
2377
|
+
The deployment name from Azure AI Foundry (not the model name).
|
|
2378
|
+
Find it under your resource → Deployments. Leave empty to test credentials only.
|
|
2379
|
+
</div>
|
|
2380
|
+
</div>
|
|
2381
|
+
<div style="grid-column:1/-1">
|
|
2382
|
+
<div class="setting-row-label">Audio Model (Realtime)</div>
|
|
2383
|
+
<input type="text" value=${ep.model} placeholder="gpt-4o-realtime-preview"
|
|
2384
|
+
onInput=${(e) => updateEndpoint(ep._id, "model", e.target.value)} />
|
|
2385
|
+
<div class="meta-text" style="margin-top:3px">
|
|
2386
|
+
The underlying model name (e.g. gpt-4o-realtime-preview). Used at runtime.
|
|
2368
2387
|
</div>
|
|
2369
2388
|
</div>
|
|
2370
2389
|
`}
|
|
@@ -2451,6 +2470,27 @@ function VoiceEndpointsEditor() {
|
|
|
2451
2470
|
onInput=${(e) => updateEndpoint(ep._id, "visionModel", e.target.value)} />
|
|
2452
2471
|
<div class="meta-text" style="margin-top:3px">Model used for screenshot / image analysis tasks.</div>
|
|
2453
2472
|
</div>
|
|
2473
|
+
${(ep.provider === "openai" || ep.provider === "azure") && html`
|
|
2474
|
+
<div style="grid-column:1/-1;display:grid;grid-template-columns:1fr auto;gap:8px;align-items:end">
|
|
2475
|
+
<div>
|
|
2476
|
+
<div class="setting-row-label">Transcription Model</div>
|
|
2477
|
+
<input type="text" value=${ep.transcriptionModel || ""}
|
|
2478
|
+
placeholder="gpt-4o-transcribe"
|
|
2479
|
+
onInput=${(e) => updateEndpoint(ep._id, "transcriptionModel", e.target.value)} />
|
|
2480
|
+
<div class="meta-text" style="margin-top:3px">
|
|
2481
|
+
Model used for input audio transcription. Leave blank for default (gpt-4o-transcribe).
|
|
2482
|
+
${ep.provider === "azure" ? " Azure endpoints default transcription OFF unless enabled." : ""}
|
|
2483
|
+
</div>
|
|
2484
|
+
</div>
|
|
2485
|
+
<div style="display:flex;align-items:center;gap:6px;padding-bottom:22px">
|
|
2486
|
+
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px">
|
|
2487
|
+
<input type="checkbox" checked=${ep.transcriptionEnabled !== false}
|
|
2488
|
+
onChange=${(e) => updateEndpoint(ep._id, "transcriptionEnabled", e.target.checked)} />
|
|
2489
|
+
Enable
|
|
2490
|
+
</label>
|
|
2491
|
+
</div>
|
|
2492
|
+
</div>
|
|
2493
|
+
`}
|
|
2454
2494
|
</div>
|
|
2455
2495
|
<!-- Test Connection -->
|
|
2456
2496
|
<div style="display:flex;align-items:center;gap:10px;margin-top:8px">
|