aiden-runtime 3.16.2 → 3.18.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/README.md +185 -7
- package/config/devos.config.json +29 -19
- package/config/hardware.json +2 -2
- package/dist/api/dashboard.js +480 -0
- package/dist/api/server.js +150 -142
- package/dist/core/agentLoop.js +94 -13
- package/dist/core/channels/email.js +1 -1
- package/dist/core/modelRegistry.js +261 -0
- package/dist/core/permissionSystem.js +239 -0
- package/dist/core/pluginLoader.js +161 -0
- package/dist/core/skillLoader.js +6 -24
- package/dist/core/toolRegistry.js +316 -31
- package/dist/core/version.js +1 -1
- package/dist/providers/router.js +2 -1
- package/dist-bundle/cli.js +50946 -29225
- package/dist-bundle/index.js +6462 -5274
- package/package.json +3 -2
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// api/dashboard.ts — Aiden local web dashboard
|
|
3
|
+
// Served at GET /ui — single self-contained HTML, no external build step.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.getDashboardHTML = getDashboardHTML;
|
|
6
|
+
function getDashboardHTML() {
|
|
7
|
+
return `<!DOCTYPE html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="UTF-8"/>
|
|
11
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
12
|
+
<title>Aiden Dashboard</title>
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
14
|
+
<style>
|
|
15
|
+
:root {
|
|
16
|
+
--bg: #0D0D0D;
|
|
17
|
+
--surface: #1A1A1A;
|
|
18
|
+
--surface2: #222222;
|
|
19
|
+
--border: #2A2A2A;
|
|
20
|
+
--orange: #FF6B35;
|
|
21
|
+
--orange-dim: #c0521e;
|
|
22
|
+
--text: #E8E8E8;
|
|
23
|
+
--text-dim: #888;
|
|
24
|
+
--green: #4CAF50;
|
|
25
|
+
--red: #F44336;
|
|
26
|
+
--yellow: #FFC107;
|
|
27
|
+
--radius: 8px;
|
|
28
|
+
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
29
|
+
}
|
|
30
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
31
|
+
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:14px;height:100vh;display:flex;flex-direction:column;overflow:hidden}
|
|
32
|
+
|
|
33
|
+
/* ── Top bar ── */
|
|
34
|
+
#topbar{display:flex;align-items:center;gap:12px;padding:0 20px;height:48px;border-bottom:1px solid var(--border);background:var(--surface);flex-shrink:0}
|
|
35
|
+
#logo{font-weight:700;font-size:16px;color:var(--orange);letter-spacing:.5px}
|
|
36
|
+
#status-dot{width:8px;height:8px;border-radius:50%;background:var(--red);transition:background .4s}
|
|
37
|
+
#status-dot.ok{background:var(--green)}
|
|
38
|
+
#status-text{color:var(--text-dim);font-size:12px}
|
|
39
|
+
#topbar-right{margin-left:auto;display:flex;gap:8px;align-items:center}
|
|
40
|
+
#version-label{color:var(--text-dim);font-size:11px}
|
|
41
|
+
|
|
42
|
+
/* ── Tab bar ── */
|
|
43
|
+
#tabbar{display:flex;gap:2px;padding:8px 20px 0;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}
|
|
44
|
+
.tab{padding:8px 18px;border-radius:6px 6px 0 0;cursor:pointer;font-size:13px;font-weight:500;color:var(--text-dim);border:1px solid transparent;border-bottom:none;background:transparent;transition:all .15s}
|
|
45
|
+
.tab:hover{color:var(--text);background:var(--surface2)}
|
|
46
|
+
.tab.active{color:var(--orange);background:var(--bg);border-color:var(--border)}
|
|
47
|
+
|
|
48
|
+
/* ── Main content ── */
|
|
49
|
+
#panels{flex:1;overflow:hidden;display:flex;flex-direction:column}
|
|
50
|
+
.panel{display:none;flex:1;overflow:hidden;flex-direction:column}
|
|
51
|
+
.panel.active{display:flex}
|
|
52
|
+
|
|
53
|
+
/* ── Shared card ── */
|
|
54
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:12px}
|
|
55
|
+
.card-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-dim);margin-bottom:10px}
|
|
56
|
+
|
|
57
|
+
/* ── Scrollable inner ── */
|
|
58
|
+
.scroll{overflow-y:auto;flex:1;padding:16px 20px}
|
|
59
|
+
.scroll::-webkit-scrollbar{width:6px}
|
|
60
|
+
.scroll::-webkit-scrollbar-track{background:transparent}
|
|
61
|
+
.scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
62
|
+
|
|
63
|
+
/* ─────────────────── CHAT ─────────────────── */
|
|
64
|
+
#chat-wrap{display:flex;flex:1;flex-direction:column;overflow:hidden}
|
|
65
|
+
#chat-messages{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:10px}
|
|
66
|
+
#chat-messages::-webkit-scrollbar{width:6px}
|
|
67
|
+
#chat-messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
68
|
+
.msg{max-width:80%;border-radius:10px;padding:10px 14px;line-height:1.55;font-size:13.5px}
|
|
69
|
+
.msg.user{align-self:flex-end;background:var(--orange);color:#fff;border-bottom-right-radius:2px}
|
|
70
|
+
.msg.aiden{align-self:flex-start;background:var(--surface);border:1px solid var(--border);color:var(--text);border-bottom-left-radius:2px}
|
|
71
|
+
.msg.aiden pre{background:var(--bg);border:1px solid var(--border);border-radius:5px;padding:8px 10px;overflow-x:auto;font-size:12px;margin-top:8px}
|
|
72
|
+
.msg.aiden code{font-size:12px;background:var(--bg);padding:1px 4px;border-radius:3px}
|
|
73
|
+
.msg.system{align-self:center;color:var(--text-dim);font-size:12px;font-style:italic}
|
|
74
|
+
.msg.thinking{align-self:flex-start;color:var(--text-dim);font-size:12px;font-style:italic;padding:6px 10px}
|
|
75
|
+
#chat-input-bar{padding:12px 20px;border-top:1px solid var(--border);display:flex;gap:8px;flex-shrink:0;background:var(--surface)}
|
|
76
|
+
#chat-input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:9px 14px;color:var(--text);font-size:14px;resize:none;min-height:40px;max-height:120px;line-height:1.4;font-family:var(--font)}
|
|
77
|
+
#chat-input:focus{outline:none;border-color:var(--orange)}
|
|
78
|
+
#send-btn{background:var(--orange);color:#fff;border:none;border-radius:8px;padding:9px 18px;font-size:14px;font-weight:600;cursor:pointer;flex-shrink:0;transition:background .15s}
|
|
79
|
+
#send-btn:hover{background:var(--orange-dim)}
|
|
80
|
+
#send-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
81
|
+
#session-bar{padding:6px 20px;font-size:11px;color:var(--text-dim);border-bottom:1px solid var(--border);display:flex;gap:16px;align-items:center;flex-shrink:0;background:var(--surface)}
|
|
82
|
+
.sess-btn{background:transparent;border:1px solid var(--border);color:var(--text-dim);border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer}
|
|
83
|
+
.sess-btn:hover{border-color:var(--orange);color:var(--orange)}
|
|
84
|
+
|
|
85
|
+
/* ─────────────────── PROVIDERS ─────────────────── */
|
|
86
|
+
.provider-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}
|
|
87
|
+
.pcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px}
|
|
88
|
+
.pcard-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
|
|
89
|
+
.pcard-name{font-weight:600;font-size:13px}
|
|
90
|
+
.badge{font-size:10px;padding:2px 7px;border-radius:20px;font-weight:600;text-transform:uppercase;letter-spacing:.4px}
|
|
91
|
+
.badge.ok{background:#1a3a1a;color:var(--green)}
|
|
92
|
+
.badge.err{background:#3a1a1a;color:var(--red)}
|
|
93
|
+
.badge.warn{background:#3a2a00;color:var(--yellow)}
|
|
94
|
+
.pcard-model{font-size:11px;color:var(--text-dim);margin-top:2px}
|
|
95
|
+
.pcard-latency{font-size:11px;color:var(--text-dim);margin-top:4px}
|
|
96
|
+
|
|
97
|
+
/* ─────────────────── MEMORY ─────────────────── */
|
|
98
|
+
.mem-item{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;margin-bottom:8px;display:flex;gap:10px;align-items:flex-start}
|
|
99
|
+
.mem-icon{color:var(--orange);font-size:16px;flex-shrink:0;margin-top:1px}
|
|
100
|
+
.mem-text{font-size:13px;line-height:1.5;color:var(--text);flex:1}
|
|
101
|
+
.mem-meta{font-size:10px;color:var(--text-dim);margin-top:3px}
|
|
102
|
+
#mem-search{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:8px 13px;color:var(--text);font-size:13px;margin-bottom:14px}
|
|
103
|
+
#mem-search:focus{outline:none;border-color:var(--orange)}
|
|
104
|
+
.mem-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-dim);margin:14px 0 8px}
|
|
105
|
+
|
|
106
|
+
/* ─────────────────── SKILLS ─────────────────── */
|
|
107
|
+
.skill-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}
|
|
108
|
+
.scard{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px}
|
|
109
|
+
.scard-name{font-weight:600;font-size:13px;color:var(--orange);margin-bottom:4px}
|
|
110
|
+
.scard-desc{font-size:12px;color:var(--text-dim);line-height:1.4}
|
|
111
|
+
.scard-trigger{font-size:10px;color:var(--text-dim);margin-top:5px;font-family:monospace}
|
|
112
|
+
|
|
113
|
+
/* ─────────────────── REFRESH ─────────────────── */
|
|
114
|
+
.refresh-btn{background:transparent;border:1px solid var(--border);color:var(--text-dim);border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;transition:all .15s}
|
|
115
|
+
.refresh-btn:hover{border-color:var(--orange);color:var(--orange)}
|
|
116
|
+
.panel-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 20px 0;flex-shrink:0}
|
|
117
|
+
.panel-toolbar h2{font-size:14px;font-weight:600;color:var(--text)}
|
|
118
|
+
</style>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
|
|
122
|
+
<!-- Top bar -->
|
|
123
|
+
<div id="topbar">
|
|
124
|
+
<span id="logo">⬡ Aiden</span>
|
|
125
|
+
<span id="status-dot"></span>
|
|
126
|
+
<span id="status-text">connecting…</span>
|
|
127
|
+
<div id="topbar-right">
|
|
128
|
+
<span id="version-label"></span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<!-- Tab bar -->
|
|
133
|
+
<div id="tabbar">
|
|
134
|
+
<div class="tab active" data-tab="chat">💬 Chat</div>
|
|
135
|
+
<div class="tab" data-tab="providers">⚡ Providers</div>
|
|
136
|
+
<div class="tab" data-tab="memory">🧠 Memory</div>
|
|
137
|
+
<div class="tab" data-tab="skills">🎯 Skills</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Panels -->
|
|
141
|
+
<div id="panels">
|
|
142
|
+
|
|
143
|
+
<!-- ── CHAT ── -->
|
|
144
|
+
<div class="panel active" id="panel-chat">
|
|
145
|
+
<div id="session-bar">
|
|
146
|
+
<span id="session-label">Session: —</span>
|
|
147
|
+
<button class="sess-btn" id="new-session-btn">+ New session</button>
|
|
148
|
+
<button class="sess-btn" id="clear-chat-btn">✕ Clear</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div id="chat-wrap">
|
|
151
|
+
<div id="chat-messages">
|
|
152
|
+
<div class="msg system">Aiden is running locally. Ask anything.</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div id="chat-input-bar">
|
|
155
|
+
<textarea id="chat-input" placeholder="Message Aiden…" rows="1"></textarea>
|
|
156
|
+
<button id="send-btn">Send</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- ── PROVIDERS ── -->
|
|
162
|
+
<div class="panel" id="panel-providers">
|
|
163
|
+
<div class="panel-toolbar">
|
|
164
|
+
<h2>LLM Providers</h2>
|
|
165
|
+
<button class="refresh-btn" id="refresh-providers">↻ Refresh</button>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="scroll">
|
|
168
|
+
<div id="providers-content"><p style="color:var(--text-dim);padding:20px 0">Loading…</p></div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- ── MEMORY ── -->
|
|
173
|
+
<div class="panel" id="panel-memory">
|
|
174
|
+
<div class="panel-toolbar">
|
|
175
|
+
<h2>Memory</h2>
|
|
176
|
+
<button class="refresh-btn" id="refresh-memory">↻ Refresh</button>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="scroll">
|
|
179
|
+
<input id="mem-search" type="text" placeholder="Search memory…" autocomplete="off"/>
|
|
180
|
+
<div id="memory-content"><p style="color:var(--text-dim)">Loading…</p></div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- ── SKILLS ── -->
|
|
185
|
+
<div class="panel" id="panel-skills">
|
|
186
|
+
<div class="panel-toolbar">
|
|
187
|
+
<h2>Skills</h2>
|
|
188
|
+
<button class="refresh-btn" id="refresh-skills">↻ Refresh</button>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="scroll">
|
|
191
|
+
<div id="skills-content"><p style="color:var(--text-dim);padding:20px 0">Loading…</p></div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
</div><!-- /panels -->
|
|
196
|
+
|
|
197
|
+
<script>
|
|
198
|
+
// ── Config ──────────────────────────────────────────────────
|
|
199
|
+
const BASE = window.location.origin // e.g. http://localhost:4200
|
|
200
|
+
|
|
201
|
+
// ── Tabs ────────────────────────────────────────────────────
|
|
202
|
+
const tabs = document.querySelectorAll('.tab')
|
|
203
|
+
const panels = document.querySelectorAll('.panel')
|
|
204
|
+
tabs.forEach(tab => {
|
|
205
|
+
tab.addEventListener('click', () => {
|
|
206
|
+
const id = tab.dataset.tab
|
|
207
|
+
tabs.forEach(t => t.classList.remove('active'))
|
|
208
|
+
panels.forEach(p => p.classList.remove('active'))
|
|
209
|
+
tab.classList.add('active')
|
|
210
|
+
document.getElementById('panel-' + id).classList.add('active')
|
|
211
|
+
if (id === 'providers') loadProviders()
|
|
212
|
+
if (id === 'memory') loadMemory()
|
|
213
|
+
if (id === 'skills') loadSkills()
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// ── Status bar ──────────────────────────────────────────────
|
|
218
|
+
async function ping() {
|
|
219
|
+
try {
|
|
220
|
+
const r = await fetch(BASE + '/api/ping', { cache: 'no-store' })
|
|
221
|
+
const d = await r.json()
|
|
222
|
+
document.getElementById('status-dot').className = 'ok'
|
|
223
|
+
document.getElementById('status-text').textContent = 'online'
|
|
224
|
+
document.getElementById('version-label').textContent = 'v' + (d.version || '')
|
|
225
|
+
} catch {
|
|
226
|
+
document.getElementById('status-dot').className = ''
|
|
227
|
+
document.getElementById('status-text').textContent = 'offline'
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
ping()
|
|
231
|
+
setInterval(ping, 10000)
|
|
232
|
+
|
|
233
|
+
// ── Chat ─────────────────────────────────────────────────────
|
|
234
|
+
let sessionId = null
|
|
235
|
+
const chatBox = document.getElementById('chat-messages')
|
|
236
|
+
const chatInput = document.getElementById('chat-input')
|
|
237
|
+
const sendBtn = document.getElementById('send-btn')
|
|
238
|
+
|
|
239
|
+
function addMsg(role, html) {
|
|
240
|
+
const el = document.createElement('div')
|
|
241
|
+
el.className = 'msg ' + role
|
|
242
|
+
el.innerHTML = html
|
|
243
|
+
chatBox.appendChild(el)
|
|
244
|
+
chatBox.scrollTop = chatBox.scrollHeight
|
|
245
|
+
return el
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function mdToHtml(text) {
|
|
249
|
+
try { return marked.parse(text) } catch { return escHtml(text) }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function escHtml(s) {
|
|
253
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
chatInput.addEventListener('keydown', e => {
|
|
257
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() }
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
document.getElementById('send-btn').addEventListener('click', sendMessage)
|
|
261
|
+
|
|
262
|
+
document.getElementById('clear-chat-btn').addEventListener('click', () => {
|
|
263
|
+
chatBox.innerHTML = '<div class="msg system">Cleared.</div>'
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
document.getElementById('new-session-btn').addEventListener('click', () => {
|
|
267
|
+
sessionId = null
|
|
268
|
+
document.getElementById('session-label').textContent = 'Session: —'
|
|
269
|
+
chatBox.innerHTML = '<div class="msg system">New session started.</div>'
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
async function sendMessage() {
|
|
273
|
+
const text = chatInput.value.trim()
|
|
274
|
+
if (!text) return
|
|
275
|
+
chatInput.value = ''
|
|
276
|
+
chatInput.style.height = ''
|
|
277
|
+
sendBtn.disabled = true
|
|
278
|
+
addMsg('user', escHtml(text))
|
|
279
|
+
|
|
280
|
+
const thinkEl = addMsg('thinking', '⋯ thinking')
|
|
281
|
+
let aiEl = null
|
|
282
|
+
let buffer = ''
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const body = { message: text }
|
|
286
|
+
if (sessionId) body.sessionId = sessionId
|
|
287
|
+
|
|
288
|
+
const resp = await fetch(BASE + '/api/chat', {
|
|
289
|
+
method: 'POST',
|
|
290
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
|
|
291
|
+
body: JSON.stringify(body)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const reader = resp.body.getReader()
|
|
295
|
+
const dec = new TextDecoder()
|
|
296
|
+
let partial = ''
|
|
297
|
+
|
|
298
|
+
while (true) {
|
|
299
|
+
const { done, value } = await reader.read()
|
|
300
|
+
if (done) break
|
|
301
|
+
partial += dec.decode(value, { stream: true })
|
|
302
|
+
const lines = partial.split('\\n')
|
|
303
|
+
partial = lines.pop()
|
|
304
|
+
for (const line of lines) {
|
|
305
|
+
if (!line.startsWith('data:')) continue
|
|
306
|
+
const raw = line.slice(5).trim()
|
|
307
|
+
if (!raw || raw === '[DONE]') continue
|
|
308
|
+
let ev
|
|
309
|
+
try { ev = JSON.parse(raw) } catch { continue }
|
|
310
|
+
|
|
311
|
+
if (ev.sessionId && !sessionId) {
|
|
312
|
+
sessionId = ev.sessionId
|
|
313
|
+
document.getElementById('session-label').textContent = 'Session: ' + sessionId.slice(0,8)
|
|
314
|
+
}
|
|
315
|
+
// server sends { token, done: false } — no type field
|
|
316
|
+
if (ev.token || ev.delta) {
|
|
317
|
+
if (thinkEl && thinkEl.parentNode) thinkEl.remove()
|
|
318
|
+
if (!aiEl) aiEl = addMsg('aiden', '')
|
|
319
|
+
buffer += (ev.delta || ev.token || '')
|
|
320
|
+
aiEl.innerHTML = mdToHtml(buffer)
|
|
321
|
+
chatBox.scrollTop = chatBox.scrollHeight
|
|
322
|
+
}
|
|
323
|
+
// server sends { tool: "name", message: "...", timestamp } for tool progress
|
|
324
|
+
if (ev.tool) {
|
|
325
|
+
if (thinkEl && thinkEl.parentNode) thinkEl.innerHTML = '🔧 ' + escHtml(ev.tool)
|
|
326
|
+
}
|
|
327
|
+
// server sends { activity: { icon, message, style, rawTool? }, done: false }
|
|
328
|
+
if (ev.activity) {
|
|
329
|
+
const act = ev.activity
|
|
330
|
+
if (thinkEl && thinkEl.parentNode) {
|
|
331
|
+
thinkEl.innerHTML = (act.icon ? act.icon + ' ' : '🔧 ') + escHtml(act.message || act.rawTool || 'working…')
|
|
332
|
+
}
|
|
333
|
+
if (act.style === 'error' && !buffer) {
|
|
334
|
+
if (thinkEl && thinkEl.parentNode) thinkEl.remove()
|
|
335
|
+
addMsg('system', '⚠ ' + escHtml(act.message || 'Error'))
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// server sends { thinking: { stage, message } }
|
|
339
|
+
if (ev.thinking) {
|
|
340
|
+
if (thinkEl && thinkEl.parentNode) thinkEl.innerHTML = '⋯ ' + escHtml(ev.thinking.message || 'thinking…')
|
|
341
|
+
}
|
|
342
|
+
// server sends { done: true } — no type field
|
|
343
|
+
if (ev.done === true) {
|
|
344
|
+
if (thinkEl && thinkEl.parentNode) thinkEl.remove()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (thinkEl && thinkEl.parentNode) thinkEl.remove()
|
|
349
|
+
if (!aiEl && !buffer) addMsg('system', '(no response)')
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (thinkEl && thinkEl.parentNode) thinkEl.remove()
|
|
352
|
+
addMsg('system', '⚠ ' + escHtml(err.message))
|
|
353
|
+
} finally {
|
|
354
|
+
sendBtn.disabled = false
|
|
355
|
+
chatInput.focus()
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Auto-resize textarea
|
|
360
|
+
chatInput.addEventListener('input', () => {
|
|
361
|
+
chatInput.style.height = 'auto'
|
|
362
|
+
chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px'
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// ── Providers ────────────────────────────────────────────────
|
|
366
|
+
async function loadProviders() {
|
|
367
|
+
const el = document.getElementById('providers-content')
|
|
368
|
+
try {
|
|
369
|
+
const [stateRes, statusRes] = await Promise.all([
|
|
370
|
+
fetch(BASE + '/api/providers/state').then(r => r.json()),
|
|
371
|
+
fetch(BASE + '/api/providers/status').then(r => r.json()).catch(() => ({}))
|
|
372
|
+
])
|
|
373
|
+
|
|
374
|
+
const providers = Array.isArray(stateRes) ? stateRes
|
|
375
|
+
: (stateRes.providers || Object.entries(stateRes).map(([k,v]) => ({ name: k, ...v })))
|
|
376
|
+
|
|
377
|
+
if (!providers.length) { el.innerHTML = '<p style="color:var(--text-dim)">No providers configured.</p>'; return }
|
|
378
|
+
|
|
379
|
+
const statusMap = {}
|
|
380
|
+
if (statusRes && typeof statusRes === 'object') {
|
|
381
|
+
const arr = Array.isArray(statusRes) ? statusRes : Object.entries(statusRes).map(([k,v])=>({name:k,...v}))
|
|
382
|
+
arr.forEach(p => { statusMap[p.name || p.id] = p })
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
el.innerHTML = '<div class="provider-grid">' + providers.map(p => {
|
|
386
|
+
const name = p.name || p.id || '?'
|
|
387
|
+
const status = p.status || statusMap[name]?.status || 'unknown'
|
|
388
|
+
const badgeCls = status === 'ok' || status === 'healthy' || status === 'active' ? 'ok'
|
|
389
|
+
: status === 'error' || status === 'down' ? 'err' : 'warn'
|
|
390
|
+
const model = p.currentModel || p.model || p.defaultModel || ''
|
|
391
|
+
const latency = p.latency || p.avgLatency || statusMap[name]?.latency || ''
|
|
392
|
+
return \`<div class="pcard">
|
|
393
|
+
<div class="pcard-header">
|
|
394
|
+
<span class="pcard-name">\${escHtml(name)}</span>
|
|
395
|
+
<span class="badge \${badgeCls}">\${escHtml(status)}</span>
|
|
396
|
+
</div>
|
|
397
|
+
\${model ? \`<div class="pcard-model">Model: \${escHtml(model)}</div>\` : ''}
|
|
398
|
+
\${latency ? \`<div class="pcard-latency">Latency: \${typeof latency==='number' ? latency+'ms' : escHtml(String(latency))}</div>\` : ''}
|
|
399
|
+
</div>\`
|
|
400
|
+
}).join('') + '</div>'
|
|
401
|
+
} catch (err) {
|
|
402
|
+
el.innerHTML = '<p style="color:var(--red)">Failed to load providers: ' + escHtml(err.message) + '</p>'
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
document.getElementById('refresh-providers').addEventListener('click', loadProviders)
|
|
406
|
+
|
|
407
|
+
// ── Memory ───────────────────────────────────────────────────
|
|
408
|
+
let allMemories = []
|
|
409
|
+
|
|
410
|
+
async function loadMemory() {
|
|
411
|
+
const el = document.getElementById('memory-content')
|
|
412
|
+
try {
|
|
413
|
+
const data = await fetch(BASE + '/api/memory').then(r => r.json())
|
|
414
|
+
allMemories = Array.isArray(data) ? data : (data.memories || data.items || [])
|
|
415
|
+
renderMemory(allMemories)
|
|
416
|
+
} catch (err) {
|
|
417
|
+
el.innerHTML = '<p style="color:var(--red)">Failed to load memory: ' + escHtml(err.message) + '</p>'
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function renderMemory(items) {
|
|
422
|
+
const el = document.getElementById('memory-content')
|
|
423
|
+
if (!items.length) { el.innerHTML = '<p style="color:var(--text-dim)">No memories stored yet.</p>'; return }
|
|
424
|
+
el.innerHTML = items.map(m => {
|
|
425
|
+
const text = m.content || m.text || m.value || JSON.stringify(m)
|
|
426
|
+
const meta = [m.type, m.category, m.created_at || m.createdAt].filter(Boolean).join(' · ')
|
|
427
|
+
return \`<div class="mem-item">
|
|
428
|
+
<span class="mem-icon">◈</span>
|
|
429
|
+
<div>
|
|
430
|
+
<div class="mem-text">\${escHtml(String(text))}</div>
|
|
431
|
+
\${meta ? \`<div class="mem-meta">\${escHtml(meta)}</div>\` : ''}
|
|
432
|
+
</div>
|
|
433
|
+
</div>\`
|
|
434
|
+
}).join('')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let memSearchTimer = null
|
|
438
|
+
document.getElementById('mem-search').addEventListener('input', e => {
|
|
439
|
+
clearTimeout(memSearchTimer)
|
|
440
|
+
const q = e.target.value.trim()
|
|
441
|
+
memSearchTimer = setTimeout(async () => {
|
|
442
|
+
if (!q) { renderMemory(allMemories); return }
|
|
443
|
+
try {
|
|
444
|
+
const data = await fetch(BASE + '/api/memory/search?q=' + encodeURIComponent(q)).then(r => r.json())
|
|
445
|
+
const results = Array.isArray(data) ? data : (data.results || data.memories || [])
|
|
446
|
+
renderMemory(results)
|
|
447
|
+
} catch { renderMemory(allMemories.filter(m => JSON.stringify(m).toLowerCase().includes(q.toLowerCase()))) }
|
|
448
|
+
}, 300)
|
|
449
|
+
})
|
|
450
|
+
document.getElementById('refresh-memory').addEventListener('click', loadMemory)
|
|
451
|
+
|
|
452
|
+
// ── Skills ───────────────────────────────────────────────────
|
|
453
|
+
async function loadSkills() {
|
|
454
|
+
const el = document.getElementById('skills-content')
|
|
455
|
+
try {
|
|
456
|
+
const data = await fetch(BASE + '/api/skills/learned').then(r => r.json())
|
|
457
|
+
const skills = Array.isArray(data) ? data : (data.skills || data.items || [])
|
|
458
|
+
if (!skills.length) { el.innerHTML = '<p style="color:var(--text-dim)">No skills loaded.</p>'; return }
|
|
459
|
+
el.innerHTML = '<div class="skill-grid">' + skills.map(s => {
|
|
460
|
+
const name = s.name || s.id || '?'
|
|
461
|
+
const desc = s.description || s.desc || ''
|
|
462
|
+
const trigger = s.trigger || s.command || ''
|
|
463
|
+
return \`<div class="scard">
|
|
464
|
+
<div class="scard-name">\${escHtml(name)}</div>
|
|
465
|
+
\${desc ? \`<div class="scard-desc">\${escHtml(desc)}</div>\` : ''}
|
|
466
|
+
\${trigger ? \`<div class="scard-trigger">trigger: \${escHtml(trigger)}</div>\` : ''}
|
|
467
|
+
</div>\`
|
|
468
|
+
}).join('') + '</div>'
|
|
469
|
+
} catch (err) {
|
|
470
|
+
el.innerHTML = '<p style="color:var(--red)">Failed to load skills: ' + escHtml(err.message) + '</p>'
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
document.getElementById('refresh-skills').addEventListener('click', loadSkills)
|
|
474
|
+
|
|
475
|
+
// ── Init ─────────────────────────────────────────────────────
|
|
476
|
+
chatInput.focus()
|
|
477
|
+
</script>
|
|
478
|
+
</body>
|
|
479
|
+
</html>`;
|
|
480
|
+
}
|