agentgui 1.0.926 → 1.0.928
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/package.json +1 -1
- package/site/app/index.html +3 -17
- package/site/app/js/app.js +231 -102
package/package.json
CHANGED
package/site/app/index.html
CHANGED
|
@@ -3,29 +3,15 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>agentgui
|
|
7
|
-
<meta name="description" content="agentgui live client
|
|
6
|
+
<title>agentgui</title>
|
|
7
|
+
<meta name="description" content="agentgui — live client for any acptoapi backend.">
|
|
8
8
|
<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@latest/dist/247420.css">
|
|
9
9
|
<script type="importmap">
|
|
10
|
-
{ "imports": {
|
|
11
|
-
"anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js"
|
|
12
|
-
} }
|
|
10
|
+
{ "imports": { "anentrypoint-design": "https://unpkg.com/anentrypoint-design@latest/dist/247420.js" } }
|
|
13
11
|
</script>
|
|
14
12
|
<style>
|
|
15
13
|
html, body { margin: 0; height: 100%; }
|
|
16
14
|
#app { height: 100vh; }
|
|
17
|
-
.app { height: 100vh; }
|
|
18
|
-
.app-main { display: flex; flex-direction: column; min-height: 0; }
|
|
19
|
-
.chat { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
|
20
|
-
.chat-thread { flex: 1; overflow-y: auto; }
|
|
21
|
-
.agentgui-history-pane { display: grid; grid-template-columns: 300px 1fr; height: 100%; min-height: 0; overflow: hidden; }
|
|
22
|
-
.agentgui-history-list { overflow-y: auto; border-right: 1px solid var(--panel-3); padding: 8px; }
|
|
23
|
-
.agentgui-history-detail { overflow-y: auto; padding: 16px; }
|
|
24
|
-
.agentgui-ev { padding: 6px 0; border-bottom: 1px solid var(--panel-2); font-family: var(--ff-mono); font-size: 12px; }
|
|
25
|
-
.agentgui-ev .h { opacity: .55; margin-bottom: 2px; }
|
|
26
|
-
.agentgui-ev pre { white-space: pre-wrap; margin: 0; }
|
|
27
|
-
.agentgui-model-bar { display: flex; gap: 8px; padding: 8px 16px; border-bottom: 1px solid var(--panel-3); align-items: center; }
|
|
28
|
-
.agentgui-model-bar select { padding: 5px 8px; border-radius: 8px; border: 1px solid var(--panel-3); background: var(--panel-1); color: var(--ink); font-family: var(--ff-ui); font-size: 13px; }
|
|
29
15
|
</style>
|
|
30
16
|
</head>
|
|
31
17
|
<body>
|
package/site/app/js/app.js
CHANGED
|
@@ -3,12 +3,11 @@ import * as B from './backend.js';
|
|
|
3
3
|
|
|
4
4
|
installStyles().catch(() => {});
|
|
5
5
|
|
|
6
|
-
const { AppShell, Topbar, Crumb, Side, Status,
|
|
7
|
-
const { Chat, ChatComposer, ChatMessage } = C;
|
|
8
|
-
const { Row } = C;
|
|
6
|
+
const { AppShell, Topbar, Crumb, Side, Status, Chat, ChatComposer, Row, Panel } = C;
|
|
9
7
|
|
|
10
8
|
const state = {
|
|
11
9
|
backend: B.getBackend(),
|
|
10
|
+
backendDraft: B.getBackend(),
|
|
12
11
|
health: { status: 'unknown' },
|
|
13
12
|
tab: 'chat',
|
|
14
13
|
models: [],
|
|
@@ -23,51 +22,92 @@ const state = {
|
|
|
23
22
|
|
|
24
23
|
let render;
|
|
25
24
|
|
|
26
|
-
function
|
|
27
|
-
|
|
25
|
+
function timeNow() {
|
|
26
|
+
const d = new Date();
|
|
27
|
+
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function navTo(tab) {
|
|
31
|
+
state.tab = tab;
|
|
32
|
+
if (tab === 'history') refreshHistory();
|
|
33
|
+
render();
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function view() {
|
|
31
37
|
const ok = state.health.status === 'ok';
|
|
32
|
-
const
|
|
33
|
-
? h('span', { key: 'hc', style: 'color:var(--live);font-size:12px' }, '● connected')
|
|
34
|
-
: h('span', { key: 'hc', style: 'color:var(--flame);font-size:12px' }, '○ offline');
|
|
38
|
+
const dot = h('span', { key: 'dot' }, ok ? '● connected' : '○ offline');
|
|
35
39
|
|
|
36
40
|
const topbar = Topbar({
|
|
37
41
|
brand: 'agentgui',
|
|
38
42
|
leaf: state.tab,
|
|
39
|
-
items:
|
|
43
|
+
items: [['chat', '#'], ['history', '#'], ['settings', '#']],
|
|
40
44
|
active: state.tab,
|
|
41
|
-
onNav: (label) =>
|
|
45
|
+
onNav: (label) => navTo(label),
|
|
42
46
|
});
|
|
43
47
|
|
|
48
|
+
const crumbRight = state.tab === 'chat'
|
|
49
|
+
? [
|
|
50
|
+
h('select', {
|
|
51
|
+
key: 'modelsel',
|
|
52
|
+
onchange: (e) => { state.selectedModel = e.target.value; render(); },
|
|
53
|
+
},
|
|
54
|
+
h('option', { key: '__', value: '' }, '— model —'),
|
|
55
|
+
...state.models.map(m =>
|
|
56
|
+
h('option', { key: m.id, value: m.id, selected: m.id === state.selectedModel }, m.id)
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
state.chat.busy
|
|
60
|
+
? h('a', { key: 'stop', onclick: cancelChat, style: 'cursor:pointer' }, '◼ stop')
|
|
61
|
+
: h('a', { key: 'new', onclick: newChat, style: 'cursor:pointer' }, '+ new'),
|
|
62
|
+
dot,
|
|
63
|
+
]
|
|
64
|
+
: [dot];
|
|
65
|
+
|
|
44
66
|
const crumb = Crumb({
|
|
45
67
|
trail: ['agentgui'],
|
|
46
68
|
leaf: state.tab,
|
|
47
|
-
right:
|
|
69
|
+
right: crumbRight,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const navSide = Side({
|
|
73
|
+
sections: [
|
|
74
|
+
{
|
|
75
|
+
group: 'navigate',
|
|
76
|
+
items: [
|
|
77
|
+
{ glyph: '▣', label: 'chat', key: 'chat', active: state.tab === 'chat',
|
|
78
|
+
onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
79
|
+
{ glyph: '§', label: 'history', key: 'history', active: state.tab === 'history',
|
|
80
|
+
onClick: (e) => { e.preventDefault(); navTo('history'); } },
|
|
81
|
+
{ glyph: '⌘', label: 'settings', key: 'settings', active: state.tab === 'settings',
|
|
82
|
+
onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
48
86
|
});
|
|
87
|
+
const side = state.tab === 'history' ? historySide() : navSide;
|
|
49
88
|
|
|
50
|
-
const side = state.tab === 'history' ? historySide() : null;
|
|
51
|
-
const main = mainContent();
|
|
52
89
|
const status = Status({
|
|
53
|
-
left: [state.backend],
|
|
54
|
-
right: [state.selectedModel
|
|
90
|
+
left: [state.backend, ok ? '● live' : '○ offline'],
|
|
91
|
+
right: [state.selectedModel ? '⌘ ' + state.selectedModel : '○ no model'],
|
|
55
92
|
});
|
|
56
93
|
|
|
57
|
-
return AppShell({ topbar, crumb, side, main, status });
|
|
94
|
+
return AppShell({ topbar, crumb, side, main: mainContent(), status });
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
function mainContent() {
|
|
61
|
-
if (state.tab === 'chat')
|
|
62
|
-
if (state.tab === 'history') return
|
|
98
|
+
if (state.tab === 'chat') return chatMain();
|
|
99
|
+
if (state.tab === 'history') return historyMain();
|
|
63
100
|
return settingsMain();
|
|
64
101
|
}
|
|
65
102
|
|
|
103
|
+
// ── chat ───────────────────────────────────────────────────────────────────
|
|
66
104
|
function chatMain() {
|
|
67
105
|
const msgs = state.chat.messages.map((m, i) => ({
|
|
68
|
-
key: i,
|
|
106
|
+
key: String(i),
|
|
69
107
|
who: m.role === 'user' ? 'you' : 'them',
|
|
70
|
-
|
|
108
|
+
name: m.role === 'assistant' ? (state.selectedModel || 'agent') : 'you',
|
|
109
|
+
time: m.time || '',
|
|
110
|
+
parts: [{ kind: 'text', text: m.content || '' }],
|
|
71
111
|
}));
|
|
72
112
|
|
|
73
113
|
const composer = ChatComposer({
|
|
@@ -78,89 +118,30 @@ function chatMain() {
|
|
|
78
118
|
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
79
119
|
});
|
|
80
120
|
|
|
81
|
-
const modelBar = h('div', { class: 'agentgui-model-bar' },
|
|
82
|
-
h('select', { onchange: (e) => { state.selectedModel = e.target.value; render(); } },
|
|
83
|
-
h('option', { value: '' }, '— choose model —'),
|
|
84
|
-
...state.models.map(m => h('option', { value: m.id, selected: m.id === state.selectedModel }, m.id)),
|
|
85
|
-
),
|
|
86
|
-
state.chat.busy
|
|
87
|
-
? h('a', { style: 'cursor:pointer;font-size:12px;color:var(--flame)', onclick: cancelChat }, 'stop ✕')
|
|
88
|
-
: null,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
121
|
return [
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
Chat({
|
|
123
|
+
title: state.selectedModel || 'agent',
|
|
124
|
+
sub: state.chat.busy ? 'streaming…' : (state.chat.messages.length + ' messages'),
|
|
125
|
+
messages: msgs,
|
|
126
|
+
composer,
|
|
127
|
+
}),
|
|
94
128
|
];
|
|
95
129
|
}
|
|
96
130
|
|
|
97
|
-
function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
key: 'sr' + i,
|
|
102
|
-
title: r.snippet || '(no snippet)',
|
|
103
|
-
sub: r.project + ' · ' + r.role + (r.tool ? ' · ' + r.tool : ''),
|
|
104
|
-
active: false,
|
|
105
|
-
onClick: () => loadSession(r.sid),
|
|
106
|
-
})
|
|
107
|
-
)
|
|
108
|
-
: state.sessions.slice(0, 80).map((s, i) =>
|
|
109
|
-
Row({
|
|
110
|
-
key: 'sess' + i,
|
|
111
|
-
title: s.title || s.project || s.sid,
|
|
112
|
-
sub: s.events + ' ev · ' + s.tools + ' tools · ' + (s.errors ? s.errors + ' err' : 'ok'),
|
|
113
|
-
active: s.sid === state.selectedSid,
|
|
114
|
-
onClick: () => loadSession(s.sid),
|
|
115
|
-
})
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
return h('div', { style: 'display:flex;flex-direction:column;height:100%;overflow:hidden' },
|
|
119
|
-
h('div', { style: 'padding:8px' },
|
|
120
|
-
h('input', {
|
|
121
|
-
style: 'width:100%;padding:6px 10px;border-radius:8px;border:1px solid var(--panel-3);background:var(--panel-1);color:var(--ink);font-family:var(--ff-ui);font-size:13px',
|
|
122
|
-
placeholder: 'search…',
|
|
123
|
-
value: state.searchQ,
|
|
124
|
-
onchange: (e) => { state.searchQ = e.target.value; runSearch(); },
|
|
125
|
-
}),
|
|
126
|
-
),
|
|
127
|
-
h('div', { style: 'flex:1;overflow-y:auto;padding:8px' }, ...rows,
|
|
128
|
-
rows.length === 0 ? h('div', { style: 'opacity:.6;padding:16px;font-size:13px' }, 'no sessions') : null,
|
|
129
|
-
),
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function historyDetail() {
|
|
134
|
-
if (!state.selectedSid) return h('div', { style: 'padding:24px;opacity:.6' }, 'pick a session');
|
|
135
|
-
if (state.events.length === 0) return h('div', { style: 'padding:24px;opacity:.6' }, 'loading…');
|
|
136
|
-
return h('div', {},
|
|
137
|
-
...state.events.map((e, i) =>
|
|
138
|
-
h('div', { key: 'ev' + i, class: 'agentgui-ev' },
|
|
139
|
-
h('div', { class: 'h' },
|
|
140
|
-
new Date(e.ts).toLocaleString() + ' · ' + (e.role || '?') + ' · ' + (e.type || '?') + (e.tool ? ' · ' + e.tool : ''),
|
|
141
|
-
),
|
|
142
|
-
h('pre', {}, (e.text || '').slice(0, 4000)),
|
|
143
|
-
)
|
|
144
|
-
),
|
|
145
|
-
);
|
|
131
|
+
function newChat() {
|
|
132
|
+
state.chat.abort?.abort();
|
|
133
|
+
state.chat = { messages: [], busy: false, abort: null, draft: '' };
|
|
134
|
+
render();
|
|
146
135
|
}
|
|
147
136
|
|
|
148
|
-
function
|
|
149
|
-
return h('div', { style: 'padding:24px;max-width:480px' },
|
|
150
|
-
h('h2', {}, 'settings'),
|
|
151
|
-
h('p', {}, 'backend: ', h('code', {}, state.backend)),
|
|
152
|
-
h('p', {}, 'health: ', h('code', {}, JSON.stringify(state.health))),
|
|
153
|
-
h('p', { style: 'opacity:.7;font-size:13px' },
|
|
154
|
-
'pass ', h('code', {}, '?backend=https://your-acptoapi-host'), ' or set localStorage[\'agentgui.backend\'] to override',
|
|
155
|
-
),
|
|
156
|
-
);
|
|
157
|
-
}
|
|
137
|
+
function cancelChat() { state.chat.abort?.abort(); }
|
|
158
138
|
|
|
159
139
|
async function sendChat() {
|
|
160
140
|
const text = (state.chat.draft || '').trim();
|
|
161
141
|
if (!text || !state.selectedModel || state.chat.busy) return;
|
|
162
|
-
|
|
163
|
-
state.chat.messages.push({ role: '
|
|
142
|
+
const t = timeNow();
|
|
143
|
+
state.chat.messages.push({ role: 'user', content: text, time: t });
|
|
144
|
+
state.chat.messages.push({ role: 'assistant', content: '', time: t });
|
|
164
145
|
state.chat.draft = '';
|
|
165
146
|
state.chat.busy = true;
|
|
166
147
|
const ctrl = new AbortController();
|
|
@@ -173,11 +154,11 @@ async function sendChat() {
|
|
|
173
154
|
messages: state.chat.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content })),
|
|
174
155
|
signal: ctrl.signal,
|
|
175
156
|
})) {
|
|
176
|
-
if (ev.type === 'text')
|
|
157
|
+
if (ev.type === 'text') { cur.content += ev.text; render(); }
|
|
177
158
|
if (ev.type === 'error') { cur.content += '\n[error] ' + JSON.stringify(ev.error); render(); }
|
|
178
159
|
}
|
|
179
160
|
} catch (e) {
|
|
180
|
-
cur.content += '\n[error] ' + e.message;
|
|
161
|
+
if (e.name !== 'AbortError') cur.content += '\n[error] ' + e.message;
|
|
181
162
|
} finally {
|
|
182
163
|
state.chat.busy = false;
|
|
183
164
|
state.chat.abort = null;
|
|
@@ -185,8 +166,142 @@ async function sendChat() {
|
|
|
185
166
|
}
|
|
186
167
|
}
|
|
187
168
|
|
|
188
|
-
|
|
169
|
+
// ── history ────────────────────────────────────────────────────────────────
|
|
170
|
+
function historyMain() {
|
|
171
|
+
const head = h('div', { class: 'ds-section' },
|
|
172
|
+
h('h1', {}, '§ history'),
|
|
173
|
+
h('p', { class: 'lede' },
|
|
174
|
+
state.selectedSid
|
|
175
|
+
? 'session ' + state.selectedSid
|
|
176
|
+
: 'pick a session from the sidebar — events stream from acptoapi /v1/history.',
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!state.selectedSid) return [head];
|
|
181
|
+
if (state.events.length === 0) return [head, Panel({ title: 'events', children: h('p', { class: 'lede' }, '◌ loading…') })];
|
|
182
|
+
|
|
183
|
+
const rows = state.events.map((e, i) =>
|
|
184
|
+
Row({
|
|
185
|
+
key: 'ev' + i,
|
|
186
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
187
|
+
title: (e.text || '').slice(0, 200) || '(empty)',
|
|
188
|
+
sub: new Date(e.ts).toLocaleString() + ' · ' + (e.role || '?') + ' · ' + (e.type || '?') + (e.tool ? ' · ⌘ ' + e.tool : ''),
|
|
189
|
+
rail: e.role === 'error' ? 'flame' : (e.role === 'user' ? 'green' : 'purple'),
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return [
|
|
194
|
+
head,
|
|
195
|
+
Panel({
|
|
196
|
+
title: state.events.length + ' events',
|
|
197
|
+
children: h('div', { class: 'ds-section' }, ...rows),
|
|
198
|
+
}),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function historySide() {
|
|
203
|
+
const searching = !!state.searchHits;
|
|
204
|
+
const rows = searching
|
|
205
|
+
? state.searchHits.results.slice(0, 60).map((r, i) =>
|
|
206
|
+
Row({
|
|
207
|
+
key: 'sr' + i,
|
|
208
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
209
|
+
title: r.snippet || '(no snippet)',
|
|
210
|
+
sub: (r.project || '?') + ' · ' + (r.role || '?') + (r.tool ? ' · ' + r.tool : ''),
|
|
211
|
+
rail: 'purple',
|
|
212
|
+
onClick: () => loadSession(r.sid),
|
|
213
|
+
})
|
|
214
|
+
)
|
|
215
|
+
: state.sessions.slice(0, 120).map((s, i) =>
|
|
216
|
+
Row({
|
|
217
|
+
key: 'sess' + i,
|
|
218
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
219
|
+
title: s.title || s.project || s.sid,
|
|
220
|
+
sub: s.events + ' ev · ' + s.tools + ' tools' + (s.errors ? ' · ' + s.errors + ' err' : ''),
|
|
221
|
+
rail: s.errors ? 'flame' : 'green',
|
|
222
|
+
active: s.sid === state.selectedSid,
|
|
223
|
+
onClick: () => loadSession(s.sid),
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return [
|
|
228
|
+
Side({
|
|
229
|
+
sections: [
|
|
230
|
+
{
|
|
231
|
+
group: 'navigate',
|
|
232
|
+
items: [
|
|
233
|
+
{ glyph: '▣', label: 'chat', key: 'chat', onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
234
|
+
{ glyph: '§', label: 'history', key: 'history', active: true },
|
|
235
|
+
{ glyph: '⌘', label: 'settings', key: 'settings', onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
}),
|
|
240
|
+
Panel({
|
|
241
|
+
title: searching ? 'matches' : 'sessions',
|
|
242
|
+
children: h('div', { class: 'ds-section' },
|
|
243
|
+
h('input', {
|
|
244
|
+
type: 'search',
|
|
245
|
+
placeholder: 'search sessions…',
|
|
246
|
+
value: state.searchQ,
|
|
247
|
+
oninput: (e) => { state.searchQ = e.target.value; runSearch(); },
|
|
248
|
+
}),
|
|
249
|
+
rows.length ? h('div', {}, ...rows) : h('p', { class: 'lede' }, 'no sessions yet'),
|
|
250
|
+
),
|
|
251
|
+
}),
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── settings ───────────────────────────────────────────────────────────────
|
|
256
|
+
function settingsMain() {
|
|
257
|
+
const ok = state.health.status === 'ok';
|
|
258
|
+
return [
|
|
259
|
+
h('div', { class: 'ds-section' },
|
|
260
|
+
h('h1', {}, '⌘ settings'),
|
|
261
|
+
h('p', { class: 'lede' }, 'point agentgui at any acptoapi backend. ?backend=… in the URL or the field below — both persist via localStorage.'),
|
|
262
|
+
Panel({
|
|
263
|
+
title: 'backend',
|
|
264
|
+
children: h('div', { class: 'ds-section' },
|
|
265
|
+
h('p', { class: 'lede' }, 'backend url'),
|
|
266
|
+
h('input', {
|
|
267
|
+
type: 'text',
|
|
268
|
+
value: state.backendDraft,
|
|
269
|
+
oninput: (e) => { state.backendDraft = e.target.value; render(); },
|
|
270
|
+
}),
|
|
271
|
+
h('p', { class: 'lede' }, (ok ? '● ' : '○ ') + JSON.stringify(state.health)),
|
|
272
|
+
h('button', {
|
|
273
|
+
onclick: () => {
|
|
274
|
+
B.setBackend(state.backendDraft);
|
|
275
|
+
state.backend = state.backendDraft;
|
|
276
|
+
state.health = { status: 'unknown' };
|
|
277
|
+
render();
|
|
278
|
+
init();
|
|
279
|
+
},
|
|
280
|
+
}, 'save + reconnect'),
|
|
281
|
+
),
|
|
282
|
+
}),
|
|
283
|
+
Panel({
|
|
284
|
+
title: 'models',
|
|
285
|
+
children: h('div', { class: 'ds-section' },
|
|
286
|
+
state.models.length
|
|
287
|
+
? h('div', {}, ...state.models.slice(0, 40).map((m, i) =>
|
|
288
|
+
Row({
|
|
289
|
+
key: 'm' + i,
|
|
290
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
291
|
+
title: m.id,
|
|
292
|
+
sub: m.owned_by || m.object || 'model',
|
|
293
|
+
rail: m.id === state.selectedModel ? 'green' : 'purple',
|
|
294
|
+
onClick: () => { state.selectedModel = m.id; render(); },
|
|
295
|
+
})
|
|
296
|
+
))
|
|
297
|
+
: h('p', { class: 'lede' }, 'no models loaded'),
|
|
298
|
+
),
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
];
|
|
302
|
+
}
|
|
189
303
|
|
|
304
|
+
// ── data ──────────────────────────────────────────────────────────────────
|
|
190
305
|
async function refreshHistory() {
|
|
191
306
|
try { state.sessions = await B.listSessions(state.backend); render(); }
|
|
192
307
|
catch (e) { console.warn('history fetch failed:', e.message); }
|
|
@@ -194,19 +309,33 @@ async function refreshHistory() {
|
|
|
194
309
|
|
|
195
310
|
async function runSearch() {
|
|
196
311
|
if (!state.searchQ.trim()) { state.searchHits = null; render(); return; }
|
|
197
|
-
try {
|
|
198
|
-
|
|
312
|
+
try {
|
|
313
|
+
state.searchHits = await B.searchHistory(state.backend, state.searchQ, 50);
|
|
314
|
+
render();
|
|
315
|
+
} catch (e) {
|
|
316
|
+
state.searchHits = { query: state.searchQ, results: [], error: e.message };
|
|
317
|
+
render();
|
|
318
|
+
}
|
|
199
319
|
}
|
|
200
320
|
|
|
201
321
|
async function loadSession(sid) {
|
|
202
|
-
state.selectedSid = sid;
|
|
322
|
+
state.selectedSid = sid;
|
|
323
|
+
state.events = [];
|
|
324
|
+
render();
|
|
203
325
|
try { state.events = await B.getSessionEvents(state.backend, sid); render(); }
|
|
204
|
-
catch (e) {
|
|
326
|
+
catch (e) {
|
|
327
|
+
state.events = [{ ts: Date.now(), role: 'error', type: 'fetch', text: e.message }];
|
|
328
|
+
render();
|
|
329
|
+
}
|
|
205
330
|
}
|
|
206
331
|
|
|
207
332
|
async function init() {
|
|
208
|
-
|
|
209
|
-
|
|
333
|
+
try {
|
|
334
|
+
const r = await B.probeBackend(state.backend);
|
|
335
|
+
state.health = r.ok ? { status: 'ok', ...r.info } : { status: 'down', ...r };
|
|
336
|
+
} catch (e) {
|
|
337
|
+
state.health = { status: 'error', error: e.message };
|
|
338
|
+
}
|
|
210
339
|
render();
|
|
211
340
|
try {
|
|
212
341
|
state.models = await B.listModels(state.backend);
|