openvoiceui 1.0.0
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 +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- package/tts_providers/supertonic_tts.py +399 -0
package/src/admin.html
ADDED
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>OpenUI Admin</title>
|
|
7
|
+
<!-- Clerk Authentication (optional — loaded dynamically if configured) -->
|
|
8
|
+
<script>
|
|
9
|
+
fetch('/api/config').then(r=>r.json()).then(cfg=>{
|
|
10
|
+
if(cfg.clerkPublishableKey){
|
|
11
|
+
const s=document.createElement('script');
|
|
12
|
+
s.async=true;s.crossOrigin='anonymous';
|
|
13
|
+
s.dataset.clerkPublishableKey=cfg.clerkPublishableKey;
|
|
14
|
+
s.src='https://cdn.jsdelivr.net/npm/@clerk/clerk-js@latest/dist/clerk.browser.js';
|
|
15
|
+
document.head.appendChild(s);
|
|
16
|
+
}
|
|
17
|
+
}).catch(()=>{});
|
|
18
|
+
</script>
|
|
19
|
+
<style>
|
|
20
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
21
|
+
:root{
|
|
22
|
+
--bg:#0d1117;--panel:#161b22;--panel2:#1c2128;--panel3:#21262d;
|
|
23
|
+
--border:#30363d;--border2:#21262d;
|
|
24
|
+
--text:#c9d1d9;--dim:#8b949e;--subtle:#484f58;--bright:#e6edf3;
|
|
25
|
+
--blue:#58a6ff;--blue-dark:#1f6feb;--blue-bg:rgba(31,111,235,0.1);
|
|
26
|
+
--green:#3fb950;--green-dark:#238636;--green-bg:rgba(63,185,80,0.1);
|
|
27
|
+
--orange:#ffa657;--orange-bg:rgba(255,166,87,0.1);
|
|
28
|
+
--red:#f85149;--red-bg:rgba(248,81,73,0.1);
|
|
29
|
+
--purple:#d2a8ff;--purple-dark:#8957e5;--purple-bg:rgba(137,87,229,0.1);
|
|
30
|
+
--cyan:#56d4dd;--yellow:#e3b341;
|
|
31
|
+
--sidebar:220px;--header:48px;--radius:6px;--radius-lg:10px;
|
|
32
|
+
--mono:'SF Mono','Fira Code','Courier New',monospace;
|
|
33
|
+
}
|
|
34
|
+
html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:14px;line-height:1.5}
|
|
35
|
+
|
|
36
|
+
/* Layout */
|
|
37
|
+
#app{display:grid;grid-template-rows:var(--header) 1fr;grid-template-columns:var(--sidebar) 1fr;height:100vh;transition:grid-template-columns .2s}
|
|
38
|
+
#app.sidebar-collapsed{grid-template-columns:52px 1fr}
|
|
39
|
+
|
|
40
|
+
/* Header */
|
|
41
|
+
#hdr{grid-column:1/-1;background:var(--panel);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;padding:0 16px;z-index:200}
|
|
42
|
+
.hdr-toggle{background:none;border:none;color:var(--dim);cursor:pointer;padding:6px 8px;border-radius:var(--radius);font-size:16px;line-height:1;transition:color .15s,background .15s}
|
|
43
|
+
.hdr-toggle:hover{color:var(--text);background:var(--panel3)}
|
|
44
|
+
.hdr-logo{font-weight:700;font-size:15px;color:var(--blue);letter-spacing:-.3px;white-space:nowrap}
|
|
45
|
+
.hdr-logo em{color:var(--dim);font-style:normal;font-weight:400}
|
|
46
|
+
.hdr-sep{flex:1}
|
|
47
|
+
.hdr-pill{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dim);background:var(--panel3);border:1px solid var(--border);border-radius:20px;padding:4px 10px}
|
|
48
|
+
.dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 5px var(--green);flex-shrink:0;animation:pulse 2s infinite}
|
|
49
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
|
50
|
+
.dot.red{background:var(--red);box-shadow:0 0 5px var(--red)}
|
|
51
|
+
.dot.yellow{background:var(--yellow);box-shadow:0 0 5px var(--yellow)}
|
|
52
|
+
.hdr-back{display:flex;align-items:center;gap:5px;color:var(--dim);text-decoration:none;font-size:12px;padding:5px 10px;border:1px solid var(--border);border-radius:var(--radius);margin-left:8px;transition:all .15s;white-space:nowrap}
|
|
53
|
+
.hdr-back:hover{color:var(--text);border-color:var(--dim)}
|
|
54
|
+
|
|
55
|
+
/* Sidebar */
|
|
56
|
+
#sidebar{background:var(--panel);border-right:1px solid var(--border);overflow:hidden;display:flex;flex-direction:column;transition:width .2s}
|
|
57
|
+
.nav-section{padding:16px 0 4px}
|
|
58
|
+
.nav-label{font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--subtle);padding:0 12px 6px;white-space:nowrap;transition:opacity .15s}
|
|
59
|
+
#app.sidebar-collapsed .nav-label{opacity:0;pointer-events:none}
|
|
60
|
+
.nav-item{display:flex;align-items:center;gap:10px;padding:7px 12px;cursor:pointer;color:var(--dim);transition:background .1s,color .1s;border-left:2px solid transparent;white-space:nowrap;overflow:hidden;user-select:none;font-size:13px;font-weight:500}
|
|
61
|
+
.nav-item:hover{background:rgba(177,186,196,.06);color:var(--text)}
|
|
62
|
+
.nav-item.active{color:var(--blue);background:var(--blue-bg);border-left-color:var(--blue)}
|
|
63
|
+
.nav-icon{font-size:16px;flex-shrink:0;width:20px;text-align:center}
|
|
64
|
+
.nav-text{transition:opacity .15s}
|
|
65
|
+
#app.sidebar-collapsed .nav-text{opacity:0;width:0;overflow:hidden}
|
|
66
|
+
.sidebar-footer{margin-top:auto;border-top:1px solid var(--border);padding:8px 0}
|
|
67
|
+
|
|
68
|
+
/* Main */
|
|
69
|
+
#main{overflow:hidden;display:flex;flex-direction:column}
|
|
70
|
+
.panel{display:none;flex:1;overflow-y:auto;min-height:0}
|
|
71
|
+
.panel.active{display:flex;flex-direction:column}
|
|
72
|
+
.panel-inner{padding:24px 28px;flex:1}
|
|
73
|
+
|
|
74
|
+
/* Panel header */
|
|
75
|
+
.ph{margin-bottom:20px}
|
|
76
|
+
.ph-title{font-size:20px;font-weight:700;color:var(--bright);letter-spacing:-.4px}
|
|
77
|
+
.ph-sub{font-size:13px;color:var(--dim);margin-top:3px}
|
|
78
|
+
.ph-row{display:flex;align-items:center;gap:10px;margin-bottom:20px}
|
|
79
|
+
|
|
80
|
+
/* Cards / Boxes */
|
|
81
|
+
.box{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden}
|
|
82
|
+
.box-title{font-size:11px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:var(--dim);padding:14px 16px 10px;border-bottom:1px solid var(--border2)}
|
|
83
|
+
.box-body{padding:16px}
|
|
84
|
+
|
|
85
|
+
/* Grid */
|
|
86
|
+
.g2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|
87
|
+
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px}
|
|
88
|
+
.gauto{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}
|
|
89
|
+
.g4{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
|
|
90
|
+
|
|
91
|
+
/* Buttons */
|
|
92
|
+
.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:var(--radius);border:1px solid;cursor:pointer;font-size:13px;font-weight:500;transition:all .15s;white-space:nowrap;font-family:inherit}
|
|
93
|
+
.btn-p{background:var(--blue-dark);border-color:var(--blue-dark);color:#fff}.btn-p:hover{background:#388bfd;border-color:#388bfd}
|
|
94
|
+
.btn-g{background:var(--green-dark);border-color:var(--green-dark);color:#fff}.btn-g:hover{background:#2ea043;border-color:#2ea043}
|
|
95
|
+
.btn-o{background:transparent;border-color:var(--border);color:var(--dim)}.btn-o:hover{background:var(--panel3);border-color:var(--dim);color:var(--text)}
|
|
96
|
+
.btn-d{background:var(--red-bg);border-color:rgba(248,81,73,.4);color:var(--red)}.btn-d:hover{background:rgba(248,81,73,.15)}
|
|
97
|
+
.btn-sm{padding:4px 10px;font-size:12px}
|
|
98
|
+
.btn:disabled{opacity:.4;cursor:not-allowed}
|
|
99
|
+
|
|
100
|
+
/* Form */
|
|
101
|
+
.fg{margin-bottom:14px}
|
|
102
|
+
.fl{font-size:11px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:var(--dim);display:block;margin-bottom:5px}
|
|
103
|
+
input[type=text],input[type=url],input[type=password],textarea,select{
|
|
104
|
+
width:100%;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);
|
|
105
|
+
color:var(--text);font-size:13px;padding:7px 11px;outline:none;transition:border-color .15s;font-family:inherit
|
|
106
|
+
}
|
|
107
|
+
input:focus,textarea:focus,select:focus{border-color:var(--blue-dark);box-shadow:0 0 0 3px rgba(31,111,235,.12)}
|
|
108
|
+
textarea{resize:vertical;min-height:120px}
|
|
109
|
+
select{cursor:pointer}
|
|
110
|
+
|
|
111
|
+
/* Toggle switch */
|
|
112
|
+
.toggle-wrap{display:flex;align-items:center;gap:8px;cursor:pointer}
|
|
113
|
+
.toggle{position:relative;width:36px;height:20px;flex-shrink:0}
|
|
114
|
+
.toggle input{opacity:0;width:0;height:0;position:absolute}
|
|
115
|
+
.toggle-track{position:absolute;inset:0;background:#333;border-radius:10px;transition:background .2s;border:1px solid #444}
|
|
116
|
+
.toggle input:checked~.toggle-track{background:var(--green-dark);border-color:var(--green-dark)}
|
|
117
|
+
.toggle-thumb{position:absolute;width:14px;height:14px;background:#fff;border-radius:50%;top:3px;left:3px;transition:transform .2s;box-shadow:0 1px 3px rgba(0,0,0,.4)}
|
|
118
|
+
.toggle input:checked~.toggle-thumb{transform:translateX(16px)}
|
|
119
|
+
.toggle-label{font-size:13px;color:var(--text)}
|
|
120
|
+
.toggle-sub{font-size:11px;color:var(--dim)}
|
|
121
|
+
|
|
122
|
+
/* Badges */
|
|
123
|
+
.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:12px;font-size:11px;font-weight:600;font-family:var(--mono);white-space:nowrap}
|
|
124
|
+
.b-green{background:var(--green-bg);color:var(--green);border:1px solid rgba(63,185,80,.3)}
|
|
125
|
+
.b-blue{background:var(--blue-bg);color:var(--blue);border:1px solid rgba(88,166,255,.3)}
|
|
126
|
+
.b-orange{background:var(--orange-bg);color:var(--orange);border:1px solid rgba(255,166,87,.3)}
|
|
127
|
+
.b-red{background:var(--red-bg);color:var(--red);border:1px solid rgba(248,81,73,.3)}
|
|
128
|
+
.b-purple{background:var(--purple-bg);color:var(--purple);border:1px solid rgba(137,87,229,.3)}
|
|
129
|
+
.b-gray{background:var(--panel3);color:var(--dim);border:1px solid var(--border)}
|
|
130
|
+
|
|
131
|
+
/* Divider */
|
|
132
|
+
.div{height:1px;background:var(--border);margin:16px 0}
|
|
133
|
+
|
|
134
|
+
/* Loading / Empty */
|
|
135
|
+
.loading{display:flex;align-items:center;gap:8px;color:var(--dim);font-size:13px;padding:20px 0}
|
|
136
|
+
.spin{width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--blue);border-radius:50%;animation:spin .7s linear infinite;flex-shrink:0}
|
|
137
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
138
|
+
.empty{text-align:center;padding:40px;color:var(--dim);font-size:13px}
|
|
139
|
+
.empty-icon{font-size:32px;margin-bottom:10px;opacity:.35}
|
|
140
|
+
|
|
141
|
+
/* Table */
|
|
142
|
+
.tbl{width:100%;border-collapse:collapse}
|
|
143
|
+
.tbl th{padding:9px 14px;text-align:left;font-size:11px;font-weight:700;letter-spacing:.7px;text-transform:uppercase;color:var(--dim);border-bottom:1px solid var(--border);white-space:nowrap}
|
|
144
|
+
.tbl td{padding:11px 14px;border-bottom:1px solid var(--border2);font-size:13px;vertical-align:middle}
|
|
145
|
+
.tbl tr:last-child td{border-bottom:none}
|
|
146
|
+
.tbl tbody tr:hover td{background:rgba(177,186,196,.04)}
|
|
147
|
+
|
|
148
|
+
/* Stat */
|
|
149
|
+
.stat-r{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border2)}
|
|
150
|
+
.stat-r:last-child{border-bottom:none}
|
|
151
|
+
.stat-l{font-size:12px;color:var(--dim);flex:1}
|
|
152
|
+
.stat-v{font-family:var(--mono);font-size:13px;color:var(--text)}
|
|
153
|
+
.stat-v.g{color:var(--green)}.stat-v.r{color:var(--red)}.stat-v.y{color:var(--yellow)}
|
|
154
|
+
|
|
155
|
+
/* Scrollbar */
|
|
156
|
+
::-webkit-scrollbar{width:5px;height:5px}
|
|
157
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
158
|
+
::-webkit-scrollbar-thumb{background:var(--panel3);border-radius:3px}
|
|
159
|
+
::-webkit-scrollbar-thumb:hover{background:var(--border)}
|
|
160
|
+
|
|
161
|
+
/* -- PANEL: Agents -- */
|
|
162
|
+
.active-banner{background:linear-gradient(135deg,#0d2d1a,#112d1a);border:2px solid var(--green-dark);border-radius:var(--radius-lg);padding:16px 20px;display:flex;align-items:center;gap:16px;margin-bottom:20px}
|
|
163
|
+
.ab-avatar{width:52px;height:52px;border-radius:50%;background:var(--green-bg);border:2px solid var(--green);display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0}
|
|
164
|
+
.ab-name{font-size:20px;font-weight:700;color:var(--bright)}
|
|
165
|
+
.ab-meta{font-size:12px;color:var(--dim);margin-top:2px}
|
|
166
|
+
.profile-card{background:var(--panel);border:2px solid var(--border);border-radius:var(--radius-lg);padding:16px;cursor:pointer;transition:all .15s;display:flex;flex-direction:column;gap:10px;position:relative}
|
|
167
|
+
.profile-card:hover{border-color:var(--dim);transform:translateY(-1px);box-shadow:0 4px 16px rgba(0,0,0,.3)}
|
|
168
|
+
.profile-card.cur{border-color:var(--green);background:linear-gradient(135deg,rgba(35,134,54,.06),rgba(63,185,80,.03))}
|
|
169
|
+
.pc-icon{font-size:24px;margin-bottom:2px}
|
|
170
|
+
.pc-name{font-size:14px;font-weight:700;color:var(--bright)}
|
|
171
|
+
.pc-desc{font-size:12px;color:var(--dim);line-height:1.5}
|
|
172
|
+
.pc-meta{display:flex;gap:6px;flex-wrap:wrap}
|
|
173
|
+
.pc-active{position:absolute;top:10px;right:10px;font-size:10px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:#000;background:var(--green);padding:2px 8px;border-radius:3px;font-family:var(--mono)}
|
|
174
|
+
.pc-actions{display:flex;gap:8px;margin-top:4px}
|
|
175
|
+
|
|
176
|
+
/* -- PANEL: Agent Builder -- */
|
|
177
|
+
.builder-layout{display:flex;flex:1;min-height:0;overflow:hidden}
|
|
178
|
+
.builder-list{width:240px;flex-shrink:0;border-right:1px solid var(--border);overflow-y:auto;background:var(--panel2)}
|
|
179
|
+
.builder-list-header{padding:12px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
|
|
180
|
+
.builder-list-title{font-size:13px;font-weight:600;color:var(--bright);flex:1}
|
|
181
|
+
.agent-list-item{padding:10px 14px;cursor:pointer;border-left:2px solid transparent;transition:all .1s;border-bottom:1px solid var(--border2)}
|
|
182
|
+
.agent-list-item:hover{background:rgba(177,186,196,.06)}
|
|
183
|
+
.agent-list-item.active{border-left-color:var(--blue);background:var(--blue-bg)}
|
|
184
|
+
.ali-name{font-size:13px;font-weight:600;color:var(--text)}
|
|
185
|
+
.ali-model{font-size:11px;color:var(--dim);margin-top:2px;font-family:var(--mono)}
|
|
186
|
+
.builder-editor{flex:1;overflow-y:auto;padding:24px 28px}
|
|
187
|
+
.be-tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:18px}
|
|
188
|
+
.be-tab{padding:8px 16px;cursor:pointer;font-size:13px;font-weight:500;color:var(--dim);border-bottom:2px solid transparent;transition:all .15s;user-select:none;white-space:nowrap}
|
|
189
|
+
.be-tab:hover{color:var(--text)}
|
|
190
|
+
.be-tab.active{color:var(--blue);border-bottom-color:var(--blue)}
|
|
191
|
+
.voice-grid{display:flex;flex-wrap:wrap;gap:6px}
|
|
192
|
+
.voice-btn{padding:6px 12px;background:var(--panel3);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;font-size:12px;color:var(--dim);transition:all .15s;font-family:var(--mono)}
|
|
193
|
+
.voice-btn:hover{border-color:var(--dim);color:var(--text)}
|
|
194
|
+
.voice-btn.sel-m{border-color:var(--blue);background:var(--blue-bg);color:var(--blue)}
|
|
195
|
+
.voice-btn.sel-f{border-color:var(--purple);background:var(--purple-bg);color:var(--purple)}
|
|
196
|
+
.tools-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
|
|
197
|
+
.tool-check{display:flex;align-items:center;gap:8px;padding:7px 10px;background:var(--panel3);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer}
|
|
198
|
+
.tool-check input{accent-color:var(--blue);width:14px;height:14px;flex-shrink:0;cursor:pointer}
|
|
199
|
+
.tool-check-label{font-size:12px;color:var(--dim);font-family:var(--mono)}
|
|
200
|
+
.greeting-tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
|
|
201
|
+
.g-tag{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:var(--panel3);border:1px solid var(--border);border-radius:12px;font-size:12px;color:var(--text)}
|
|
202
|
+
.g-tag-x{background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px;line-height:1;padding:0 2px}
|
|
203
|
+
.g-tag-x:hover{color:var(--red)}
|
|
204
|
+
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
|
205
|
+
.sect-label{font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--dim);margin:16px 0 10px}
|
|
206
|
+
|
|
207
|
+
/* -- PANEL: Provider Config -- */
|
|
208
|
+
.prov-col{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;display:flex;flex-direction:column}
|
|
209
|
+
.prov-col-hdr{padding:12px 16px;background:var(--panel3);border-bottom:1px solid var(--border);font-size:12px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:var(--dim);text-align:center}
|
|
210
|
+
.prov-list{flex:1;overflow-y:auto;padding:8px 0}
|
|
211
|
+
.prov-item{padding:10px 14px;cursor:pointer;display:flex;align-items:flex-start;gap:10px;border-left:2px solid transparent;transition:all .1s;border-bottom:1px solid var(--border2)}
|
|
212
|
+
.prov-item:last-child{border-bottom:none}
|
|
213
|
+
.prov-item:hover{background:rgba(177,186,196,.04)}
|
|
214
|
+
.prov-item.sel{border-left-color:var(--blue);background:var(--blue-bg)}
|
|
215
|
+
.prov-radio{width:16px;height:16px;border-radius:50%;border:2px solid var(--border);flex-shrink:0;margin-top:2px;position:relative;transition:border-color .15s}
|
|
216
|
+
.prov-item.sel .prov-radio{border-color:var(--blue)}
|
|
217
|
+
.prov-item.sel .prov-radio::after{content:'';position:absolute;width:8px;height:8px;background:var(--blue);border-radius:50%;top:50%;left:50%;transform:translate(-50%,-50%)}
|
|
218
|
+
.prov-info-name{font-size:13px;font-weight:600;color:var(--text)}
|
|
219
|
+
.prov-info-detail{font-size:11px;color:var(--dim);margin-top:2px}
|
|
220
|
+
.prov-config{background:var(--panel2);border-top:1px solid var(--border);padding:12px 14px;display:none}
|
|
221
|
+
.prov-item.sel .prov-config{display:block}
|
|
222
|
+
.test-btn{display:flex;align-items:center;gap:6px;padding:5px 12px;border-radius:var(--radius);border:1px solid var(--border);background:transparent;color:var(--dim);font-size:12px;cursor:pointer;transition:all .15s;margin-top:8px}
|
|
223
|
+
.test-btn:hover{border-color:var(--orange);color:var(--orange)}
|
|
224
|
+
.test-btn.testing{border-color:var(--orange);color:var(--orange)}
|
|
225
|
+
.test-btn.pass{border-color:var(--green);color:var(--green);background:var(--green-bg)}
|
|
226
|
+
.test-btn.fail{border-color:var(--red);color:var(--red);background:var(--red-bg)}
|
|
227
|
+
|
|
228
|
+
/* -- PANEL: Install -- */
|
|
229
|
+
.install-steps{display:flex;margin-bottom:20px;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
|
230
|
+
.ist{flex:1;padding:10px 6px;font-size:11px;font-weight:600;text-align:center;color:var(--subtle);border-right:1px solid var(--border);transition:all .2s}
|
|
231
|
+
.ist:last-child{border-right:none}
|
|
232
|
+
.ist.done{color:var(--green);background:var(--green-bg)}
|
|
233
|
+
.ist.active{color:var(--blue);background:var(--blue-bg)}
|
|
234
|
+
.url-bar{display:flex;gap:10px;margin-bottom:16px}
|
|
235
|
+
.url-bar input{flex:1}
|
|
236
|
+
.term{background:#010409;border:1px solid var(--border);border-radius:var(--radius-lg);padding:16px;font-family:var(--mono);font-size:12px;line-height:1.8;min-height:300px;max-height:460px;overflow-y:auto;color:#c9d1d9}
|
|
237
|
+
.t-dim{color:var(--subtle)}.t-g{color:var(--green)}.t-b{color:var(--blue)}.t-y{color:var(--yellow)}.t-r{color:var(--red)}.t-c{color:var(--cyan)}.t-sec{color:var(--dim);margin:6px 0;border-top:1px solid var(--border2);padding-top:6px;display:block}
|
|
238
|
+
|
|
239
|
+
/* -- PANEL: Tests -- */
|
|
240
|
+
.test-row{display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--panel);border:1px solid var(--border2);border-radius:var(--radius)}
|
|
241
|
+
.test-icon{font-size:15px;width:20px;text-align:center;flex-shrink:0}
|
|
242
|
+
.test-name{flex:1;font-size:13px;font-weight:500}
|
|
243
|
+
.test-res{font-size:12px;font-family:var(--mono)}.test-res.pass{color:var(--green)}.test-res.fail{color:var(--red)}.test-res.skip{color:var(--subtle)}.test-res.running{color:var(--blue)}
|
|
244
|
+
.test-ms{font-size:11px;color:var(--subtle);font-family:var(--mono);min-width:55px;text-align:right}
|
|
245
|
+
|
|
246
|
+
/* -- PANEL: DJ Prompt -- */
|
|
247
|
+
.prompt-bar{display:flex;align-items:center;gap:10px;margin-bottom:12px}
|
|
248
|
+
.prompt-stats{font-size:12px;color:var(--dim);margin-left:auto}
|
|
249
|
+
.prompt-dirty{color:var(--yellow);font-size:12px}
|
|
250
|
+
#prompt-ta{min-height:420px;font-family:var(--mono);font-size:12px;line-height:1.7;background:#010409;border-color:var(--border)}
|
|
251
|
+
|
|
252
|
+
/* -- PANEL: Frameworks -- */
|
|
253
|
+
.fw-cap{display:flex;gap:4px;flex-wrap:wrap}
|
|
254
|
+
.fw-cap span{font-size:10px;padding:2px 7px;border-radius:10px;border:1px solid rgba(88,166,255,.3);color:var(--blue);background:var(--blue-bg)}
|
|
255
|
+
|
|
256
|
+
/* -- PANEL: System -- */
|
|
257
|
+
.hc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px;margin-bottom:20px}
|
|
258
|
+
.hc{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius-lg);padding:14px}
|
|
259
|
+
.hc-label{font-size:11px;color:var(--dim);margin-bottom:6px;font-weight:600;letter-spacing:.5px;text-transform:uppercase}
|
|
260
|
+
.hc-val{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--bright)}
|
|
261
|
+
.hc-val.g{color:var(--green)}.hc-val.r{color:var(--red)}.hc-val.y{color:var(--yellow)}
|
|
262
|
+
.hc-sub{font-size:11px;color:var(--dim);margin-top:3px}
|
|
263
|
+
</style>
|
|
264
|
+
</head>
|
|
265
|
+
<body>
|
|
266
|
+
<div id="app">
|
|
267
|
+
|
|
268
|
+
<!-- Header -->
|
|
269
|
+
<header id="hdr">
|
|
270
|
+
<button class="hdr-toggle" onclick="toggleSidebar()">☰</button>
|
|
271
|
+
<div class="hdr-logo">OpenUI <em>Admin</em></div>
|
|
272
|
+
<div class="hdr-sep"></div>
|
|
273
|
+
<div class="hdr-pill"><div class="dot" id="health-dot"></div><span id="health-txt">Checking...</span></div>
|
|
274
|
+
<a href="/" class="hdr-back">← Voice App</a>
|
|
275
|
+
</header>
|
|
276
|
+
|
|
277
|
+
<!-- Sidebar -->
|
|
278
|
+
<nav id="sidebar">
|
|
279
|
+
<div class="nav-section">
|
|
280
|
+
<div class="nav-label">Agents</div>
|
|
281
|
+
<div class="nav-item active" id="nav-agents" onclick="show('agents')"><span class="nav-icon">🤖</span><span class="nav-text">Agent Profiles</span></div>
|
|
282
|
+
<div class="nav-item" id="nav-builder" onclick="show('builder')"><span class="nav-icon">🔧</span><span class="nav-text">Agent Builder</span></div>
|
|
283
|
+
<div class="nav-item" id="nav-instructions" onclick="show('instructions')"><span class="nav-icon">📝</span><span class="nav-text">Instructions</span></div>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="nav-section">
|
|
286
|
+
<div class="nav-label">Frameworks</div>
|
|
287
|
+
<div class="nav-item" id="nav-frameworks" onclick="show('frameworks')"><span class="nav-icon">🔌</span><span class="nav-text">Framework Registry</span></div>
|
|
288
|
+
<div class="nav-item" id="nav-install" onclick="show('install')"><span class="nav-icon">📦</span><span class="nav-text">Install Framework</span></div>
|
|
289
|
+
<div class="nav-item" id="nav-tests" onclick="show('tests')"><span class="nav-icon">🧪</span><span class="nav-text">Connector Tests</span></div>
|
|
290
|
+
</div>
|
|
291
|
+
<div class="nav-section">
|
|
292
|
+
<div class="nav-label">Providers</div>
|
|
293
|
+
<div class="nav-item" id="nav-providers" onclick="show('providers')"><span class="nav-icon">⚡</span><span class="nav-text">Provider Config</span></div>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="nav-section">
|
|
296
|
+
<div class="nav-label">Content</div>
|
|
297
|
+
<div class="nav-item" id="nav-djtools" onclick="show('djtools')"><span class="nav-icon">🎵</span><span class="nav-text">Music Agent</span></div>
|
|
298
|
+
<div class="nav-item" id="nav-canvas" onclick="show('canvas')"><span class="nav-icon">🖥️</span><span class="nav-text">Canvas Pages</span></div>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="nav-section">
|
|
301
|
+
<div class="nav-label">System</div>
|
|
302
|
+
<div class="nav-item" id="nav-system" onclick="show('system')"><span class="nav-icon">⚙️</span><span class="nav-text">System & Health</span></div>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="sidebar-footer">
|
|
305
|
+
<div class="nav-item" onclick="location.href='/'"><span class="nav-icon">←</span><span class="nav-text">Back to App</span></div>
|
|
306
|
+
</div>
|
|
307
|
+
</nav>
|
|
308
|
+
|
|
309
|
+
<!-- Main -->
|
|
310
|
+
<main id="main">
|
|
311
|
+
|
|
312
|
+
<!-- AGENTS -->
|
|
313
|
+
<div class="panel active" id="panel-agents">
|
|
314
|
+
<div class="panel-inner">
|
|
315
|
+
<div class="ph"><div class="ph-title">Agent Profiles</div><div class="ph-sub">Select the active agent — controls personality, voice, LLM, and capabilities</div></div>
|
|
316
|
+
<div id="active-banner"></div>
|
|
317
|
+
<div class="ph-row"><button class="btn btn-o" onclick="Agents.load()">↺ Refresh</button></div>
|
|
318
|
+
<div class="g4" id="agents-grid"><div class="loading"><div class="spin"></div>Loading...</div></div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<!-- AGENT BUILDER -->
|
|
323
|
+
<div class="panel" id="panel-builder">
|
|
324
|
+
<div class="builder-layout">
|
|
325
|
+
<div class="builder-list">
|
|
326
|
+
<div class="builder-list-header">
|
|
327
|
+
<span class="builder-list-title">Agents</span>
|
|
328
|
+
<button class="btn btn-p btn-sm" onclick="Builder.newAgent()">+ New</button>
|
|
329
|
+
</div>
|
|
330
|
+
<div id="builder-agent-list"><div class="loading" style="padding:14px"><div class="spin"></div>Loading...</div></div>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="builder-editor" id="builder-editor">
|
|
333
|
+
<div class="empty"><div class="empty-icon">🔧</div>Select an agent from the list to edit</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<!-- FRAMEWORKS -->
|
|
339
|
+
<div class="panel" id="panel-frameworks">
|
|
340
|
+
<div class="panel-inner">
|
|
341
|
+
<div class="ph"><div class="ph-title">Framework Registry</div><div class="ph-sub">All installed agent frameworks — health, capabilities, and configuration</div></div>
|
|
342
|
+
<div class="ph-row"><button class="btn btn-o" onclick="Frameworks.load()">↺ Refresh</button><button class="btn btn-p" onclick="show('install')">+ Install New</button></div>
|
|
343
|
+
<div class="box" style="padding:0;overflow:hidden;margin-bottom:20px">
|
|
344
|
+
<table class="tbl" id="fw-table">
|
|
345
|
+
<thead><tr><th>Framework</th><th>Status</th><th>Capabilities</th><th>LLM</th><th>TTS</th><th>Actions</th></tr></thead>
|
|
346
|
+
<tbody id="fw-tbody"><tr><td colspan="6"><div class="loading" style="padding:14px"><div class="spin"></div></div></td></tr></tbody>
|
|
347
|
+
</table>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="box"><div class="box-title">Config Viewer</div><div class="box-body" id="fw-config"><div class="empty"><div class="empty-icon">🔌</div>Click "Config" on a framework above</div></div></div>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<!-- INSTALL -->
|
|
354
|
+
<div class="panel" id="panel-install">
|
|
355
|
+
<div class="panel-inner">
|
|
356
|
+
<div class="ph"><div class="ph-title">Install Framework</div><div class="ph-sub">Provide a GitHub URL or package name — the agent researches, installs, writes a connector, tests, and registers it automatically</div></div>
|
|
357
|
+
<div class="install-steps">
|
|
358
|
+
<div class="ist" id="ist-research">1 · Research</div>
|
|
359
|
+
<div class="ist" id="ist-plan">2 · Plan</div>
|
|
360
|
+
<div class="ist" id="ist-install">3 · Install</div>
|
|
361
|
+
<div class="ist" id="ist-connector">4 · Connector</div>
|
|
362
|
+
<div class="ist" id="ist-test">5 · Test</div>
|
|
363
|
+
<div class="ist" id="ist-register">6 · Register</div>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="url-bar">
|
|
366
|
+
<input type="url" id="install-url" placeholder="https://github.com/org/repo or pipecat-ai or livekit-agents">
|
|
367
|
+
<button class="btn btn-p" id="install-btn" onclick="Install.start()">▶ Start</button>
|
|
368
|
+
<button class="btn btn-o" onclick="Install.clear()">✕ Clear</button>
|
|
369
|
+
</div>
|
|
370
|
+
<div class="term" id="install-term"><span class="t-dim">Enter a URL or package name above and click Start.</span><br><span class="t-dim">The agent will handle everything: research → install → connector → test → register.</span></div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<!-- TESTS -->
|
|
375
|
+
<div class="panel" id="panel-tests">
|
|
376
|
+
<div class="panel-inner">
|
|
377
|
+
<div class="ph"><div class="ph-title">Connector Tests</div><div class="ph-sub">Automated diagnostic suite against live API endpoints</div></div>
|
|
378
|
+
<div class="ph-row">
|
|
379
|
+
<select id="test-sel" style="width:220px"><option value="active">Active Framework</option></select>
|
|
380
|
+
<button class="btn btn-p" id="run-btn" onclick="Tests.run()">▶ Run All Tests</button>
|
|
381
|
+
<span id="test-sum" style="font-size:12px;color:var(--dim)"></span>
|
|
382
|
+
</div>
|
|
383
|
+
<div style="display:flex;flex-direction:column;gap:6px" id="test-list"><div class="empty"><div class="empty-icon">🧪</div>Click Run All Tests to begin</div></div>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<!-- PROVIDERS -->
|
|
388
|
+
<div class="panel" id="panel-providers">
|
|
389
|
+
<div class="panel-inner">
|
|
390
|
+
<div class="ph"><div class="ph-title">Provider Config</div><div class="ph-sub">Select and configure LLM, TTS, and STT providers for the active agent</div></div>
|
|
391
|
+
<div class="g3" id="prov-grid" style="align-items:start">
|
|
392
|
+
<div class="loading"><div class="spin"></div>Loading...</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<!-- MUSIC AGENT (coming soon) -->
|
|
398
|
+
<div class="panel" id="panel-djtools">
|
|
399
|
+
<div class="panel-inner">
|
|
400
|
+
<div class="ph"><div class="ph-title">Music Agent</div><div class="ph-sub">Music configuration — tracks, prompt, sound effects, and music scheduling</div></div>
|
|
401
|
+
<div style="background:var(--panel);border:1px solid var(--border);border-radius:var(--radius-lg);padding:40px;text-align:center;margin-top:20px">
|
|
402
|
+
<div style="font-size:40px;margin-bottom:16px;opacity:.4">🎵</div>
|
|
403
|
+
<div style="font-size:16px;font-weight:600;color:var(--bright);margin-bottom:8px">Music Agent Settings</div>
|
|
404
|
+
<div style="font-size:13px;color:var(--dim);max-width:400px;margin:0 auto 20px">Track library, DJ prompt, sound effects, and music scheduling will be managed here.</div>
|
|
405
|
+
<div style="display:flex;gap:10px;justify-content:center">
|
|
406
|
+
<button class="btn btn-p" onclick="show('builder');Builder.load()">Configure Music Agent</button>
|
|
407
|
+
<span class="badge b-yellow" style="align-self:center;padding:6px 12px">Coming Soon</span>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<!-- SYSTEM -->
|
|
414
|
+
<div class="panel" id="panel-system">
|
|
415
|
+
<div class="panel-inner">
|
|
416
|
+
<div class="ph"><div class="ph-title">System & Health</div><div class="ph-sub">Server status, gateway connection, and resource usage</div></div>
|
|
417
|
+
<div class="hc-grid" id="hc-grid"><div class="loading"><div class="spin"></div>Loading...</div></div>
|
|
418
|
+
<div class="g2" style="gap:16px;margin-bottom:16px">
|
|
419
|
+
<div class="box"><div class="box-title">Service Health</div><div class="box-body" id="sys-health"></div></div>
|
|
420
|
+
<div class="box"><div class="box-title">Server Resources</div><div class="box-body" id="sys-stats"></div></div>
|
|
421
|
+
</div>
|
|
422
|
+
<div class="box">
|
|
423
|
+
<div class="box-title">Gateway</div>
|
|
424
|
+
<div class="box-body" id="sys-gw"><div class="loading"><div class="spin"></div></div></div>
|
|
425
|
+
<div style="padding:0 16px 14px;display:flex;gap:8px">
|
|
426
|
+
<button class="btn btn-o btn-sm" onclick="System.checkGW()">↺ Test Gateway</button>
|
|
427
|
+
<button class="btn btn-d btn-sm" onclick="System.resetSession()">↺ Reset Session</button>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<!-- INSTRUCTIONS -->
|
|
434
|
+
<div class="panel" id="panel-instructions">
|
|
435
|
+
<div class="panel-inner">
|
|
436
|
+
<div class="ph">
|
|
437
|
+
<div class="ph-title">Agent Instructions</div>
|
|
438
|
+
<div class="ph-sub">Live instruction files injected into the agent. App-scope files reload on the next message — no restart needed. OpenClaw files are read fresh each session.</div>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="ph-row">
|
|
441
|
+
<button class="btn btn-o" onclick="Instructions.load()">↺ Refresh</button>
|
|
442
|
+
</div>
|
|
443
|
+
<div id="instr-layout" style="display:grid;grid-template-columns:220px 1fr;gap:16px;height:calc(100vh - 220px)">
|
|
444
|
+
<!-- File list -->
|
|
445
|
+
<div class="box" style="overflow-y:auto">
|
|
446
|
+
<div class="box-title">Files</div>
|
|
447
|
+
<div id="instr-list" style="padding:8px 0"><div class="loading"><div class="spin"></div>Loading...</div></div>
|
|
448
|
+
</div>
|
|
449
|
+
<!-- Editor -->
|
|
450
|
+
<div class="box" style="display:flex;flex-direction:column;overflow:hidden">
|
|
451
|
+
<div class="box-title" style="display:flex;align-items:center;justify-content:space-between">
|
|
452
|
+
<span id="instr-editor-title">Select a file</span>
|
|
453
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
454
|
+
<span id="instr-badge" class="badge b-gray" style="display:none"></span>
|
|
455
|
+
<button id="instr-save-btn" class="btn btn-g btn-sm" style="display:none" onclick="Instructions.save()">💾 Save</button>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
<div id="instr-desc" style="padding:8px 16px;font-size:12px;color:var(--dim);border-bottom:1px solid var(--border2)"></div>
|
|
459
|
+
<textarea id="instr-editor"
|
|
460
|
+
style="flex:1;background:var(--bg);border:none;outline:none;resize:none;padding:16px;font-family:var(--mono);font-size:13px;color:var(--text);line-height:1.6;min-height:0"
|
|
461
|
+
placeholder="Select a file from the list to edit..."
|
|
462
|
+
oninput="Instructions.onEdit()"
|
|
463
|
+
></textarea>
|
|
464
|
+
<div id="instr-status" style="padding:6px 16px;font-size:11px;color:var(--dim);border-top:1px solid var(--border2);min-height:24px"></div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
<!-- CANVAS PAGES -->
|
|
471
|
+
<div class="panel" id="panel-canvas">
|
|
472
|
+
<div class="panel-inner">
|
|
473
|
+
<div class="ph">
|
|
474
|
+
<div class="ph-title">Canvas Pages</div>
|
|
475
|
+
<div class="ph-sub">Manage page visibility — toggle public access per page</div>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="box">
|
|
478
|
+
<div class="box-body" id="canvas-pages-list"><div class="loading"><div class="spin"></div>Loading...</div></div>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
</main>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<div id="toast" style="position:fixed;bottom:24px;right:24px;padding:10px 18px;border-radius:8px;font-size:13px;z-index:9999;opacity:0;transition:opacity .3s;box-shadow:0 4px 20px rgba(0,0,0,.5)"></div>
|
|
487
|
+
|
|
488
|
+
<script>
|
|
489
|
+
const $ = id => document.getElementById(id);
|
|
490
|
+
const esc = s => String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
491
|
+
|
|
492
|
+
// Sidebar
|
|
493
|
+
let collapsed = false;
|
|
494
|
+
function toggleSidebar(){collapsed=!collapsed;$('app').classList.toggle('sidebar-collapsed',collapsed)}
|
|
495
|
+
|
|
496
|
+
// Panel routing
|
|
497
|
+
const loaders = {
|
|
498
|
+
agents:()=>Agents.load(),
|
|
499
|
+
builder:()=>Builder.load(),
|
|
500
|
+
frameworks:()=>Frameworks.load(),
|
|
501
|
+
install:()=>{},
|
|
502
|
+
tests:()=>Tests.initSel(),
|
|
503
|
+
providers:()=>Providers.load(),
|
|
504
|
+
djtools:()=>DJTools.load(),
|
|
505
|
+
system:()=>System.load(),
|
|
506
|
+
instructions:()=>Instructions.load(),
|
|
507
|
+
canvas:()=>CanvasPages.load()
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
function show(name){
|
|
511
|
+
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
|
|
512
|
+
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
|
|
513
|
+
$('panel-'+name)?.classList.add('active');
|
|
514
|
+
$('nav-'+name)?.classList.add('active');
|
|
515
|
+
loaders[name]?.();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Agents
|
|
519
|
+
const ICONS = {'default':'🤖','hume-evi':'🌊','hume_evi':'🌊','elevenlabs-classic':'🎙️','elevenlabs_classic':'🎙️',developer:'🛠️'};
|
|
520
|
+
const LLMS = {'default':'GLM-4.7','hume-evi':'eLLM','elevenlabs-classic':'EL-AI',developer:'GLM-4.7'};
|
|
521
|
+
const TTSS = {'default':'Groq/Supertonic','hume-evi':'Hume EVI','elevenlabs-classic':'ElevenLabs',developer:'Supertonic'};
|
|
522
|
+
|
|
523
|
+
const Agents = {
|
|
524
|
+
profiles:[],active:null,
|
|
525
|
+
async load(){
|
|
526
|
+
$('agents-grid').innerHTML='<div class="loading"><div class="spin"></div>Loading...</div>';
|
|
527
|
+
try{
|
|
528
|
+
const d=await(await fetch('/api/profiles')).json();
|
|
529
|
+
this.profiles=d.profiles||[];this.active=d.active;
|
|
530
|
+
this.renderBanner();this.renderGrid();
|
|
531
|
+
}catch(e){$('agents-grid').innerHTML='<div class="empty"><div class="empty-icon">⚠️</div>'+esc(e.message)+'</div>'}
|
|
532
|
+
},
|
|
533
|
+
renderBanner(){
|
|
534
|
+
const p=this.profiles.find(x=>x.id===this.active);
|
|
535
|
+
$('active-banner').innerHTML=p?'<div class="active-banner"><div class="ab-avatar">'+(ICONS[p.id]||'🤖')+'</div><div><div class="ab-name">'+esc(p.name)+'</div><div class="ab-meta">'+esc(p.description||'')+' · <span class="badge b-green">Active</span></div></div></div>':''
|
|
536
|
+
},
|
|
537
|
+
renderGrid(){
|
|
538
|
+
const g=$('agents-grid');
|
|
539
|
+
if(!this.profiles.length){g.innerHTML='<div class="empty"><div class="empty-icon">🤖</div>No profiles found</div>';return}
|
|
540
|
+
g.innerHTML=this.profiles.map(p=>{
|
|
541
|
+
const cur=p.id===this.active;
|
|
542
|
+
return'<div class="profile-card'+(cur?' cur':'')+'" onclick="Agents.activate(\''+esc(p.id)+'\')">'+
|
|
543
|
+
(cur?'<div class="pc-active">Active</div>':'')+
|
|
544
|
+
'<div class="pc-icon">'+(ICONS[p.id]||'🤖')+'</div>'+
|
|
545
|
+
'<div class="pc-name">'+esc(p.name)+'</div>'+
|
|
546
|
+
'<div class="pc-desc">'+esc(p.description||'')+'</div>'+
|
|
547
|
+
'<div class="pc-meta"><span class="badge b-blue">'+esc(p.id)+'</span><span class="badge b-gray">v'+esc(p.version||'1.0')+'</span></div>'+
|
|
548
|
+
'<div class="pc-actions" onclick="event.stopPropagation()">'+
|
|
549
|
+
(cur?'<span class="badge b-green">✓ Running</span>':'<button class="btn btn-p btn-sm" onclick="event.stopPropagation();Agents.activate(\''+esc(p.id)+'\')">▶ Activate</button>')+
|
|
550
|
+
'<button class="btn btn-o btn-sm" onclick="event.stopPropagation();show(\'builder\')">Edit</button>'+
|
|
551
|
+
'</div></div>'
|
|
552
|
+
}).join('');
|
|
553
|
+
},
|
|
554
|
+
async activate(id){
|
|
555
|
+
try{
|
|
556
|
+
const d=await(await fetch('/api/profiles/activate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({profile_id:id})})).json();
|
|
557
|
+
if(d.active||d.profile){this.active=id;this.renderBanner();this.renderGrid();toast('Activated: '+id,'green')}
|
|
558
|
+
else toast(d.error||'Failed','red');
|
|
559
|
+
}catch(e){toast(e.message,'red')}
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Agent Builder
|
|
564
|
+
const Builder = {
|
|
565
|
+
profiles:[], currentId:null, activeId:null, _voice:null, activeTab:'general',
|
|
566
|
+
|
|
567
|
+
async load(){
|
|
568
|
+
const r=await fetch('/api/profiles').catch(()=>null);
|
|
569
|
+
if(!r){$('builder-agent-list').innerHTML='<div style="padding:14px;color:var(--red)">Error loading profiles</div>';return}
|
|
570
|
+
const d=await r.json();
|
|
571
|
+
this.profiles=d.profiles||[]; this.activeId=d.active;
|
|
572
|
+
this.renderList();
|
|
573
|
+
if(this.profiles.length && !this.currentId) this.selectAgent(this.profiles[0].id);
|
|
574
|
+
else if(this.currentId) this.selectAgent(this.currentId);
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
renderList(){
|
|
578
|
+
$('builder-agent-list').innerHTML=this.profiles.map(p=>
|
|
579
|
+
'<div class="agent-list-item'+(p.id===this.currentId?' active':'')+'" id="ali-'+p.id+'" onclick="Builder.selectAgent(\''+esc(p.id)+'\')">' +
|
|
580
|
+
'<div style="display:flex;align-items:center;gap:6px">' +
|
|
581
|
+
'<div class="ali-name">'+esc(p.name)+'</div>' +
|
|
582
|
+
(p.id===this.activeId?'<span style="font-size:9px;background:var(--green-dark);color:#fff;padding:1px 5px;border-radius:3px;font-weight:700">LIVE</span>':'') +
|
|
583
|
+
'</div>' +
|
|
584
|
+
'<div class="ali-model">'+esc(p.llm?.provider||LLMS[p.id]||'—')+'</div></div>'
|
|
585
|
+
).join('');
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
async selectAgent(id){
|
|
589
|
+
this.currentId=id;
|
|
590
|
+
document.querySelectorAll('.agent-list-item').forEach(el=>el.classList.remove('active'));
|
|
591
|
+
$('ali-'+id)?.classList.add('active');
|
|
592
|
+
$('builder-editor').innerHTML='<div class="loading" style="padding:24px"><div class="spin"></div>Loading '+esc(id)+'...</div>';
|
|
593
|
+
try{
|
|
594
|
+
const r=await fetch('/api/profiles/'+id);
|
|
595
|
+
if(!r.ok) throw new Error('HTTP '+r.status);
|
|
596
|
+
const data=await r.json();
|
|
597
|
+
// GET /api/profiles/:id returns profile directly (not wrapped)
|
|
598
|
+
const p=data.profile||data;
|
|
599
|
+
this._voice=p.voice?.voice_id||'M1';
|
|
600
|
+
this.renderEditor(id,p);
|
|
601
|
+
}catch(e){
|
|
602
|
+
$('builder-editor').innerHTML='<div style="padding:24px;color:var(--red)">Error: '+esc(e.message)+'</div>';
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
|
|
606
|
+
renderEditor(id,p){
|
|
607
|
+
const voices=['M1','M2','M3','M4','M5','F1','F2','F3','F4','F5'];
|
|
608
|
+
const selVoice=this._voice||'M1';
|
|
609
|
+
// features is {canvas:bool, music:bool, ...} object
|
|
610
|
+
const featObj=Array.isArray(p.features)?Object.fromEntries(p.features.map(k=>[k,true])):(p.features||{});
|
|
611
|
+
const allFeats=['canvas','music','tools','vision','memory','caller','sounds','commercial'];
|
|
612
|
+
const llmProv=p.llm?.provider||'zai';
|
|
613
|
+
const ttsProv=p.voice?.tts_provider||'supertonic';
|
|
614
|
+
|
|
615
|
+
const generalTab=
|
|
616
|
+
'<div class="g2" style="margin-bottom:14px">'+
|
|
617
|
+
'<div class="fg"><label class="fl">Name</label><input type="text" id="be-name" value="'+esc(p.name||'')+'"></div>'+
|
|
618
|
+
'<div class="fg"><label class="fl">Version</label><input type="text" id="be-ver" value="'+esc(p.version||'1.0')+'"></div>'+
|
|
619
|
+
'</div>'+
|
|
620
|
+
'<div class="fg"><label class="fl">Description</label><textarea id="be-desc" style="min-height:52px">'+esc(p.description||'')+'</textarea></div>'+
|
|
621
|
+
'<div class="sect-label">Framework</div>'+
|
|
622
|
+
'<div class="g2" style="margin-bottom:14px">'+
|
|
623
|
+
'<div class="fg"><label class="fl">LLM / Agent System</label><select id="be-llm">'+
|
|
624
|
+
'<option value="zai"'+(llmProv==='zai'?' selected':'')+'>Z.AI GLM via OpenClaw Gateway</option>'+
|
|
625
|
+
'<option value="gateway"'+(llmProv==='gateway'?' selected':'')+'>OpenClaw Direct</option>'+
|
|
626
|
+
'<option value="hume_evi"'+(llmProv==='hume_evi'?' selected':'')+'>Hume EVI</option>'+
|
|
627
|
+
'<option value="elevenlabs"'+(llmProv==='elevenlabs'?' selected':'')+'>ElevenLabs AI</option>'+
|
|
628
|
+
'<option value="openai"'+(llmProv==='openai'?' selected':'')+'>OpenAI</option>'+
|
|
629
|
+
'</select></div>'+
|
|
630
|
+
'<div class="fg"><label class="fl">TTS Provider</label><select id="be-tts">'+
|
|
631
|
+
'<option value="supertonic"'+(ttsProv==='supertonic'?' selected':'')+'>Supertonic (Free · Local)</option>'+
|
|
632
|
+
'<option value="groq"'+(ttsProv==='groq'?' selected':'')+'>Groq Orpheus (Fast · Cloud)</option>'+
|
|
633
|
+
'<option value="hume"'+(ttsProv==='hume'?' selected':'')+'>Hume EVI</option>'+
|
|
634
|
+
'<option value="elevenlabs"'+(ttsProv==='elevenlabs'?' selected':'')+'>ElevenLabs</option>'+
|
|
635
|
+
'</select></div>'+
|
|
636
|
+
'</div>'+
|
|
637
|
+
'<div class="fg"><label class="fl">Voice</label>'+
|
|
638
|
+
'<div class="voice-grid" id="voice-grid">'+
|
|
639
|
+
voices.map(v=>{const f=v.startsWith('F');const sel=v===selVoice;return'<button class="voice-btn'+(sel?(f?' sel-f':' sel-m'):'')+'" onclick="Builder.setVoice(\''+v+'\','+f+')" id="vb-'+v+'">'+v+'</button>'}).join('')+
|
|
640
|
+
'</div>'+
|
|
641
|
+
'<div style="font-size:11px;color:var(--dim);margin-top:5px"><span style="color:var(--blue)">■</span> Male <span style="color:var(--purple)">■</span> Female</div>'+
|
|
642
|
+
'</div>'+
|
|
643
|
+
'<div class="sect-label">Vision / Camera AI</div>'+
|
|
644
|
+
'<div class="fg">'+
|
|
645
|
+
'<label class="fl">Vision Model <span style="font-size:10px;color:var(--dim)">— used for “look at” triggers and face recognition</span></label>'+
|
|
646
|
+
'<select id="be-vision-model">'+
|
|
647
|
+
'<option value="glm-4.6v"'+( (p.vision?.model||'glm-4.6v')==='glm-4.6v' ?' selected':'')+'>GLM-4.6V (128K · Paid)</option>'+
|
|
648
|
+
'<option value="glm-4v-plus"'+( (p.vision?.model||'glm-4.6v')==='glm-4v-plus' ?' selected':'')+'>GLM-4V Plus (Legacy · Paid)</option>'+
|
|
649
|
+
'</select>'+
|
|
650
|
+
'<div style="font-size:11px;color:var(--dim);margin-top:4px">Requires camera to be enabled. Model processes frames for “what do you see” and face ID.</div>'+
|
|
651
|
+
'</div>'+
|
|
652
|
+
'<div class="sect-label">Wake Word</div>'+
|
|
653
|
+
'<div class="fg">'+
|
|
654
|
+
'<label class="fl">Wake Word(s) <span style="font-size:10px;color:var(--dim)">— comma-separated phrases that activate this agent</span></label>'+
|
|
655
|
+
'<input type="text" id="be-wake-words" placeholder="wake up" value="'+esc((p.stt?.wake_words||['wake up']).join(', '))+'">'+
|
|
656
|
+
'<div style="font-size:11px;color:var(--dim);margin-top:4px">Default: <code style="font-size:11px">wake up</code> · Examples: <code style="font-size:11px">hey jarvis, jarvis</code> · Leave blank to reset to default</div>'+
|
|
657
|
+
'</div>'+
|
|
658
|
+
'<div class="g2" style="margin-top:10px">'+
|
|
659
|
+
'<label class="tool-check"><input type="checkbox" id="be-identify-on-wake"'+(p.stt?.identify_on_wake!==false?' checked':'')+'>'+
|
|
660
|
+
'<span class="tool-check-label">Identify face on wake <span style="font-size:10px;color:var(--dim)">— greet by name if recognized</span></span></label>'+
|
|
661
|
+
'<label class="tool-check"><input type="checkbox" id="be-require-camera-auth"'+(p.stt?.require_camera_auth===true?' checked':'')+'>'+
|
|
662
|
+
'<span class="tool-check-label">Require camera auth to wake <span style="font-size:10px;color:var(--dim)">— blocks unrecognized users</span></span></label>'+
|
|
663
|
+
'</div>';
|
|
664
|
+
|
|
665
|
+
const instrTab=
|
|
666
|
+
'<div style="font-size:12px;color:var(--dim);margin-bottom:10px">The agent\'s core personality, role, and behavior — equivalent to a SOUL.md file. Sent as the system prompt on every conversation.</div>'+
|
|
667
|
+
'<textarea id="be-prompt" style="min-height:360px;font-family:var(--mono);font-size:12px;line-height:1.7;background:#010409">'+esc(p.system_prompt||'')+'</textarea>'+
|
|
668
|
+
'<div style="font-size:11px;color:var(--subtle);margin-top:6px">Ctrl+S saves when Instructions tab is active</div>';
|
|
669
|
+
|
|
670
|
+
const featTab=
|
|
671
|
+
'<div style="font-size:12px;color:var(--dim);margin-bottom:14px">Enable capabilities for this agent. Takes effect on next conversation.</div>'+
|
|
672
|
+
'<div class="tools-grid">'+
|
|
673
|
+
allFeats.map(f=>'<label class="tool-check"><input type="checkbox"'+(featObj[f]?' checked':'')+' id="feat-'+f+'"><span class="tool-check-label">'+f+'</span></label>').join('')+
|
|
674
|
+
'</div>'+
|
|
675
|
+
'<div class="sect-label">Context</div>'+
|
|
676
|
+
'<div class="g2">'+
|
|
677
|
+
'<div class="fg"><label class="fl">Max History Messages</label><input type="text" id="be-maxhist" value="'+esc(String(p.context?.max_history_messages||12))+'"></div>'+
|
|
678
|
+
'<div class="fg"><label class="fl">Max Response Tokens</label><input type="text" id="be-maxtok" value="'+esc(String(p.llm?.parameters?.max_tokens||512))+'"></div>'+
|
|
679
|
+
'</div>';
|
|
680
|
+
|
|
681
|
+
$('builder-editor').innerHTML=
|
|
682
|
+
'<div class="ph" style="margin-bottom:14px">'+
|
|
683
|
+
'<div style="display:flex;align-items:center;gap:10px">'+
|
|
684
|
+
'<div class="ph-title">'+esc(p.name||id)+'</div>'+
|
|
685
|
+
(id===this.activeId?'<span class="badge b-green">● Live</span>':
|
|
686
|
+
'<button class="btn btn-p btn-sm" onclick="Agents.activate(\''+esc(id)+'\').then(()=>Builder.load())">▶ Activate</button>')+
|
|
687
|
+
'</div>'+
|
|
688
|
+
'<div class="ph-sub">'+esc(id)+' · v'+esc(p.version||'1.0')+'</div>'+
|
|
689
|
+
'</div>'+
|
|
690
|
+
'<div class="be-tabs">'+
|
|
691
|
+
['general','instructions','features'].map(t=>
|
|
692
|
+
'<div class="be-tab'+(this.activeTab===t?' active':'')+'" onclick="Builder.switchTab(\''+t+'\')">'+
|
|
693
|
+
{general:'⚙ General',instructions:'📝 Instructions',features:'🔧 Features'}[t]+'</div>'
|
|
694
|
+
).join('')+
|
|
695
|
+
'</div>'+
|
|
696
|
+
'<div id="be-tab-general" class="be-tab-body"'+(this.activeTab==='general'?'':' style="display:none"')+'>'+generalTab+'</div>'+
|
|
697
|
+
'<div id="be-tab-instructions" class="be-tab-body"'+(this.activeTab==='instructions'?'':' style="display:none"')+'>'+instrTab+'</div>'+
|
|
698
|
+
'<div id="be-tab-features" class="be-tab-body"'+(this.activeTab==='features'?'':' style="display:none"')+'>'+featTab+'</div>'+
|
|
699
|
+
'<div class="div"></div>'+
|
|
700
|
+
'<div style="display:flex;gap:8px">'+
|
|
701
|
+
'<button class="btn btn-p" onclick="Builder.save(\''+esc(id)+'\')">💾 Save Changes</button>'+
|
|
702
|
+
'<button class="btn btn-o" onclick="Builder.selectAgent(\''+esc(id)+'\')">↺ Discard</button>'+
|
|
703
|
+
(id!=='default'?'<button class="btn btn-d" onclick="Builder.delete(\''+esc(id)+'\')">🗑 Delete</button>':'')+
|
|
704
|
+
'</div>';
|
|
705
|
+
},
|
|
706
|
+
|
|
707
|
+
switchTab(tab){
|
|
708
|
+
this.activeTab=tab;
|
|
709
|
+
['general','instructions','features'].forEach(t=>{
|
|
710
|
+
const body=$('be-tab-'+t); if(body) body.style.display=t===tab?'':'none';
|
|
711
|
+
});
|
|
712
|
+
document.querySelectorAll('.be-tab').forEach(el=>{
|
|
713
|
+
el.classList.toggle('active', el.onclick?.toString().includes("'"+tab+"'"));
|
|
714
|
+
});
|
|
715
|
+
// Re-query by content since onclick toString varies
|
|
716
|
+
document.querySelectorAll('.be-tab').forEach(el=>{
|
|
717
|
+
const matches={general:'General',instructions:'Instructions',features:'Features'};
|
|
718
|
+
el.classList.toggle('active', el.textContent.trim().includes(matches[tab]||''));
|
|
719
|
+
});
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
setVoice(v,isFemale){
|
|
723
|
+
document.querySelectorAll('.voice-btn').forEach(b=>{b.className='voice-btn'});
|
|
724
|
+
$('vb-'+v).className='voice-btn '+(isFemale?'sel-f':'sel-m');
|
|
725
|
+
this._voice=v;
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
async save(id){
|
|
729
|
+
const featObj={};
|
|
730
|
+
['canvas','music','tools','vision','memory','caller','sounds','commercial'].forEach(f=>{
|
|
731
|
+
const el=$('feat-'+f); if(el) featObj[f]=el.checked;
|
|
732
|
+
});
|
|
733
|
+
const payload={
|
|
734
|
+
name:$('be-name')?.value||undefined,
|
|
735
|
+
description:$('be-desc')?.value||undefined,
|
|
736
|
+
version:$('be-ver')?.value||undefined,
|
|
737
|
+
system_prompt:$('be-prompt')?.value||undefined,
|
|
738
|
+
llm:{provider:$('be-llm')?.value, parameters:{max_tokens:parseInt($('be-maxtok')?.value)||512}},
|
|
739
|
+
voice:{tts_provider:$('be-tts')?.value, voice_id:this._voice||undefined},
|
|
740
|
+
stt:{
|
|
741
|
+
wake_words:(()=>{const raw=($('be-wake-words')?.value||'').trim();return raw?raw.split(',').map(w=>w.trim()).filter(Boolean):['wake up'];})(),
|
|
742
|
+
identify_on_wake:$('be-identify-on-wake')?.checked!==false,
|
|
743
|
+
require_camera_auth:$('be-require-camera-auth')?.checked===true,
|
|
744
|
+
},
|
|
745
|
+
vision:{model:$('be-vision-model')?.value||'glm-4.6v'},
|
|
746
|
+
features:Object.keys(featObj).length?featObj:undefined,
|
|
747
|
+
context:{max_history_messages:parseInt($('be-maxhist')?.value)||12},
|
|
748
|
+
};
|
|
749
|
+
Object.keys(payload).forEach(k=>payload[k]===undefined&&delete payload[k]);
|
|
750
|
+
try{
|
|
751
|
+
const r=await fetch('/api/profiles/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|
752
|
+
const d=await r.json();
|
|
753
|
+
// PUT returns full profile directly (d.id means success)
|
|
754
|
+
if(d.id||d.name){toast('Saved \u2713','green');}
|
|
755
|
+
else if(d.error) toast(d.error,'red');
|
|
756
|
+
else toast('Saved','green');
|
|
757
|
+
}catch(e){toast(e.message,'red')}
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
async newAgent(){
|
|
761
|
+
const name=prompt('Agent name (e.g. "CodeBot", "Assistant"):');
|
|
762
|
+
if(!name||!name.trim()) return;
|
|
763
|
+
const id=name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
|
|
764
|
+
const payload={
|
|
765
|
+
id, name:name.trim(), description:'New AI voice agent', version:'1.0',
|
|
766
|
+
system_prompt:'You are '+name.trim()+', an AI voice assistant. Be helpful, concise, and friendly.',
|
|
767
|
+
llm:{provider:'zai',model:'glm-4-7-flash',parameters:{max_tokens:512,temperature:0.7}},
|
|
768
|
+
voice:{tts_provider:'supertonic',voice_id:'M1',speed:1.1,parameters:{total_step:16}},
|
|
769
|
+
stt:{provider:'webspeech',language:'en-US'},
|
|
770
|
+
features:{canvas:false,music:false,tools:false,vision:false},
|
|
771
|
+
context:{max_history_messages:12,enable_history:true,enable_briefing:false,enable_fts:true},
|
|
772
|
+
ui:{face_enabled:true,face_mood:'neutral',theme:'dark',thought_bubbles:true,transcript_panel:true},
|
|
773
|
+
};
|
|
774
|
+
try{
|
|
775
|
+
const r=await fetch('/api/profiles',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|
776
|
+
const d=await r.json();
|
|
777
|
+
if(d.id||d.ok){toast('Created: '+name,'green'); this.currentId=id; await this.load();}
|
|
778
|
+
else toast(d.error||'Failed — check profile ID is unique','red');
|
|
779
|
+
}catch(e){toast(e.message,'red')}
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
async delete(id){
|
|
783
|
+
if(!confirm('Delete agent "'+id+'"?')) return;
|
|
784
|
+
try{
|
|
785
|
+
const r=await fetch('/api/profiles/'+id,{method:'DELETE'});
|
|
786
|
+
const d=await r.json();
|
|
787
|
+
if(d.ok||d.deleted){toast('Deleted','green'); this.currentId=null; await this.load();}
|
|
788
|
+
else toast(d.error||'Failed','red');
|
|
789
|
+
}catch(e){toast(e.message,'red')}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// Frameworks
|
|
794
|
+
const Frameworks={
|
|
795
|
+
CAPS:{'default':['Voice','Tools','Memory','Canvas','Files'],'hume-evi':['Voice','Emotion','Real-time'],'elevenlabs-classic':['Voice','Music','Caller','Phone'],developer:['Voice','Debug']},
|
|
796
|
+
async load(){
|
|
797
|
+
const tbody=$('fw-tbody');
|
|
798
|
+
tbody.innerHTML='<tr><td colspan="6"><div class="loading" style="padding:14px"><div class="spin"></div></div></td></tr>';
|
|
799
|
+
try{
|
|
800
|
+
const d=await(await fetch('/api/profiles')).json();
|
|
801
|
+
const profiles=d.profiles||[],active=d.active;
|
|
802
|
+
tbody.innerHTML=profiles.map(p=>{
|
|
803
|
+
const caps=(this.CAPS[p.id]||['Voice']).map(c=>'<span>'+esc(c)+'</span>').join('');
|
|
804
|
+
return'<tr>'+
|
|
805
|
+
'<td><div style="font-weight:600;color:var(--bright)">'+esc(p.name)+'</div><div style="font-size:11px;color:var(--subtle);font-family:var(--mono)">'+esc(p.id)+'</div></td>'+
|
|
806
|
+
'<td>'+(p.id===active?'<span class="badge b-green">● Active</span>':'<span class="badge b-gray">Ready</span>')+'</td>'+
|
|
807
|
+
'<td><div class="fw-cap">'+caps+'</div></td>'+
|
|
808
|
+
'<td><span style="font-family:var(--mono);font-size:12px">'+esc(LLMS[p.id]||'—')+'</span></td>'+
|
|
809
|
+
'<td><span style="font-family:var(--mono);font-size:12px">'+esc(TTSS[p.id]||'—')+'</span></td>'+
|
|
810
|
+
'<td style="display:flex;gap:6px">'+
|
|
811
|
+
'<button class="btn btn-o btn-sm" onclick="Frameworks.cfg(\''+esc(p.id)+'\',\''+esc(p.name)+'\')">Config</button>'+
|
|
812
|
+
(p.id!==active?'<button class="btn btn-p btn-sm" onclick="Agents.activate(\''+esc(p.id)+'\');Frameworks.load()">Activate</button>':'')+
|
|
813
|
+
'</td></tr>'
|
|
814
|
+
}).join('');
|
|
815
|
+
}catch(e){tbody.innerHTML='<tr><td colspan="6" style="color:var(--red)">'+esc(e.message)+'</td></tr>'}
|
|
816
|
+
},
|
|
817
|
+
async cfg(id,name){
|
|
818
|
+
const el=$('fw-config');el.innerHTML='<div class="loading"><div class="spin"></div>Loading...</div>';
|
|
819
|
+
try{
|
|
820
|
+
const d=await(await fetch('/api/profiles/'+id)).json();
|
|
821
|
+
el.innerHTML='<div style="font-weight:600;color:var(--bright);margin-bottom:10px">'+esc(name)+'</div><pre style="background:var(--bg);border:1px solid var(--border);border-radius:5px;padding:14px;font-family:var(--mono);font-size:11px;overflow-x:auto;color:var(--text)">'+esc(JSON.stringify(d.profile||d,null,2))+'</pre>';
|
|
822
|
+
}catch(e){el.innerHTML='<span style="color:var(--red)">'+esc(e.message)+'</span>'}
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// Install
|
|
827
|
+
const Install={
|
|
828
|
+
running:false,
|
|
829
|
+
setStep(n){
|
|
830
|
+
const all=['research','plan','install','connector','test','register'];
|
|
831
|
+
all.forEach(s=>{
|
|
832
|
+
const el=$('ist-'+s);if(!el)return;
|
|
833
|
+
el.className='ist';
|
|
834
|
+
if(s===n)el.classList.add('active');
|
|
835
|
+
else if(all.indexOf(s)<all.indexOf(n))el.classList.add('done');
|
|
836
|
+
});
|
|
837
|
+
},
|
|
838
|
+
log(html){const t=$('install-term');t.innerHTML+=html+'\n';t.scrollTop=t.scrollHeight},
|
|
839
|
+
clear(){
|
|
840
|
+
$('install-term').innerHTML='<span class="t-dim">Enter a URL or package name above and click Start.</span>';
|
|
841
|
+
$('install-url').value='';
|
|
842
|
+
['research','plan','install','connector','test','register'].forEach(s=>{const e=$('ist-'+s);if(e)e.className='ist'});
|
|
843
|
+
this.running=false;$('install-btn').disabled=false;
|
|
844
|
+
},
|
|
845
|
+
async start(){
|
|
846
|
+
const url=$('install-url').value.trim();
|
|
847
|
+
if(!url){toast('Enter a URL or framework name','yellow');return}
|
|
848
|
+
if(this.running)return;
|
|
849
|
+
this.running=true;$('install-btn').disabled=true;
|
|
850
|
+
$('install-term').innerHTML='';
|
|
851
|
+
this.setStep('research');
|
|
852
|
+
this.log('<span class="t-c">-- Installing: '+esc(url)+' --</span>');
|
|
853
|
+
try{
|
|
854
|
+
const r=await fetch('/api/admin/install/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});
|
|
855
|
+
if(r.headers.get('content-type')?.includes('text/event-stream')){
|
|
856
|
+
const reader=r.body.getReader(),dec=new TextDecoder();
|
|
857
|
+
while(true){
|
|
858
|
+
const{done,value}=await reader.read();if(done)break;
|
|
859
|
+
for(const line of dec.decode(value).split('\n')){
|
|
860
|
+
if(line.startsWith('data: ')){
|
|
861
|
+
try{const e=JSON.parse(line.slice(6));this.handleEvt(e)}catch{this.log(esc(line.slice(6)))}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}else{
|
|
866
|
+
const d=await r.json();
|
|
867
|
+
this.log('<span class="t-b">'+esc(d.message||JSON.stringify(d))+'</span>');
|
|
868
|
+
}
|
|
869
|
+
}catch(e){
|
|
870
|
+
this.log('<span class="t-y">Routing through gateway...</span>');
|
|
871
|
+
try{
|
|
872
|
+
const r=await fetch('/api/admin/gateway/rpc',{method:'POST',headers:{'Content-Type':'application/json'},
|
|
873
|
+
body:JSON.stringify({method:'chat.send',params:{sessionKey:'admin-install',message:'[ADMIN INSTALL] Install framework: '+url+'. Research, install, write connector, test, register. Report each step.'}})});
|
|
874
|
+
const d=await r.json();
|
|
875
|
+
this.log('<span class="t-g">✓ Agent dispatched</span>');
|
|
876
|
+
this.log('<span class="t-dim">'+esc((d.message||JSON.stringify(d)).slice(0,300))+'</span>');
|
|
877
|
+
}catch(e2){this.log('<span class="t-r">✕ '+esc(e2.message)+'</span>')}
|
|
878
|
+
}
|
|
879
|
+
this.running=false;$('install-btn').disabled=false;
|
|
880
|
+
},
|
|
881
|
+
handleEvt(e){
|
|
882
|
+
const sm={research:'research',plan:'plan',install:'install',connector:'connector',test:'test',register:'register'};
|
|
883
|
+
if(e.step&&sm[e.step])this.setStep(e.step);
|
|
884
|
+
if(e.message||e.data){
|
|
885
|
+
const cls=e.level==='error'?'t-r':e.level==='success'?'t-g':e.level==='section'?'t-sec':'t-dim';
|
|
886
|
+
this.log('<span class="'+cls+'">'+esc(e.message||e.data)+'</span>');
|
|
887
|
+
}
|
|
888
|
+
if(e.type==='done'){this.log('<span class="t-g">✓ Complete!</span>');this.setStep('register')}
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
// Tests
|
|
893
|
+
const TESTS=[
|
|
894
|
+
{id:'health',name:'Health Check',icon:'💚'},
|
|
895
|
+
{id:'profile',name:'Active Profile',icon:'🤖'},
|
|
896
|
+
{id:'gateway',name:'Gateway Connection',icon:'🔌'},
|
|
897
|
+
{id:'profiles_api',name:'Profiles API',icon:'📋'},
|
|
898
|
+
{id:'tts',name:'TTS Voices',icon:'🎙️'},
|
|
899
|
+
{id:'music',name:'Music Endpoint',icon:'🎵'},
|
|
900
|
+
{id:'dj_prompt',name:'DJ Prompt API',icon:'✏️'},
|
|
901
|
+
{id:'server_stats',name:'Server Stats',icon:'📊'},
|
|
902
|
+
{id:'session',name:'Session API',icon:'🔄'},
|
|
903
|
+
{id:'canvas',name:'Canvas API',icon:'🖥️'},
|
|
904
|
+
{id:'latency',name:'Latency Benchmark',icon:'⚡'},
|
|
905
|
+
{id:'conversation',name:'Conversation API',icon:'💬'},
|
|
906
|
+
];
|
|
907
|
+
const Tests={
|
|
908
|
+
res:{},
|
|
909
|
+
initSel(){
|
|
910
|
+
fetch('/api/profiles').then(r=>r.json()).then(d=>{
|
|
911
|
+
const s=$('test-sel');
|
|
912
|
+
s.innerHTML='<option value="active">Active ('+(d.active||'?')+')</option>'+(d.profiles||[]).map(p=>'<option value="'+esc(p.id)+'">'+esc(p.name)+'</option>').join('');
|
|
913
|
+
}).catch(()=>{});
|
|
914
|
+
},
|
|
915
|
+
renderList(running){
|
|
916
|
+
$('test-list').innerHTML=TESTS.map(t=>{
|
|
917
|
+
const r=this.res[t.id];
|
|
918
|
+
let cls='skip',txt='—',ms='';
|
|
919
|
+
if(r){cls=r.pass?'pass':'fail';txt=(r.pass?'✓ PASS':'✕ FAIL')+(r.detail?' — '+esc(r.detail):'');if(r.ms)ms=r.ms+'ms'}
|
|
920
|
+
if(running&&!r){cls='running';txt='...'}
|
|
921
|
+
return'<div class="test-row"><span class="test-icon">'+t.icon+'</span><span class="test-name">'+t.name+'</span><span class="test-res '+cls+'">'+txt+'</span><span class="test-ms">'+ms+'</span></div>'
|
|
922
|
+
}).join('');
|
|
923
|
+
},
|
|
924
|
+
async run(){
|
|
925
|
+
this.res={};$('test-sum').textContent='Running...';$('run-btn').disabled=true;this.renderList(true);
|
|
926
|
+
const run=async(id,fn)=>{const s=Date.now();try{const d=await fn();this.res[id]={pass:true,detail:d,ms:Date.now()-s}}catch(e){this.res[id]={pass:false,detail:e.message,ms:Date.now()-s}};this.renderList(true)};
|
|
927
|
+
await run('health',async()=>{const d=await(await fetch('/api/health')).json();if(d.status!=='ok')throw new Error(d.status);return d.service});
|
|
928
|
+
await run('profile',async()=>{const d=await(await fetch('/api/profiles/active')).json();return d.profile?.name||d.active||'?'});
|
|
929
|
+
await run('gateway',async()=>{const d=await(await fetch('/api/admin/gateway/status')).json();if(!d.ok&&!d.healthy&&!d.connected)throw new Error(d.error||'not connected');return'connected'});
|
|
930
|
+
await run('profiles_api',async()=>{const d=await(await fetch('/api/profiles')).json();if(!d.profiles)throw new Error('no profiles');return d.profiles.length+' profiles'});
|
|
931
|
+
await run('tts',async()=>{const r=await fetch('/api/tts/voices').catch(()=>null);return r?.ok?(await r.json()).voices?.length+' voices':'endpoint ok'});
|
|
932
|
+
await run('music',async()=>{const d=await(await fetch('/api/music?action=status')).json();return d.status||'ok'});
|
|
933
|
+
await run('dj_prompt',async()=>{const d=await(await fetch('/api/dj-prompt')).json();if(!d.prompt&&!d.text&&!d.content)throw new Error('no prompt');return'loaded'});
|
|
934
|
+
await run('server_stats',async()=>{const d=await(await fetch('/api/server-stats')).json();return'cpu '+(d.cpu_percent||'?')+'%'});
|
|
935
|
+
await run('session',async()=>{const r=await fetch('/api/session').catch(()=>null);return r?.ok?'ok':'endpoint ok'});
|
|
936
|
+
await run('canvas',async()=>{const d=await(await fetch('/api/canvas/manifest')).json();return(d.pages||[]).length+' pages'});
|
|
937
|
+
await run('latency',async()=>{const t=Date.now();await fetch('/api/health');return(Date.now()-t)+'ms'});
|
|
938
|
+
await run('conversation',async()=>'skipped (would use tokens)');
|
|
939
|
+
const pass=Object.values(this.res).filter(r=>r.pass).length;
|
|
940
|
+
$('test-sum').textContent=pass+'/'+TESTS.length+' passed';$('run-btn').disabled=false;this.renderList(false);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// Providers
|
|
945
|
+
const Providers={
|
|
946
|
+
sel:{llm:null,tts:null,stt:null},
|
|
947
|
+
PROV:{
|
|
948
|
+
llm:[
|
|
949
|
+
{id:'gateway',name:'OpenClaw Gateway',detail:'GLM-4.7-Flash · Full tools · Memory · Canvas',badge:'b-green',badgeTxt:'ACTIVE'},
|
|
950
|
+
{id:'zai_direct',name:'Z.AI Direct',detail:'GLM-4.5-Flash · No tools · Fast',badge:'b-gray',badgeTxt:'ALT'},
|
|
951
|
+
{id:'hume_evi',name:'Hume EVI',detail:'Built-in eLLM · Emotion-aware · Real-time',badge:'b-orange',badgeTxt:'CLOUD'},
|
|
952
|
+
{id:'elevenlabs',name:'ElevenLabs AI',detail:'Conversational AI · Built-in',badge:'b-orange',badgeTxt:'CLOUD'},
|
|
953
|
+
],
|
|
954
|
+
tts:[
|
|
955
|
+
{id:'supertonic',name:'Supertonic',detail:'Local ONNX · Free · 10 voices · ~2s',badge:'b-green',badgeTxt:'LOCAL'},
|
|
956
|
+
{id:'groq',name:'Groq Orpheus',detail:'Cloud API · Fast · ~300ms',badge:'b-blue',badgeTxt:'CLOUD'},
|
|
957
|
+
{id:'hume',name:'Hume EVI',detail:'Emotional · Real-time · Paired with Hume LLM',badge:'b-orange',badgeTxt:'CLOUD'},
|
|
958
|
+
{id:'elevenlabs',name:'ElevenLabs',detail:'High quality · Many voices · API',badge:'b-orange',badgeTxt:'CLOUD'},
|
|
959
|
+
],
|
|
960
|
+
stt:[
|
|
961
|
+
{id:'webspeech',name:'Web Speech API',detail:'Browser-native · Chrome/Edge · Free',badge:'b-green',badgeTxt:'LOCAL'},
|
|
962
|
+
{id:'groq_whisper',name:'Groq Whisper',detail:'Cloud · Fast · Accurate',badge:'b-blue',badgeTxt:'CLOUD'},
|
|
963
|
+
{id:'local_whisper',name:'Local Whisper',detail:'faster-whisper · Private · GPU optional',badge:'b-purple',badgeTxt:'LOCAL'},
|
|
964
|
+
]
|
|
965
|
+
},
|
|
966
|
+
load(){
|
|
967
|
+
this.sel={llm:'gateway',tts:'supertonic',stt:'webspeech'};
|
|
968
|
+
const mkCol=(type,label,items)=>
|
|
969
|
+
'<div class="prov-col">'+
|
|
970
|
+
'<div class="prov-col-hdr">'+label+'</div>'+
|
|
971
|
+
'<div class="prov-list">'+
|
|
972
|
+
items.map(p=>
|
|
973
|
+
'<div class="prov-item'+(this.sel[type]===p.id?' sel':'')+'" id="pi-'+type+'-'+p.id+'" onclick="Providers.select(\''+type+'\',\''+p.id+'\')">'+
|
|
974
|
+
'<div class="prov-radio"></div>'+
|
|
975
|
+
'<div style="flex:1;min-width:0">'+
|
|
976
|
+
'<div style="display:flex;align-items:center;gap:6px;margin-bottom:3px">'+
|
|
977
|
+
'<div class="prov-info-name">'+esc(p.name)+'</div>'+
|
|
978
|
+
'<span class="badge '+p.badge+'" style="font-size:9px">'+p.badgeTxt+'</span>'+
|
|
979
|
+
'</div>'+
|
|
980
|
+
'<div class="prov-info-detail">'+esc(p.detail)+'</div>'+
|
|
981
|
+
'<div class="prov-config">'+
|
|
982
|
+
(type==='llm'&&p.id==='gateway'?'<div class="fg"><label class="fl">Session Key</label><input type="text" value="voice-main-3"></div>':'')+
|
|
983
|
+
(type==='tts'&&p.id==='groq'?'<div class="fg"><label class="fl">API Key</label><input type="password" placeholder="gsk_..."></div>':'')+
|
|
984
|
+
(type==='tts'&&p.id==='supertonic'?'<div class="fg"><label class="fl" style="margin-bottom:4px">Speed</label><input type="range" min="0.5" max="2" step="0.1" value="1.1" style="width:100%"></div>':'')+
|
|
985
|
+
'<button class="test-btn" id="tb-'+type+'-'+p.id+'" onclick="Providers.test(\''+type+'\',\''+p.id+'\')">▶ Test Connection</button>'+
|
|
986
|
+
'</div>'+
|
|
987
|
+
'</div>'+
|
|
988
|
+
'</div>'
|
|
989
|
+
).join('')+
|
|
990
|
+
'</div>'+
|
|
991
|
+
'</div>';
|
|
992
|
+
$('prov-grid').innerHTML=mkCol('llm','LLM',this.PROV.llm)+mkCol('tts','TTS',this.PROV.tts)+mkCol('stt','STT',this.PROV.stt);
|
|
993
|
+
},
|
|
994
|
+
select(type,id){
|
|
995
|
+
this.sel[type]=id;
|
|
996
|
+
document.querySelectorAll('[id^="pi-'+type+'-"]').forEach(el=>el.classList.remove('sel'));
|
|
997
|
+
$('pi-'+type+'-'+id)?.classList.add('sel');
|
|
998
|
+
toast(type.toUpperCase()+': '+id,'blue');
|
|
999
|
+
},
|
|
1000
|
+
async test(type,id){
|
|
1001
|
+
const btn=$('tb-'+type+'-'+id);if(!btn)return;
|
|
1002
|
+
btn.className='test-btn testing';btn.textContent='Testing...';
|
|
1003
|
+
try{
|
|
1004
|
+
if(type==='llm'){const d=await(await fetch('/api/admin/gateway/status')).json();const ok=d.ok||d.healthy;btn.className='test-btn '+(ok?'pass':'fail');btn.textContent=ok?'✓ Connected':'✕ Failed'}
|
|
1005
|
+
else if(type==='tts'){const d=await(await fetch('/api/health/ready')).json();const ok=d.details?.tts?.healthy;btn.className='test-btn '+(ok?'pass':'fail');btn.textContent=ok?'✓ TTS Ready':'✕ TTS Error'}
|
|
1006
|
+
else{btn.className='test-btn pass';btn.textContent='✓ Browser API Ready'}
|
|
1007
|
+
}catch(e){btn.className='test-btn fail';btn.textContent='✕ Error: '+e.message.slice(0,30)}
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
// DJ Tools (Music Agent placeholder - no-op)
|
|
1012
|
+
const DJTools={load(){}};
|
|
1013
|
+
|
|
1014
|
+
// System
|
|
1015
|
+
const System={
|
|
1016
|
+
async load(){await Promise.all([this.health(),this.stats(),this.checkGW()])},
|
|
1017
|
+
async health(){
|
|
1018
|
+
const dot=$('health-dot'),txt=$('health-txt'),cards=$('hc-grid'),det=$('sys-health');
|
|
1019
|
+
try{
|
|
1020
|
+
const[lr,rr]=await Promise.all([fetch('/health/live'),fetch('/health/ready')]);
|
|
1021
|
+
const live=await lr.json(),ready=await rr.json();
|
|
1022
|
+
dot.className='dot'+(ready.healthy?'':' yellow');
|
|
1023
|
+
txt.textContent=ready.healthy?'All systems operational':'Degraded';
|
|
1024
|
+
const up=live.details?.uptime_seconds||0;
|
|
1025
|
+
const h=Math.floor(up/3600),m=Math.floor((up%3600)/60);
|
|
1026
|
+
cards.innerHTML=
|
|
1027
|
+
'<div class="hc"><div class="hc-label">Process</div><div class="hc-val g">Running</div><div class="hc-sub">'+h+'h '+m+'m uptime</div></div>'+
|
|
1028
|
+
'<div class="hc"><div class="hc-label">Gateway</div><div class="hc-val '+(ready.details?.gateway?.healthy?'g':'r')+'">'+(ready.details?.gateway?.healthy?'✓ OK':'✕ Down')+'</div><div class="hc-sub">'+esc(ready.details?.gateway?.message||'')+'</div></div>'+
|
|
1029
|
+
'<div class="hc"><div class="hc-label">TTS</div><div class="hc-val '+(ready.details?.tts?.healthy?'g':'y')+'">'+(ready.details?.tts?.healthy?'✓ Ready':'⚠ Check')+'</div><div class="hc-sub">'+esc(ready.details?.tts?.message||'')+'</div></div>';
|
|
1030
|
+
det.innerHTML=Object.entries(ready.details||{}).map(([k,v])=>'<div class="stat-r"><span class="stat-l">'+esc(k)+'</span><span class="stat-v '+(v.healthy?'g':'r')+'">'+(v.healthy?'✓':'✕')+' '+esc(v.message||'')+'</span></div>').join('')||'<div class="empty">No details</div>';
|
|
1031
|
+
}catch(e){dot.className='dot red';txt.textContent='Error';cards.innerHTML='<div class="hc"><div class="hc-label">Status</div><div class="hc-val r">Error</div></div>'}
|
|
1032
|
+
},
|
|
1033
|
+
async stats(){
|
|
1034
|
+
const el=$('sys-stats');
|
|
1035
|
+
try{
|
|
1036
|
+
const d=await(await fetch('/api/server-stats')).json();
|
|
1037
|
+
const cpu=d.cpu_percent||0;
|
|
1038
|
+
const ram=d.memory?.percent||d.ram_percent||0;
|
|
1039
|
+
const ramUsed=d.memory?.used_gb||d.ram_used_gb||'?';
|
|
1040
|
+
const ramTotal=d.memory?.total_gb||d.ram_total_gb||'?';
|
|
1041
|
+
const diskUsed=d.disk?.used_gb||d.disk_used_gb||'?';
|
|
1042
|
+
const diskTotal=d.disk?.total_gb||d.disk_total_gb||'?';
|
|
1043
|
+
const diskPct=d.disk?.percent||0;
|
|
1044
|
+
el.innerHTML=[
|
|
1045
|
+
['CPU',cpu+'%',cpu>80?'r':cpu>60?'y':'g'],
|
|
1046
|
+
['RAM',ramUsed+' / '+ramTotal+' GB',ram>85?'r':ram>70?'y':''],
|
|
1047
|
+
['RAM %',ram+'%',''],
|
|
1048
|
+
['Disk',diskUsed+' / '+diskTotal+' GB',diskPct>90?'r':diskPct>80?'y':''],
|
|
1049
|
+
['Uptime',d.uptime||'?',''],
|
|
1050
|
+
['Top Process',(d.top_processes||[])[0]?.name||'?',''],
|
|
1051
|
+
].map(([l,v,c])=>'<div class="stat-r"><span class="stat-l">'+l+'</span><span class="stat-v '+c+'">'+esc(String(v))+'</span></div>').join('');
|
|
1052
|
+
}catch(e){el.innerHTML='<span style="color:var(--red)">'+esc(e.message)+'</span>'}
|
|
1053
|
+
},
|
|
1054
|
+
async checkGW(){
|
|
1055
|
+
const el=$('sys-gw');
|
|
1056
|
+
el.innerHTML='<div class="loading"><div class="spin"></div>Checking...</div>';
|
|
1057
|
+
try{
|
|
1058
|
+
const d=await(await fetch('/api/admin/gateway/status')).json();
|
|
1059
|
+
const ok=d.ok||d.healthy||d.connected;
|
|
1060
|
+
el.innerHTML='<div class="stat-r"><span class="stat-l">Status</span><span class="stat-v '+(ok?'g':'r')+'">'+(ok?'✓ Connected':'✕ Disconnected')+'</span></div>'+
|
|
1061
|
+
'<div class="stat-r"><span class="stat-l">Message</span><span class="stat-v">'+esc(d.message||d.error||'—')+'</span></div>'+
|
|
1062
|
+
(d.latency_ms?'<div class="stat-r"><span class="stat-l">Latency</span><span class="stat-v">'+d.latency_ms+'ms</span></div>':'');
|
|
1063
|
+
}catch(e){el.innerHTML='<span style="color:var(--red)">'+esc(e.message)+'</span>'}
|
|
1064
|
+
},
|
|
1065
|
+
async resetSession(){
|
|
1066
|
+
if(!confirm('Reset voice session? Next message will be slower (cold start).'))return;
|
|
1067
|
+
try{const d=await(await fetch('/api/session/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})).json();toast(d.ok?'Session reset':d.error||'Failed',d.ok?'green':'red')}
|
|
1068
|
+
catch(e){toast(e.message,'red')}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// Instructions editor
|
|
1073
|
+
const Instructions = {
|
|
1074
|
+
files: [],
|
|
1075
|
+
current: null,
|
|
1076
|
+
dirty: false,
|
|
1077
|
+
|
|
1078
|
+
async load() {
|
|
1079
|
+
$('instr-list').innerHTML = '<div class="loading"><div class="spin"></div>Loading...</div>';
|
|
1080
|
+
try {
|
|
1081
|
+
const d = await (await fetch('/api/instructions')).json();
|
|
1082
|
+
this.files = d.files || [];
|
|
1083
|
+
this.renderList();
|
|
1084
|
+
// Auto-select first app-scope file
|
|
1085
|
+
if (!this.current) {
|
|
1086
|
+
const first = this.files.find(f => f.scope === 'app' && f.exists !== false);
|
|
1087
|
+
if (first) this.select(first.id);
|
|
1088
|
+
}
|
|
1089
|
+
} catch(e) {
|
|
1090
|
+
$('instr-list').innerHTML = '<div class="empty" style="padding:16px"><span style="color:var(--red)">'+esc(e.message)+'</span></div>';
|
|
1091
|
+
}
|
|
1092
|
+
},
|
|
1093
|
+
|
|
1094
|
+
renderList() {
|
|
1095
|
+
const app = this.files.filter(f => f.scope === 'app');
|
|
1096
|
+
const oc = this.files.filter(f => f.scope === 'openclaw');
|
|
1097
|
+
let html = '';
|
|
1098
|
+
if (app.length) {
|
|
1099
|
+
html += '<div style="padding:6px 12px 2px;font-size:10px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:var(--subtle)">This App</div>';
|
|
1100
|
+
html += app.map(f => this._fileItem(f)).join('');
|
|
1101
|
+
}
|
|
1102
|
+
if (oc.length) {
|
|
1103
|
+
html += '<div style="padding:10px 12px 2px;font-size:10px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:var(--subtle)">OpenClaw Workspace</div>';
|
|
1104
|
+
html += oc.map(f => this._fileItem(f)).join('');
|
|
1105
|
+
}
|
|
1106
|
+
$('instr-list').innerHTML = html || '<div class="empty" style="padding:16px">No files found</div>';
|
|
1107
|
+
},
|
|
1108
|
+
|
|
1109
|
+
_fileItem(f) {
|
|
1110
|
+
const active = f.id === this.current ? 'background:var(--blue-bg);color:var(--blue);border-left:2px solid var(--blue)' : 'border-left:2px solid transparent';
|
|
1111
|
+
const dot = f.scope === 'app'
|
|
1112
|
+
? '<span style="font-size:10px;color:var(--green)">●</span>'
|
|
1113
|
+
: '<span style="font-size:10px;color:var(--purple)">●</span>';
|
|
1114
|
+
const missing = !f.exists ? ' <span style="color:var(--dim);font-size:10px">(missing)</span>' : '';
|
|
1115
|
+
return '<div onclick="Instructions.select(\''+f.id+'\')" style="padding:8px 12px;cursor:pointer;font-size:13px;'+active+';display:flex;align-items:center;gap:6px;transition:background .1s" onmouseover="this.style.background=\'rgba(177,186,196,.06)\'" onmouseout="this.style.background=\''+(f.id===this.current?'var(--blue-bg)':'transparent')+'\'">'+dot+esc(f.label)+missing+'</div>';
|
|
1116
|
+
},
|
|
1117
|
+
|
|
1118
|
+
async select(id) {
|
|
1119
|
+
if (this.dirty && this.current) {
|
|
1120
|
+
if (!confirm('You have unsaved changes. Discard?')) return;
|
|
1121
|
+
}
|
|
1122
|
+
this.current = id;
|
|
1123
|
+
this.dirty = false;
|
|
1124
|
+
this.renderList();
|
|
1125
|
+
const meta = this.files.find(f => f.id === id);
|
|
1126
|
+
$('instr-editor-title').textContent = meta ? meta.label : id;
|
|
1127
|
+
$('instr-desc').textContent = meta ? meta.description : '';
|
|
1128
|
+
const badge = $('instr-badge');
|
|
1129
|
+
const saveBtn = $('instr-save-btn');
|
|
1130
|
+
if (meta) {
|
|
1131
|
+
badge.textContent = meta.scope === 'app' ? 'hot-reload' : 'openclaw';
|
|
1132
|
+
badge.className = 'badge ' + (meta.scope === 'app' ? 'b-green' : 'b-purple');
|
|
1133
|
+
badge.style.display = '';
|
|
1134
|
+
}
|
|
1135
|
+
saveBtn.style.display = meta && meta.scope === 'app' ? '' : 'none';
|
|
1136
|
+
$('instr-editor').value = '';
|
|
1137
|
+
$('instr-status').textContent = 'Loading...';
|
|
1138
|
+
try {
|
|
1139
|
+
const d = await (await fetch('/api/instructions/'+id)).json();
|
|
1140
|
+
if (d.error) { $('instr-status').textContent = 'Error: '+d.error; return; }
|
|
1141
|
+
$('instr-editor').value = d.content || '';
|
|
1142
|
+
$('instr-status').textContent = d.exists ? (d.size+' chars') : 'File does not exist yet — save to create it.';
|
|
1143
|
+
$('instr-editor').disabled = meta && meta.scope === 'openclaw';
|
|
1144
|
+
} catch(e) {
|
|
1145
|
+
$('instr-status').textContent = 'Error: '+e.message;
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
|
|
1149
|
+
onEdit() {
|
|
1150
|
+
this.dirty = true;
|
|
1151
|
+
const len = $('instr-editor').value.length;
|
|
1152
|
+
$('instr-status').textContent = len+' chars (unsaved)';
|
|
1153
|
+
},
|
|
1154
|
+
|
|
1155
|
+
async save() {
|
|
1156
|
+
if (!this.current) return;
|
|
1157
|
+
const content = $('instr-editor').value;
|
|
1158
|
+
$('instr-save-btn').disabled = true;
|
|
1159
|
+
try {
|
|
1160
|
+
const d = await (await fetch('/api/instructions/'+this.current, {
|
|
1161
|
+
method: 'PUT',
|
|
1162
|
+
headers: {'Content-Type':'application/json'},
|
|
1163
|
+
body: JSON.stringify({content})
|
|
1164
|
+
})).json();
|
|
1165
|
+
if (d.ok) {
|
|
1166
|
+
this.dirty = false;
|
|
1167
|
+
$('instr-status').textContent = d.message+' ('+d.size+' chars)';
|
|
1168
|
+
toast('Saved: '+this.current, 'green');
|
|
1169
|
+
} else {
|
|
1170
|
+
toast(d.error||'Save failed', 'red');
|
|
1171
|
+
}
|
|
1172
|
+
} catch(e) {
|
|
1173
|
+
toast(e.message, 'red');
|
|
1174
|
+
}
|
|
1175
|
+
$('instr-save-btn').disabled = false;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
// Toast
|
|
1180
|
+
let toastT;
|
|
1181
|
+
function toast(msg,color){
|
|
1182
|
+
color=color||'blue';
|
|
1183
|
+
const el=$('toast');
|
|
1184
|
+
const styles={green:'background:#0d2d1a;border:1px solid #3fb950;color:#3fb950',blue:'background:#0d1926;border:1px solid #1f6feb;color:#58a6ff',red:'background:#1a0810;border:1px solid #f85149;color:#f85149',yellow:'background:#1a1200;border:1px solid #e3b341;color:#e3b341'};
|
|
1185
|
+
el.setAttribute('style','position:fixed;bottom:24px;right:24px;padding:10px 18px;border-radius:8px;font-size:13px;z-index:9999;opacity:1;transition:opacity .3s;box-shadow:0 4px 20px rgba(0,0,0,.5);'+(styles[color]||styles.blue));
|
|
1186
|
+
el.textContent=msg;
|
|
1187
|
+
clearTimeout(toastT);toastT=setTimeout(()=>el.style.opacity='0',3000);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Keyboard
|
|
1191
|
+
document.addEventListener('keydown',function(e){
|
|
1192
|
+
if((e.ctrlKey||e.metaKey)&&e.key==='s'){
|
|
1193
|
+
e.preventDefault();
|
|
1194
|
+
if($('panel-builder').classList.contains('active')&&Builder.currentId) Builder.save(Builder.currentId);
|
|
1195
|
+
if($('panel-instructions').classList.contains('active')&&Instructions.current) Instructions.save();
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Canvas Pages manager
|
|
1200
|
+
const CanvasPages = {
|
|
1201
|
+
pages: {},
|
|
1202
|
+
async load() {
|
|
1203
|
+
const el = $('canvas-pages-list');
|
|
1204
|
+
el.innerHTML = '<div class="loading"><div class="spin"></div>Loading...</div>';
|
|
1205
|
+
try {
|
|
1206
|
+
const d = await (await fetch('/api/canvas/manifest')).json();
|
|
1207
|
+
this.pages = d.pages || {};
|
|
1208
|
+
this.render();
|
|
1209
|
+
} catch(e) {
|
|
1210
|
+
el.innerHTML = '<div class="empty">Failed to load: '+esc(e.message)+'</div>';
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
render() {
|
|
1214
|
+
const el = $('canvas-pages-list');
|
|
1215
|
+
const ids = Object.keys(this.pages);
|
|
1216
|
+
if (!ids.length) {
|
|
1217
|
+
el.innerHTML = '<div class="empty"><div class="empty-icon">🖥️</div>No canvas pages yet</div>';
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
el.innerHTML = '<table style="width:100%;border-collapse:collapse">' +
|
|
1221
|
+
'<thead><tr style="border-bottom:1px solid var(--border);color:var(--dim);font-size:12px">' +
|
|
1222
|
+
'<th style="padding:8px 12px;text-align:left">Page</th>' +
|
|
1223
|
+
'<th style="padding:8px 12px;text-align:left">URL</th>' +
|
|
1224
|
+
'<th style="padding:8px 12px;text-align:left">Category</th>' +
|
|
1225
|
+
'<th style="padding:8px 12px;text-align:center">Lock</th>' +
|
|
1226
|
+
'<th style="padding:8px 12px;text-align:right">Visibility</th>' +
|
|
1227
|
+
'</tr></thead><tbody>' +
|
|
1228
|
+
ids.map(id => {
|
|
1229
|
+
const p = this.pages[id];
|
|
1230
|
+
const pub = p.is_public || false;
|
|
1231
|
+
const locked = p.is_locked || false;
|
|
1232
|
+
return '<tr style="border-bottom:1px solid var(--border2)">' +
|
|
1233
|
+
'<td style="padding:10px 12px">' +
|
|
1234
|
+
'<div style="font-weight:500;color:var(--bright)">'+esc(p.display_name||id)+'</div>' +
|
|
1235
|
+
'<div style="font-size:11px;color:var(--dim);margin-top:2px">'+esc(id)+'</div>' +
|
|
1236
|
+
'</td>' +
|
|
1237
|
+
'<td style="padding:10px 12px">' +
|
|
1238
|
+
'<div style="display:flex;align-items:center;gap:6px">' +
|
|
1239
|
+
'<span style="font-size:11px;color:var(--dim);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+esc(location.origin+'/pages/'+id+'.html')+'">/pages/'+esc(id)+'.html</span>' +
|
|
1240
|
+
'<button onclick="CanvasPages.copyUrl(\''+esc(id)+'\')" title="Copy URL" style="' +
|
|
1241
|
+
'border:none;background:none;cursor:pointer;padding:2px;font-size:14px;color:var(--dim);opacity:0.7">' +
|
|
1242
|
+
'📋</button></div></td>' +
|
|
1243
|
+
'<td style="padding:10px 12px;color:var(--dim);font-size:12px">'+esc(p.category||'—')+'</td>' +
|
|
1244
|
+
'<td style="padding:10px 12px;text-align:center">' +
|
|
1245
|
+
'<button onclick="CanvasPages.toggleLock(\''+esc(id)+'\')" title="'+(locked?'Locked — agent cannot change visibility':'Unlocked — agent can change visibility')+'" style="' +
|
|
1246
|
+
'border:1px solid '+(locked?'var(--yellow, #f5a623)':'var(--border)')+';' +
|
|
1247
|
+
'background:'+(locked?'rgba(245,166,35,0.1)':'transparent')+';' +
|
|
1248
|
+
'color:'+(locked?'var(--yellow, #f5a623)':'var(--dim)')+';' +
|
|
1249
|
+
'padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px;transition:all .15s">' +
|
|
1250
|
+
(locked ? '🔐' : '🔓') +
|
|
1251
|
+
'</button></td>' +
|
|
1252
|
+
'<td style="padding:10px 12px;text-align:right">' +
|
|
1253
|
+
'<button onclick="CanvasPages.toggle(\''+esc(id)+'\')" style="' +
|
|
1254
|
+
'border:1px solid '+(pub?'var(--green)':'var(--border)')+';' +
|
|
1255
|
+
'background:'+(pub?'var(--green-bg)':'transparent')+';' +
|
|
1256
|
+
'color:'+(pub?'var(--green)':'var(--dim)')+';' +
|
|
1257
|
+
'padding:4px 12px;border-radius:20px;cursor:pointer;font-size:12px;transition:all .15s">' +
|
|
1258
|
+
(pub ? '🌐 Public' : '🔒 Private') +
|
|
1259
|
+
'</button></td></tr>';
|
|
1260
|
+
}).join('') +
|
|
1261
|
+
'</tbody></table>';
|
|
1262
|
+
},
|
|
1263
|
+
async toggle(id) {
|
|
1264
|
+
const page = this.pages[id];
|
|
1265
|
+
if (!page) return;
|
|
1266
|
+
const newPublic = !(page.is_public || false);
|
|
1267
|
+
try {
|
|
1268
|
+
const r = await fetch('/api/canvas/manifest/page/'+id, {
|
|
1269
|
+
method: 'PATCH',
|
|
1270
|
+
headers: {'Content-Type':'application/json'},
|
|
1271
|
+
body: JSON.stringify({is_public: newPublic})
|
|
1272
|
+
});
|
|
1273
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
1274
|
+
this.pages[id].is_public = newPublic;
|
|
1275
|
+
this.render();
|
|
1276
|
+
toast((newPublic ? '🌐 Public: ' : '🔒 Private: ') + esc(page.display_name||id), newPublic?'green':'blue');
|
|
1277
|
+
} catch(e) {
|
|
1278
|
+
toast('Failed: '+e.message, 'red');
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
copyUrl(id) {
|
|
1282
|
+
const url = location.origin + '/pages/' + id + '.html';
|
|
1283
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
1284
|
+
toast('Copied: ' + url, 'blue');
|
|
1285
|
+
}).catch(() => {
|
|
1286
|
+
// Fallback for non-HTTPS contexts
|
|
1287
|
+
const ta = document.createElement('textarea');
|
|
1288
|
+
ta.value = url; document.body.appendChild(ta); ta.select();
|
|
1289
|
+
document.execCommand('copy'); document.body.removeChild(ta);
|
|
1290
|
+
toast('Copied: ' + url, 'blue');
|
|
1291
|
+
});
|
|
1292
|
+
},
|
|
1293
|
+
async toggleLock(id) {
|
|
1294
|
+
const page = this.pages[id];
|
|
1295
|
+
if (!page) return;
|
|
1296
|
+
const newLocked = !(page.is_locked || false);
|
|
1297
|
+
try {
|
|
1298
|
+
const r = await fetch('/api/canvas/manifest/page/'+id, {
|
|
1299
|
+
method: 'PATCH',
|
|
1300
|
+
headers: {'Content-Type':'application/json'},
|
|
1301
|
+
body: JSON.stringify({is_locked: newLocked})
|
|
1302
|
+
});
|
|
1303
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
1304
|
+
this.pages[id].is_locked = newLocked;
|
|
1305
|
+
this.render();
|
|
1306
|
+
toast((newLocked ? '🔐 Locked: ' : '🔓 Unlocked: ') + esc(page.display_name||id), newLocked?'yellow':'blue');
|
|
1307
|
+
} catch(e) {
|
|
1308
|
+
toast('Failed: '+e.message, 'red');
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// Clerk auth gate for admin panel (skipped in local mode — no Clerk key configured)
|
|
1314
|
+
async function initAdminAuth() {
|
|
1315
|
+
// If Clerk SDK wasn't injected (no key in env), skip auth entirely
|
|
1316
|
+
if (!document.querySelector('script[data-clerk-publishable-key]')) return;
|
|
1317
|
+
for (let i = 0; i < 10; i++) {
|
|
1318
|
+
if (typeof Clerk !== 'undefined') break;
|
|
1319
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1320
|
+
}
|
|
1321
|
+
if (typeof Clerk === 'undefined') return; // skip if Clerk unavailable
|
|
1322
|
+
await Clerk.load();
|
|
1323
|
+
if (!Clerk.user) {
|
|
1324
|
+
// Redirect to main app login
|
|
1325
|
+
location.href = '/?redirect=/admin' + location.hash;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Init
|
|
1330
|
+
addEventListener('DOMContentLoaded', async function(){
|
|
1331
|
+
await initAdminAuth();
|
|
1332
|
+
System.health();
|
|
1333
|
+
const hash=location.hash.slice(1);
|
|
1334
|
+
const valid=['agents','builder','frameworks','install','tests','providers','djtools','system','instructions','canvas'];
|
|
1335
|
+
show(valid.includes(hash)?hash:'agents');
|
|
1336
|
+
});
|
|
1337
|
+
</script>
|
|
1338
|
+
</body>
|
|
1339
|
+
</html>
|