agentgui 1.0.926 → 1.0.927
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 +241 -101
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,53 +22,96 @@ 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 = 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
|
|
|
44
48
|
const crumb = Crumb({
|
|
45
49
|
trail: ['agentgui'],
|
|
46
50
|
leaf: state.tab,
|
|
47
|
-
right: [
|
|
51
|
+
right: [dot],
|
|
48
52
|
});
|
|
49
53
|
|
|
50
|
-
const
|
|
51
|
-
|
|
54
|
+
const navSide = Side({
|
|
55
|
+
sections: [
|
|
56
|
+
{
|
|
57
|
+
group: 'navigate',
|
|
58
|
+
items: [
|
|
59
|
+
{ glyph: '▣', label: 'chat', key: 'chat', active: state.tab === 'chat',
|
|
60
|
+
onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
61
|
+
{ glyph: '§', label: 'history', key: 'history', active: state.tab === 'history',
|
|
62
|
+
onClick: (e) => { e.preventDefault(); navTo('history'); } },
|
|
63
|
+
{ glyph: '⌘', label: 'settings', key: 'settings', active: state.tab === 'settings',
|
|
64
|
+
onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
const side = state.tab === 'history' ? historySide() : navSide;
|
|
70
|
+
|
|
52
71
|
const status = Status({
|
|
53
|
-
left: [state.backend],
|
|
54
|
-
right: [state.selectedModel
|
|
72
|
+
left: [state.backend, ok ? '● live' : '○ offline'],
|
|
73
|
+
right: [state.selectedModel ? '⌘ ' + state.selectedModel : '○ no model'],
|
|
55
74
|
});
|
|
56
75
|
|
|
57
|
-
return AppShell({ topbar, crumb, side, main, status });
|
|
76
|
+
return AppShell({ topbar, crumb, side, main: mainContent(), status });
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
function mainContent() {
|
|
61
|
-
if (state.tab === 'chat')
|
|
62
|
-
if (state.tab === 'history') return
|
|
80
|
+
if (state.tab === 'chat') return chatMain();
|
|
81
|
+
if (state.tab === 'history') return historyMain();
|
|
63
82
|
return settingsMain();
|
|
64
83
|
}
|
|
65
84
|
|
|
85
|
+
// ── chat ───────────────────────────────────────────────────────────────────
|
|
66
86
|
function chatMain() {
|
|
67
87
|
const msgs = state.chat.messages.map((m, i) => ({
|
|
68
|
-
key: i,
|
|
88
|
+
key: String(i),
|
|
69
89
|
who: m.role === 'user' ? 'you' : 'them',
|
|
70
|
-
|
|
90
|
+
name: m.role === 'assistant' ? (state.selectedModel || 'agent') : 'you',
|
|
91
|
+
time: m.time || '',
|
|
92
|
+
parts: [{ kind: 'text', text: m.content || '' }],
|
|
71
93
|
}));
|
|
72
94
|
|
|
95
|
+
const modelPanel = Panel({
|
|
96
|
+
title: 'model',
|
|
97
|
+
children: h('div', { class: 'ds-section' },
|
|
98
|
+
h('select', {
|
|
99
|
+
value: state.selectedModel,
|
|
100
|
+
onchange: (e) => { state.selectedModel = e.target.value; render(); },
|
|
101
|
+
},
|
|
102
|
+
h('option', { value: '' }, '— choose model —'),
|
|
103
|
+
...state.models.map(m =>
|
|
104
|
+
h('option', { value: m.id, selected: m.id === state.selectedModel }, m.id)
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
h('p', { class: 'lede' },
|
|
108
|
+
state.chat.busy
|
|
109
|
+
? h('button', { onclick: cancelChat }, '◼ stop')
|
|
110
|
+
: h('button', { onclick: newChat }, '+ new chat'),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
});
|
|
114
|
+
|
|
73
115
|
const composer = ChatComposer({
|
|
74
116
|
value: state.chat.draft,
|
|
75
117
|
disabled: state.chat.busy,
|
|
@@ -78,89 +120,39 @@ function chatMain() {
|
|
|
78
120
|
onSend: (v) => { state.chat.draft = v; sendChat(); },
|
|
79
121
|
});
|
|
80
122
|
|
|
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
123
|
return [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(); },
|
|
124
|
+
h('div', { class: 'ds-section' },
|
|
125
|
+
h('h1', {}, '# ' + (state.selectedModel || 'agent')),
|
|
126
|
+
h('p', { class: 'lede' },
|
|
127
|
+
state.chat.messages.length
|
|
128
|
+
? state.chat.messages.length + ' messages in this thread'
|
|
129
|
+
: 'start a conversation with the selected model.',
|
|
130
|
+
),
|
|
131
|
+
modelPanel,
|
|
132
|
+
Chat({
|
|
133
|
+
title: state.selectedModel || 'agent',
|
|
134
|
+
sub: state.chat.busy ? 'streaming…' : (state.chat.messages.length + ' messages'),
|
|
135
|
+
messages: msgs,
|
|
136
|
+
composer,
|
|
125
137
|
}),
|
|
126
138
|
),
|
|
127
|
-
|
|
128
|
-
rows.length === 0 ? h('div', { style: 'opacity:.6;padding:16px;font-size:13px' }, 'no sessions') : null,
|
|
129
|
-
),
|
|
130
|
-
);
|
|
139
|
+
];
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
);
|
|
142
|
+
function newChat() {
|
|
143
|
+
state.chat.abort?.abort();
|
|
144
|
+
state.chat = { messages: [], busy: false, abort: null, draft: '' };
|
|
145
|
+
render();
|
|
146
146
|
}
|
|
147
147
|
|
|
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
|
-
}
|
|
148
|
+
function cancelChat() { state.chat.abort?.abort(); }
|
|
158
149
|
|
|
159
150
|
async function sendChat() {
|
|
160
151
|
const text = (state.chat.draft || '').trim();
|
|
161
152
|
if (!text || !state.selectedModel || state.chat.busy) return;
|
|
162
|
-
|
|
163
|
-
state.chat.messages.push({ role: '
|
|
153
|
+
const t = timeNow();
|
|
154
|
+
state.chat.messages.push({ role: 'user', content: text, time: t });
|
|
155
|
+
state.chat.messages.push({ role: 'assistant', content: '', time: t });
|
|
164
156
|
state.chat.draft = '';
|
|
165
157
|
state.chat.busy = true;
|
|
166
158
|
const ctrl = new AbortController();
|
|
@@ -173,11 +165,11 @@ async function sendChat() {
|
|
|
173
165
|
messages: state.chat.messages.slice(0, -1).map(m => ({ role: m.role, content: m.content })),
|
|
174
166
|
signal: ctrl.signal,
|
|
175
167
|
})) {
|
|
176
|
-
if (ev.type === 'text')
|
|
168
|
+
if (ev.type === 'text') { cur.content += ev.text; render(); }
|
|
177
169
|
if (ev.type === 'error') { cur.content += '\n[error] ' + JSON.stringify(ev.error); render(); }
|
|
178
170
|
}
|
|
179
171
|
} catch (e) {
|
|
180
|
-
cur.content += '\n[error] ' + e.message;
|
|
172
|
+
if (e.name !== 'AbortError') cur.content += '\n[error] ' + e.message;
|
|
181
173
|
} finally {
|
|
182
174
|
state.chat.busy = false;
|
|
183
175
|
state.chat.abort = null;
|
|
@@ -185,8 +177,142 @@ async function sendChat() {
|
|
|
185
177
|
}
|
|
186
178
|
}
|
|
187
179
|
|
|
188
|
-
|
|
180
|
+
// ── history ────────────────────────────────────────────────────────────────
|
|
181
|
+
function historyMain() {
|
|
182
|
+
const head = h('div', { class: 'ds-section' },
|
|
183
|
+
h('h1', {}, '§ history'),
|
|
184
|
+
h('p', { class: 'lede' },
|
|
185
|
+
state.selectedSid
|
|
186
|
+
? 'session ' + state.selectedSid
|
|
187
|
+
: 'pick a session from the sidebar — events stream from acptoapi /v1/history.',
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!state.selectedSid) return [head];
|
|
192
|
+
if (state.events.length === 0) return [head, Panel({ title: 'events', children: h('p', { class: 'lede' }, '◌ loading…') })];
|
|
189
193
|
|
|
194
|
+
const rows = state.events.map((e, i) =>
|
|
195
|
+
Row({
|
|
196
|
+
key: 'ev' + i,
|
|
197
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
198
|
+
title: (e.text || '').slice(0, 200) || '(empty)',
|
|
199
|
+
sub: new Date(e.ts).toLocaleString() + ' · ' + (e.role || '?') + ' · ' + (e.type || '?') + (e.tool ? ' · ⌘ ' + e.tool : ''),
|
|
200
|
+
rail: e.role === 'error' ? 'flame' : (e.role === 'user' ? 'green' : 'purple'),
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return [
|
|
205
|
+
head,
|
|
206
|
+
Panel({
|
|
207
|
+
title: state.events.length + ' events',
|
|
208
|
+
children: h('div', { class: 'ds-section' }, ...rows),
|
|
209
|
+
}),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function historySide() {
|
|
214
|
+
const searching = !!state.searchHits;
|
|
215
|
+
const rows = searching
|
|
216
|
+
? state.searchHits.results.slice(0, 60).map((r, i) =>
|
|
217
|
+
Row({
|
|
218
|
+
key: 'sr' + i,
|
|
219
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
220
|
+
title: r.snippet || '(no snippet)',
|
|
221
|
+
sub: (r.project || '?') + ' · ' + (r.role || '?') + (r.tool ? ' · ' + r.tool : ''),
|
|
222
|
+
rail: 'purple',
|
|
223
|
+
onClick: () => loadSession(r.sid),
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
: state.sessions.slice(0, 120).map((s, i) =>
|
|
227
|
+
Row({
|
|
228
|
+
key: 'sess' + i,
|
|
229
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
230
|
+
title: s.title || s.project || s.sid,
|
|
231
|
+
sub: s.events + ' ev · ' + s.tools + ' tools' + (s.errors ? ' · ' + s.errors + ' err' : ''),
|
|
232
|
+
rail: s.errors ? 'flame' : 'green',
|
|
233
|
+
active: s.sid === state.selectedSid,
|
|
234
|
+
onClick: () => loadSession(s.sid),
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
return [
|
|
239
|
+
Side({
|
|
240
|
+
sections: [
|
|
241
|
+
{
|
|
242
|
+
group: 'navigate',
|
|
243
|
+
items: [
|
|
244
|
+
{ glyph: '▣', label: 'chat', key: 'chat', onClick: (e) => { e.preventDefault(); navTo('chat'); } },
|
|
245
|
+
{ glyph: '§', label: 'history', key: 'history', active: true },
|
|
246
|
+
{ glyph: '⌘', label: 'settings', key: 'settings', onClick: (e) => { e.preventDefault(); navTo('settings'); } },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
}),
|
|
251
|
+
Panel({
|
|
252
|
+
title: searching ? 'matches' : 'sessions',
|
|
253
|
+
children: h('div', { class: 'ds-section' },
|
|
254
|
+
h('input', {
|
|
255
|
+
type: 'search',
|
|
256
|
+
placeholder: 'search sessions…',
|
|
257
|
+
value: state.searchQ,
|
|
258
|
+
oninput: (e) => { state.searchQ = e.target.value; runSearch(); },
|
|
259
|
+
}),
|
|
260
|
+
rows.length ? h('div', {}, ...rows) : h('p', { class: 'lede' }, 'no sessions yet'),
|
|
261
|
+
),
|
|
262
|
+
}),
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── settings ───────────────────────────────────────────────────────────────
|
|
267
|
+
function settingsMain() {
|
|
268
|
+
const ok = state.health.status === 'ok';
|
|
269
|
+
return [
|
|
270
|
+
h('div', { class: 'ds-section' },
|
|
271
|
+
h('h1', {}, '⌘ settings'),
|
|
272
|
+
h('p', { class: 'lede' }, 'point agentgui at any acptoapi backend. ?backend=… in the URL or the field below — both persist via localStorage.'),
|
|
273
|
+
Panel({
|
|
274
|
+
title: 'backend',
|
|
275
|
+
children: h('div', { class: 'ds-section' },
|
|
276
|
+
h('p', { class: 'lede' }, 'backend url'),
|
|
277
|
+
h('input', {
|
|
278
|
+
type: 'text',
|
|
279
|
+
value: state.backendDraft,
|
|
280
|
+
oninput: (e) => { state.backendDraft = e.target.value; render(); },
|
|
281
|
+
}),
|
|
282
|
+
h('p', { class: 'lede' }, (ok ? '● ' : '○ ') + JSON.stringify(state.health)),
|
|
283
|
+
h('button', {
|
|
284
|
+
onclick: () => {
|
|
285
|
+
B.setBackend(state.backendDraft);
|
|
286
|
+
state.backend = state.backendDraft;
|
|
287
|
+
state.health = { status: 'unknown' };
|
|
288
|
+
render();
|
|
289
|
+
init();
|
|
290
|
+
},
|
|
291
|
+
}, 'save + reconnect'),
|
|
292
|
+
),
|
|
293
|
+
}),
|
|
294
|
+
Panel({
|
|
295
|
+
title: 'models',
|
|
296
|
+
children: h('div', { class: 'ds-section' },
|
|
297
|
+
state.models.length
|
|
298
|
+
? h('div', {}, ...state.models.slice(0, 40).map((m, i) =>
|
|
299
|
+
Row({
|
|
300
|
+
key: 'm' + i,
|
|
301
|
+
rank: String(i + 1).padStart(3, '0'),
|
|
302
|
+
title: m.id,
|
|
303
|
+
sub: m.owned_by || m.object || 'model',
|
|
304
|
+
rail: m.id === state.selectedModel ? 'green' : 'purple',
|
|
305
|
+
onClick: () => { state.selectedModel = m.id; render(); },
|
|
306
|
+
})
|
|
307
|
+
))
|
|
308
|
+
: h('p', { class: 'lede' }, 'no models loaded'),
|
|
309
|
+
),
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── data ──────────────────────────────────────────────────────────────────
|
|
190
316
|
async function refreshHistory() {
|
|
191
317
|
try { state.sessions = await B.listSessions(state.backend); render(); }
|
|
192
318
|
catch (e) { console.warn('history fetch failed:', e.message); }
|
|
@@ -194,19 +320,33 @@ async function refreshHistory() {
|
|
|
194
320
|
|
|
195
321
|
async function runSearch() {
|
|
196
322
|
if (!state.searchQ.trim()) { state.searchHits = null; render(); return; }
|
|
197
|
-
try {
|
|
198
|
-
|
|
323
|
+
try {
|
|
324
|
+
state.searchHits = await B.searchHistory(state.backend, state.searchQ, 50);
|
|
325
|
+
render();
|
|
326
|
+
} catch (e) {
|
|
327
|
+
state.searchHits = { query: state.searchQ, results: [], error: e.message };
|
|
328
|
+
render();
|
|
329
|
+
}
|
|
199
330
|
}
|
|
200
331
|
|
|
201
332
|
async function loadSession(sid) {
|
|
202
|
-
state.selectedSid = sid;
|
|
333
|
+
state.selectedSid = sid;
|
|
334
|
+
state.events = [];
|
|
335
|
+
render();
|
|
203
336
|
try { state.events = await B.getSessionEvents(state.backend, sid); render(); }
|
|
204
|
-
catch (e) {
|
|
337
|
+
catch (e) {
|
|
338
|
+
state.events = [{ ts: Date.now(), role: 'error', type: 'fetch', text: e.message }];
|
|
339
|
+
render();
|
|
340
|
+
}
|
|
205
341
|
}
|
|
206
342
|
|
|
207
343
|
async function init() {
|
|
208
|
-
|
|
209
|
-
|
|
344
|
+
try {
|
|
345
|
+
const r = await B.probeBackend(state.backend);
|
|
346
|
+
state.health = r.ok ? { status: 'ok', ...r.info } : { status: 'down', ...r };
|
|
347
|
+
} catch (e) {
|
|
348
|
+
state.health = { status: 'error', error: e.message };
|
|
349
|
+
}
|
|
210
350
|
render();
|
|
211
351
|
try {
|
|
212
352
|
state.models = await B.listModels(state.backend);
|