@alashi/cli 0.1.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/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +148 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +256 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/store-CwgDCAAx.mjs +1201 -0
- package/dist/store-CwgDCAAx.mjs.map +1 -0
- package/package.json +51 -0
- package/templates/basic-app/README.md +47 -0
- package/templates/basic-app/gitignore +1 -0
- package/templates/basic-app/index.mjs +72 -0
- package/templates/basic-app/package.json +9 -0
- package/templates/basic-app/ui/index.html +466 -0
|
@@ -0,0 +1,466 @@
|
|
|
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">
|
|
6
|
+
<title>__APP_NAME__</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #f6f7fb;
|
|
10
|
+
--panel: #ffffff;
|
|
11
|
+
--border: #e4e6ef;
|
|
12
|
+
--text: #1c1e26;
|
|
13
|
+
--muted: #6b7080;
|
|
14
|
+
--accent: #6d28d9;
|
|
15
|
+
--accent-soft: #ede9fe;
|
|
16
|
+
--sidebar: #17141f;
|
|
17
|
+
--sidebar-text: #b9b3c9;
|
|
18
|
+
--ok: #15803d;
|
|
19
|
+
--err: #b91c1c;
|
|
20
|
+
--radius: 10px;
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; }
|
|
23
|
+
body {
|
|
24
|
+
margin: 0;
|
|
25
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
display: flex;
|
|
29
|
+
min-height: 100vh;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ---- shell ---- */
|
|
33
|
+
#sidebar {
|
|
34
|
+
width: 230px;
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
background: var(--sidebar);
|
|
37
|
+
color: var(--sidebar-text);
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
padding: 1.25rem 0.75rem;
|
|
41
|
+
position: sticky;
|
|
42
|
+
top: 0;
|
|
43
|
+
height: 100vh;
|
|
44
|
+
}
|
|
45
|
+
#brand {
|
|
46
|
+
color: #fff;
|
|
47
|
+
font-weight: 700;
|
|
48
|
+
font-size: 1.05rem;
|
|
49
|
+
padding: 0 0.75rem 1.25rem;
|
|
50
|
+
letter-spacing: 0.02em;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
text-overflow: ellipsis;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
}
|
|
55
|
+
#brand .dot { color: var(--accent); }
|
|
56
|
+
nav a {
|
|
57
|
+
display: block;
|
|
58
|
+
color: var(--sidebar-text);
|
|
59
|
+
text-decoration: none;
|
|
60
|
+
padding: 0.55rem 0.75rem;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
margin-bottom: 2px;
|
|
63
|
+
font-size: 0.95rem;
|
|
64
|
+
}
|
|
65
|
+
nav a:hover { background: rgba(255, 255, 255, 0.06); color: #fff; }
|
|
66
|
+
nav a.active { background: var(--accent); color: #fff; font-weight: 600; }
|
|
67
|
+
#sidebar footer {
|
|
68
|
+
margin-top: auto;
|
|
69
|
+
padding: 0.75rem;
|
|
70
|
+
font-size: 0.75rem;
|
|
71
|
+
color: #5d5871;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main { flex: 1; min-width: 0; padding: 2rem 2.5rem; }
|
|
75
|
+
.page { display: none; }
|
|
76
|
+
.page.visible { display: block; }
|
|
77
|
+
h1 { margin: 0 0 0.25rem; font-size: 1.4rem; }
|
|
78
|
+
.subtitle { color: var(--muted); margin: 0 0 1.5rem; font-size: 0.95rem; }
|
|
79
|
+
|
|
80
|
+
/* ---- components ---- */
|
|
81
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
|
82
|
+
.card {
|
|
83
|
+
background: var(--panel);
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
border-radius: var(--radius);
|
|
86
|
+
padding: 1rem 1.25rem;
|
|
87
|
+
}
|
|
88
|
+
.card .label { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
89
|
+
.card .value { font-size: 1.6rem; font-weight: 700; margin-top: 0.25rem; }
|
|
90
|
+
|
|
91
|
+
.panel {
|
|
92
|
+
background: var(--panel);
|
|
93
|
+
border: 1px solid var(--border);
|
|
94
|
+
border-radius: var(--radius);
|
|
95
|
+
padding: 1.25rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.92rem; }
|
|
99
|
+
th { text-align: left; color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
100
|
+
th, td { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); }
|
|
101
|
+
tr:last-child td { border-bottom: none; }
|
|
102
|
+
.empty { color: var(--muted); padding: 1.5rem 0; text-align: center; }
|
|
103
|
+
|
|
104
|
+
button {
|
|
105
|
+
font: inherit;
|
|
106
|
+
border: 1px solid var(--border);
|
|
107
|
+
background: var(--panel);
|
|
108
|
+
border-radius: 8px;
|
|
109
|
+
padding: 0.45rem 0.9rem;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
}
|
|
112
|
+
button:hover { border-color: var(--accent); color: var(--accent); }
|
|
113
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
114
|
+
button.primary:hover { opacity: 0.92; color: #fff; }
|
|
115
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
116
|
+
input[type='text'] {
|
|
117
|
+
font: inherit;
|
|
118
|
+
padding: 0.5rem 0.75rem;
|
|
119
|
+
border: 1px solid var(--border);
|
|
120
|
+
border-radius: 8px;
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-width: 0;
|
|
123
|
+
}
|
|
124
|
+
input[type='text']:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
|
|
125
|
+
.row { display: flex; gap: 0.5rem; }
|
|
126
|
+
.badge { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
|
|
127
|
+
.badge.ok { background: #dcfce7; color: var(--ok); }
|
|
128
|
+
.badge.err { background: #fee2e2; color: var(--err); }
|
|
129
|
+
.muted { color: var(--muted); }
|
|
130
|
+
|
|
131
|
+
/* ---- chat ---- */
|
|
132
|
+
#chat-layout { display: flex; gap: 1rem; height: calc(100vh - 9rem); }
|
|
133
|
+
#conversations-panel { width: 220px; flex-shrink: 0; display: flex; flex-direction: column; }
|
|
134
|
+
#conversations { list-style: none; margin: 0.75rem 0 0; padding: 0; overflow-y: auto; }
|
|
135
|
+
#conversations li {
|
|
136
|
+
padding: 0.45rem 0.6rem;
|
|
137
|
+
border-radius: 8px;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
font-size: 0.88rem;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
text-overflow: ellipsis;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
}
|
|
144
|
+
#conversations li:hover { background: var(--bg); }
|
|
145
|
+
#conversations li.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
146
|
+
#chat-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
147
|
+
#log { flex: 1; overflow-y: auto; padding-right: 0.5rem; }
|
|
148
|
+
.msg { max-width: 82%; padding: 0.6rem 0.85rem; border-radius: 12px; margin-bottom: 0.6rem; white-space: pre-wrap; overflow-wrap: break-word; }
|
|
149
|
+
.msg.user { background: var(--accent); color: #fff; margin-left: auto; }
|
|
150
|
+
.msg.assistant { background: var(--bg); border: 1px solid var(--border); }
|
|
151
|
+
#chat-form { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
|
|
152
|
+
#chat-status { min-height: 1.2rem; font-size: 0.8rem; color: var(--muted); margin-top: 0.4rem; }
|
|
153
|
+
</style>
|
|
154
|
+
</head>
|
|
155
|
+
<body>
|
|
156
|
+
<aside id="sidebar">
|
|
157
|
+
<div id="brand">__APP_NAME__<span class="dot">.</span></div>
|
|
158
|
+
<nav>
|
|
159
|
+
<a href="#/overview" data-page="overview">Overview</a>
|
|
160
|
+
<a href="#/chat" data-page="chat">Chat</a>
|
|
161
|
+
<a href="#/notes" data-page="notes">Notes</a>
|
|
162
|
+
<a href="#/jobs" data-page="jobs">Jobs</a>
|
|
163
|
+
</nav>
|
|
164
|
+
<footer>powered by alashi</footer>
|
|
165
|
+
</aside>
|
|
166
|
+
|
|
167
|
+
<main>
|
|
168
|
+
<section class="page" id="page-overview">
|
|
169
|
+
<h1>Overview</h1>
|
|
170
|
+
<p class="subtitle" id="greeting"></p>
|
|
171
|
+
<div class="cards">
|
|
172
|
+
<div class="card"><div class="label">Notes</div><div class="value" id="stat-notes">–</div></div>
|
|
173
|
+
<div class="card"><div class="label">Conversations</div><div class="value" id="stat-convos">–</div></div>
|
|
174
|
+
<div class="card"><div class="label">Last heartbeat</div><div class="value" id="stat-heartbeat" style="font-size:1rem">–</div></div>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="panel muted">
|
|
177
|
+
Edit <code>index.mjs</code> for API routes, jobs, and manifest; <code>ui/index.html</code> for this page.
|
|
178
|
+
The app's SQLite database and files live in its data directory.
|
|
179
|
+
</div>
|
|
180
|
+
</section>
|
|
181
|
+
|
|
182
|
+
<section class="page" id="page-chat">
|
|
183
|
+
<h1>Chat</h1>
|
|
184
|
+
<p class="subtitle">Talks to the engine through the alashi host.</p>
|
|
185
|
+
<div id="chat-layout">
|
|
186
|
+
<div class="panel" id="conversations-panel">
|
|
187
|
+
<button id="new-chat" type="button">+ New chat</button>
|
|
188
|
+
<ul id="conversations"></ul>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="panel" id="chat-main">
|
|
191
|
+
<div id="log"></div>
|
|
192
|
+
<form id="chat-form">
|
|
193
|
+
<input id="chat-input" type="text" placeholder="Say something" autocomplete="off">
|
|
194
|
+
<button id="chat-send" class="primary" type="submit">Send</button>
|
|
195
|
+
</form>
|
|
196
|
+
<div id="chat-status"></div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</section>
|
|
200
|
+
|
|
201
|
+
<section class="page" id="page-notes">
|
|
202
|
+
<h1>Notes</h1>
|
|
203
|
+
<p class="subtitle">A tiny CRUD example backed by the app's own SQLite database.</p>
|
|
204
|
+
<div class="panel">
|
|
205
|
+
<form id="note-form" class="row" style="margin-bottom:1rem">
|
|
206
|
+
<input id="note-input" type="text" placeholder="Add a note" autocomplete="off">
|
|
207
|
+
<button class="primary" type="submit">Add</button>
|
|
208
|
+
</form>
|
|
209
|
+
<table>
|
|
210
|
+
<thead><tr><th>Note</th><th style="width:12rem">Created</th><th style="width:5rem"></th></tr></thead>
|
|
211
|
+
<tbody id="notes-body"></tbody>
|
|
212
|
+
</table>
|
|
213
|
+
<div class="empty" id="notes-empty" hidden>No notes yet.</div>
|
|
214
|
+
</div>
|
|
215
|
+
</section>
|
|
216
|
+
|
|
217
|
+
<section class="page" id="page-jobs">
|
|
218
|
+
<h1>Jobs</h1>
|
|
219
|
+
<p class="subtitle">Manifest-declared schedules, run by the host.</p>
|
|
220
|
+
<div class="panel">
|
|
221
|
+
<table>
|
|
222
|
+
<thead><tr><th>Job</th><th>Schedule</th><th>Next run</th><th>Last run</th><th>Status</th><th style="width:7rem"></th></tr></thead>
|
|
223
|
+
<tbody id="jobs-body"></tbody>
|
|
224
|
+
</table>
|
|
225
|
+
<div class="empty" id="jobs-empty" hidden>No jobs declared.</div>
|
|
226
|
+
</div>
|
|
227
|
+
</section>
|
|
228
|
+
</main>
|
|
229
|
+
|
|
230
|
+
<script>
|
|
231
|
+
const APP = decodeURIComponent(location.pathname.split('/')[2]);
|
|
232
|
+
const appApi = (path) => `/app/${APP}/api${path}`;
|
|
233
|
+
const hostApi = (path) => `/api/apps/${APP}${path}`;
|
|
234
|
+
|
|
235
|
+
function el(tag, props = {}, children = []) {
|
|
236
|
+
const node = document.createElement(tag);
|
|
237
|
+
Object.assign(node, props);
|
|
238
|
+
for (const child of children) node.append(child);
|
|
239
|
+
return node;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const fmt = (iso) => (iso ? new Date(iso).toLocaleString() : '–');
|
|
243
|
+
|
|
244
|
+
/* ---- routing ---- */
|
|
245
|
+
const loaders = { overview: loadOverview, chat: loadChat, notes: loadNotes, jobs: loadJobs };
|
|
246
|
+
|
|
247
|
+
function route() {
|
|
248
|
+
const page = (location.hash.replace('#/', '') || 'overview').split('?')[0];
|
|
249
|
+
const known = Object.hasOwn(loaders, page) ? page : 'overview';
|
|
250
|
+
for (const section of document.querySelectorAll('.page')) {
|
|
251
|
+
section.classList.toggle('visible', section.id === `page-${known}`);
|
|
252
|
+
}
|
|
253
|
+
for (const link of document.querySelectorAll('nav a')) {
|
|
254
|
+
link.classList.toggle('active', link.dataset.page === known);
|
|
255
|
+
}
|
|
256
|
+
loaders[known]();
|
|
257
|
+
}
|
|
258
|
+
window.addEventListener('hashchange', route);
|
|
259
|
+
|
|
260
|
+
/* ---- overview ---- */
|
|
261
|
+
async function loadOverview() {
|
|
262
|
+
try {
|
|
263
|
+
const summary = await (await fetch(appApi('/summary'))).json();
|
|
264
|
+
document.getElementById('greeting').textContent = summary.greeting ?? '';
|
|
265
|
+
document.getElementById('stat-notes').textContent = summary.notes;
|
|
266
|
+
document.getElementById('stat-heartbeat').textContent = fmt(summary.lastHeartbeat);
|
|
267
|
+
const { conversations } = await (await fetch(hostApi('/chat/conversations'))).json();
|
|
268
|
+
document.getElementById('stat-convos').textContent = conversations.length;
|
|
269
|
+
} catch {
|
|
270
|
+
document.getElementById('greeting').textContent = 'Failed to load summary.';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ---- notes ---- */
|
|
275
|
+
async function loadNotes() {
|
|
276
|
+
const { notes } = await (await fetch(appApi('/notes'))).json();
|
|
277
|
+
const body = document.getElementById('notes-body');
|
|
278
|
+
body.replaceChildren(
|
|
279
|
+
...notes.map((note) =>
|
|
280
|
+
el('tr', {}, [
|
|
281
|
+
el('td', { textContent: note.text }),
|
|
282
|
+
el('td', { textContent: fmt(note.created_at), className: 'muted' }),
|
|
283
|
+
el('td', {}, [
|
|
284
|
+
el('button', {
|
|
285
|
+
textContent: 'Delete',
|
|
286
|
+
onclick: async () => {
|
|
287
|
+
await fetch(appApi(`/notes/${note.id}`), { method: 'DELETE' });
|
|
288
|
+
loadNotes();
|
|
289
|
+
},
|
|
290
|
+
}),
|
|
291
|
+
]),
|
|
292
|
+
]),
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
document.getElementById('notes-empty').hidden = notes.length > 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
document.getElementById('note-form').addEventListener('submit', async (event) => {
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
const input = document.getElementById('note-input');
|
|
301
|
+
const text = input.value.trim();
|
|
302
|
+
if (!text) return;
|
|
303
|
+
input.value = '';
|
|
304
|
+
await fetch(appApi('/notes'), {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
headers: { 'content-type': 'application/json' },
|
|
307
|
+
body: JSON.stringify({ text }),
|
|
308
|
+
});
|
|
309
|
+
loadNotes();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/* ---- jobs ---- */
|
|
313
|
+
async function loadJobs() {
|
|
314
|
+
const { jobs } = await (await fetch(hostApi('/jobs'))).json();
|
|
315
|
+
const body = document.getElementById('jobs-body');
|
|
316
|
+
body.replaceChildren(
|
|
317
|
+
...jobs.map((job) => {
|
|
318
|
+
const last = job.recentRuns[0];
|
|
319
|
+
const status = !job.valid
|
|
320
|
+
? el('span', { textContent: 'invalid schedule', className: 'badge err' })
|
|
321
|
+
: job.running
|
|
322
|
+
? el('span', { textContent: 'running', className: 'badge ok' })
|
|
323
|
+
: last
|
|
324
|
+
? el('span', { textContent: last.ok ? 'ok' : 'failed', className: `badge ${last.ok ? 'ok' : 'err'}` })
|
|
325
|
+
: el('span', { textContent: 'never run', className: 'muted' });
|
|
326
|
+
return el('tr', {}, [
|
|
327
|
+
el('td', { textContent: job.name }),
|
|
328
|
+
el('td', {}, [el('code', { textContent: job.schedule })]),
|
|
329
|
+
el('td', { textContent: fmt(job.nextRun), className: 'muted' }),
|
|
330
|
+
el('td', { textContent: fmt(job.lastRun), className: 'muted' }),
|
|
331
|
+
el('td', {}, [status]),
|
|
332
|
+
el('td', {}, [
|
|
333
|
+
el('button', {
|
|
334
|
+
textContent: 'Run now',
|
|
335
|
+
onclick: async (e) => {
|
|
336
|
+
e.target.disabled = true;
|
|
337
|
+
await fetch(hostApi(`/jobs/${job.name}/run`), { method: 'POST' });
|
|
338
|
+
loadJobs();
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
341
|
+
]),
|
|
342
|
+
]);
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
document.getElementById('jobs-empty').hidden = jobs.length > 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ---- chat ---- */
|
|
349
|
+
const log = document.getElementById('log');
|
|
350
|
+
const chatStatus = document.getElementById('chat-status');
|
|
351
|
+
let conversationId = null;
|
|
352
|
+
let chatLoaded = false;
|
|
353
|
+
|
|
354
|
+
function appendMsg(text, role) {
|
|
355
|
+
const div = el('div', { textContent: text, className: `msg ${role}` });
|
|
356
|
+
log.appendChild(div);
|
|
357
|
+
log.scrollTop = log.scrollHeight;
|
|
358
|
+
return div;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function loadConversations() {
|
|
362
|
+
const { conversations } = await (await fetch(hostApi('/chat/conversations'))).json();
|
|
363
|
+
document.getElementById('conversations').replaceChildren(
|
|
364
|
+
...conversations.map((conv) =>
|
|
365
|
+
el('li', {
|
|
366
|
+
textContent: conv.title || conv.id,
|
|
367
|
+
title: conv.title || conv.id,
|
|
368
|
+
className: conv.id === conversationId ? 'active' : '',
|
|
369
|
+
onclick: () => openConversation(conv.id),
|
|
370
|
+
}),
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
return conversations;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function openConversation(id) {
|
|
377
|
+
conversationId = id;
|
|
378
|
+
log.replaceChildren();
|
|
379
|
+
chatStatus.textContent = 'loading history…';
|
|
380
|
+
try {
|
|
381
|
+
const { messages } = await (await fetch(hostApi(`/chat/${id}/history`))).json();
|
|
382
|
+
for (const message of messages) appendMsg(message.text, message.role);
|
|
383
|
+
chatStatus.textContent = '';
|
|
384
|
+
} catch (err) {
|
|
385
|
+
chatStatus.textContent = String(err);
|
|
386
|
+
}
|
|
387
|
+
loadConversations();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function loadChat() {
|
|
391
|
+
if (chatLoaded) return;
|
|
392
|
+
chatLoaded = true;
|
|
393
|
+
loadConversations()
|
|
394
|
+
.then((conversations) => {
|
|
395
|
+
if (conversations.length > 0) openConversation(conversations[0].id);
|
|
396
|
+
})
|
|
397
|
+
.catch(() => {});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
document.getElementById('new-chat').addEventListener('click', () => {
|
|
401
|
+
conversationId = null;
|
|
402
|
+
log.replaceChildren();
|
|
403
|
+
chatStatus.textContent = '';
|
|
404
|
+
loadConversations();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
document.getElementById('chat-form').addEventListener('submit', async (event) => {
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
const input = document.getElementById('chat-input');
|
|
410
|
+
const send = document.getElementById('chat-send');
|
|
411
|
+
const message = input.value.trim();
|
|
412
|
+
if (!message) return;
|
|
413
|
+
input.value = '';
|
|
414
|
+
send.disabled = true;
|
|
415
|
+
appendMsg(message, 'user');
|
|
416
|
+
const reply = appendMsg('', 'assistant');
|
|
417
|
+
chatStatus.textContent = 'thinking…';
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const res = await fetch(hostApi('/chat'), {
|
|
421
|
+
method: 'POST',
|
|
422
|
+
headers: { 'content-type': 'application/json' },
|
|
423
|
+
body: JSON.stringify({ message, ...(conversationId ? { conversationId } : {}) }),
|
|
424
|
+
});
|
|
425
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
426
|
+
const isNew = !conversationId;
|
|
427
|
+
conversationId = res.headers.get('x-conversation-id') || conversationId;
|
|
428
|
+
|
|
429
|
+
const reader = res.body.getReader();
|
|
430
|
+
const decoder = new TextDecoder();
|
|
431
|
+
let buffer = '';
|
|
432
|
+
for (;;) {
|
|
433
|
+
const { done, value } = await reader.read();
|
|
434
|
+
if (done) break;
|
|
435
|
+
buffer += decoder.decode(value, { stream: true });
|
|
436
|
+
const frames = buffer.split('\n\n');
|
|
437
|
+
buffer = frames.pop();
|
|
438
|
+
for (const frame of frames) {
|
|
439
|
+
const dataLine = frame.split('\n').find((line) => line.startsWith('data: '));
|
|
440
|
+
if (!dataLine) continue;
|
|
441
|
+
const agentEvent = JSON.parse(dataLine.slice(6));
|
|
442
|
+
if (agentEvent.type === 'text') {
|
|
443
|
+
reply.textContent += agentEvent.text;
|
|
444
|
+
log.scrollTop = log.scrollHeight;
|
|
445
|
+
}
|
|
446
|
+
if (agentEvent.type === 'tool_use') chatStatus.textContent = `using tool: ${agentEvent.name}`;
|
|
447
|
+
if (agentEvent.type === 'result') {
|
|
448
|
+
chatStatus.textContent = agentEvent.ok
|
|
449
|
+
? `done${agentEvent.costUsd ? ` ($${agentEvent.costUsd.toFixed(4)})` : ''}`
|
|
450
|
+
: `error: ${agentEvent.error}`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (isNew) loadConversations();
|
|
455
|
+
} catch (err) {
|
|
456
|
+
chatStatus.textContent = String(err);
|
|
457
|
+
} finally {
|
|
458
|
+
send.disabled = false;
|
|
459
|
+
input.focus();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
route();
|
|
464
|
+
</script>
|
|
465
|
+
</body>
|
|
466
|
+
</html>
|