@tangle-network/agent-app 0.42.17 → 0.42.19
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/assistant/index.d.ts +619 -0
- package/dist/assistant/index.js +2706 -0
- package/dist/assistant/index.js.map +1 -0
- package/dist/chunk-WV2U5UIB.js +2158 -0
- package/dist/chunk-WV2U5UIB.js.map +1 -0
- package/dist/composer/index.d.ts +2 -0
- package/dist/composer/index.js +22 -0
- package/dist/composer/index.js.map +1 -0
- package/dist/design-canvas-react/index.js +4 -4
- package/dist/index.js +56 -56
- package/dist/teams-react/index.js +3 -3
- package/dist/web-react/index.d.ts +56 -2
- package/dist/web-react/index.js +36 -1839
- package/dist/web-react/index.js.map +1 -1
- package/package.json +13 -3
|
@@ -0,0 +1,2706 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChatComposer,
|
|
3
|
+
ChatMessages,
|
|
4
|
+
ModelPicker,
|
|
5
|
+
ProviderLogo
|
|
6
|
+
} from "../chunk-WV2U5UIB.js";
|
|
7
|
+
import "../chunk-2QI7XV2T.js";
|
|
8
|
+
import "../chunk-E7QYOOON.js";
|
|
9
|
+
|
|
10
|
+
// src/assistant/sse.ts
|
|
11
|
+
var SSEChunkParser = class {
|
|
12
|
+
buffer = "";
|
|
13
|
+
current = {};
|
|
14
|
+
push(chunk) {
|
|
15
|
+
this.buffer += chunk;
|
|
16
|
+
const lines = this.buffer.split("\n");
|
|
17
|
+
this.buffer = lines.pop() ?? "";
|
|
18
|
+
return this.processLines(lines);
|
|
19
|
+
}
|
|
20
|
+
flush() {
|
|
21
|
+
const lines = this.buffer ? [this.buffer] : [];
|
|
22
|
+
this.buffer = "";
|
|
23
|
+
const events = this.processLines(lines);
|
|
24
|
+
const finalEvent = this.parseCurrent();
|
|
25
|
+
if (finalEvent) {
|
|
26
|
+
events.push(finalEvent);
|
|
27
|
+
this.current = {};
|
|
28
|
+
}
|
|
29
|
+
return events;
|
|
30
|
+
}
|
|
31
|
+
processLines(lines) {
|
|
32
|
+
const events = [];
|
|
33
|
+
for (const rawLine of lines) {
|
|
34
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
35
|
+
if (line.startsWith(":")) continue;
|
|
36
|
+
if (line === "") {
|
|
37
|
+
const parsed = this.parseCurrent();
|
|
38
|
+
if (parsed) events.push(parsed);
|
|
39
|
+
this.current = {};
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (line.startsWith("id:")) {
|
|
43
|
+
this.current.id = line.slice(3).trim();
|
|
44
|
+
} else if (line.startsWith("event:")) {
|
|
45
|
+
this.current.event = line.slice(6).trim();
|
|
46
|
+
} else if (line.startsWith("data:")) {
|
|
47
|
+
let value = line.slice(5);
|
|
48
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
49
|
+
this.current.data = this.current.data !== void 0 ? `${this.current.data}
|
|
50
|
+
${value}` : value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return events;
|
|
54
|
+
}
|
|
55
|
+
parseCurrent() {
|
|
56
|
+
if (this.current.data === void 0) return null;
|
|
57
|
+
const rawData = this.current.data.trim();
|
|
58
|
+
if (!rawData) return null;
|
|
59
|
+
let data;
|
|
60
|
+
try {
|
|
61
|
+
data = JSON.parse(rawData);
|
|
62
|
+
} catch {
|
|
63
|
+
data = rawData;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
data,
|
|
67
|
+
rawData,
|
|
68
|
+
eventId: this.current.id,
|
|
69
|
+
eventType: this.current.event
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
async function readSSEEvents(body, onEvent) {
|
|
74
|
+
const reader = body.getReader();
|
|
75
|
+
const decoder = new TextDecoder();
|
|
76
|
+
const parser = new SSEChunkParser();
|
|
77
|
+
try {
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) break;
|
|
81
|
+
for (const event of parser.push(decoder.decode(value, { stream: true }))) {
|
|
82
|
+
onEvent(event);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const tail = decoder.decode();
|
|
86
|
+
if (tail) {
|
|
87
|
+
for (const event of parser.push(tail)) onEvent(event);
|
|
88
|
+
}
|
|
89
|
+
for (const event of parser.flush()) onEvent(event);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
await reader.cancel(err).catch(() => {
|
|
92
|
+
});
|
|
93
|
+
throw err;
|
|
94
|
+
} finally {
|
|
95
|
+
reader.releaseLock();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/assistant/client.ts
|
|
100
|
+
var EMPTY_MODELS = { default: null, models: [] };
|
|
101
|
+
function asObject(v) {
|
|
102
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
103
|
+
}
|
|
104
|
+
function reqStr(v) {
|
|
105
|
+
return typeof v === "string" && v !== "" ? v : null;
|
|
106
|
+
}
|
|
107
|
+
function numOrNull(v) {
|
|
108
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
109
|
+
}
|
|
110
|
+
function parseRequirement(raw) {
|
|
111
|
+
if (!raw || typeof raw !== "object") return null;
|
|
112
|
+
const r = raw;
|
|
113
|
+
if (typeof r.provider !== "string" || r.provider === "") return null;
|
|
114
|
+
if (typeof r.connected !== "boolean") return null;
|
|
115
|
+
const kind = r.kind === "integration" || r.kind === "github_app" ? r.kind : void 0;
|
|
116
|
+
const connectUrl = typeof r.connectUrl === "string" || r.connectUrl === null ? r.connectUrl : void 0;
|
|
117
|
+
return {
|
|
118
|
+
provider: r.provider,
|
|
119
|
+
connected: r.connected,
|
|
120
|
+
...kind ? { kind } : {},
|
|
121
|
+
...connectUrl !== void 0 ? { connectUrl } : {}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function parseRequirements(v) {
|
|
125
|
+
if (!Array.isArray(v)) return void 0;
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const item of v) {
|
|
128
|
+
const parsed = parseRequirement(item);
|
|
129
|
+
if (parsed) out.push(parsed);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function toStreamEvent(event, data) {
|
|
134
|
+
const obj = asObject(data);
|
|
135
|
+
if (!obj) return null;
|
|
136
|
+
switch (event) {
|
|
137
|
+
case "thread": {
|
|
138
|
+
const threadId = reqStr(obj.threadId);
|
|
139
|
+
const turnId = reqStr(obj.turnId);
|
|
140
|
+
if (!threadId || !turnId) return null;
|
|
141
|
+
return {
|
|
142
|
+
type: "thread",
|
|
143
|
+
data: { threadId, turnId, model: reqStr(obj.model) }
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
case "delta": {
|
|
147
|
+
if (typeof obj.text !== "string") return null;
|
|
148
|
+
return { type: "delta", data: { text: obj.text } };
|
|
149
|
+
}
|
|
150
|
+
case "reasoning": {
|
|
151
|
+
if (typeof obj.text !== "string") return null;
|
|
152
|
+
return { type: "reasoning", data: { text: obj.text } };
|
|
153
|
+
}
|
|
154
|
+
case "tool_call": {
|
|
155
|
+
const callId = reqStr(obj.callId);
|
|
156
|
+
const name = reqStr(obj.name);
|
|
157
|
+
if (!callId || !name) return null;
|
|
158
|
+
return { type: "tool_call", data: { callId, name } };
|
|
159
|
+
}
|
|
160
|
+
case "tool_result": {
|
|
161
|
+
const callId = reqStr(obj.callId);
|
|
162
|
+
const name = reqStr(obj.name);
|
|
163
|
+
if (!callId || !name) return null;
|
|
164
|
+
return {
|
|
165
|
+
type: "tool_result",
|
|
166
|
+
data: {
|
|
167
|
+
callId,
|
|
168
|
+
name,
|
|
169
|
+
ok: Boolean(obj.ok),
|
|
170
|
+
output: obj.output,
|
|
171
|
+
error: obj.error
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
case "tool_proposal": {
|
|
176
|
+
const callId = reqStr(obj.callId);
|
|
177
|
+
const name = reqStr(obj.name);
|
|
178
|
+
if (!callId || !name) return null;
|
|
179
|
+
return {
|
|
180
|
+
type: "tool_proposal",
|
|
181
|
+
data: {
|
|
182
|
+
proposalId: obj.proposalId == null ? null : reqStr(obj.proposalId),
|
|
183
|
+
callId,
|
|
184
|
+
name,
|
|
185
|
+
args: obj.args,
|
|
186
|
+
requirements: parseRequirements(obj.requirements)
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
case "usage":
|
|
191
|
+
return {
|
|
192
|
+
type: "usage",
|
|
193
|
+
data: {
|
|
194
|
+
promptTokens: numOrNull(obj.promptTokens),
|
|
195
|
+
completionTokens: numOrNull(obj.completionTokens),
|
|
196
|
+
costUsd: numOrNull(obj.costUsd),
|
|
197
|
+
balanceUsd: numOrNull(obj.balanceUsd),
|
|
198
|
+
replayed: Boolean(obj.replayed)
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
case "done": {
|
|
202
|
+
const turnId = reqStr(obj.turnId);
|
|
203
|
+
const status = reqStr(obj.status);
|
|
204
|
+
if (!turnId || !status) return null;
|
|
205
|
+
return {
|
|
206
|
+
type: "done",
|
|
207
|
+
data: {
|
|
208
|
+
turnId,
|
|
209
|
+
status,
|
|
210
|
+
proposed: Boolean(obj.proposed),
|
|
211
|
+
capped: Boolean(obj.capped)
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
case "error":
|
|
216
|
+
return {
|
|
217
|
+
type: "error",
|
|
218
|
+
data: {
|
|
219
|
+
code: reqStr(obj.code) ?? "STREAM_FAILED",
|
|
220
|
+
message: reqStr(obj.message) ?? "The assistant stream failed"
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
default:
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function readErrorEvent(res) {
|
|
228
|
+
try {
|
|
229
|
+
const body = await res.json();
|
|
230
|
+
return {
|
|
231
|
+
type: "error",
|
|
232
|
+
data: {
|
|
233
|
+
code: body.error?.code ?? `HTTP_${res.status}`,
|
|
234
|
+
message: body.error?.message ?? `Request failed (${res.status})`
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
} catch {
|
|
238
|
+
return {
|
|
239
|
+
type: "error",
|
|
240
|
+
data: {
|
|
241
|
+
code: `HTTP_${res.status}`,
|
|
242
|
+
message: `Request failed (${res.status})`
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function parseRestoredProposal(raw) {
|
|
248
|
+
if (!raw || typeof raw !== "object") return null;
|
|
249
|
+
const r = raw;
|
|
250
|
+
if (typeof r.proposalId !== "string" || r.proposalId === "") return null;
|
|
251
|
+
if (typeof r.callId !== "string" || r.callId === "") return null;
|
|
252
|
+
if (typeof r.name !== "string" || r.name === "") return null;
|
|
253
|
+
const requirements = Array.isArray(r.requirements) ? r.requirements.map(parseRequirement).filter((x) => x !== null) : void 0;
|
|
254
|
+
return {
|
|
255
|
+
proposalId: r.proposalId,
|
|
256
|
+
callId: r.callId,
|
|
257
|
+
name: r.name,
|
|
258
|
+
args: r.args,
|
|
259
|
+
...requirements ? { requirements } : {}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function createAssistantClient(config) {
|
|
263
|
+
const base = config.baseUrl.replace(/\/+$/, "");
|
|
264
|
+
const credentials = config.credentials ?? "include";
|
|
265
|
+
const authHeaders = () => config.headers?.() ?? {};
|
|
266
|
+
const url = (path) => `${base}${path}`;
|
|
267
|
+
async function postJson(path, body) {
|
|
268
|
+
try {
|
|
269
|
+
const res = await fetch(url(path), {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { ...authHeaders(), "Content-Type": "application/json" },
|
|
272
|
+
credentials,
|
|
273
|
+
body: JSON.stringify(body)
|
|
274
|
+
});
|
|
275
|
+
const json = await res.json();
|
|
276
|
+
if (!res.ok) {
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
error: json?.error?.message || `HTTP ${res.status}`
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return { success: true, data: json?.data ?? json };
|
|
283
|
+
} catch (err) {
|
|
284
|
+
return {
|
|
285
|
+
success: false,
|
|
286
|
+
error: err instanceof Error ? err.message : "Request failed"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
async fetchModels(signal) {
|
|
292
|
+
try {
|
|
293
|
+
const res = await fetch(url("/models"), {
|
|
294
|
+
method: "GET",
|
|
295
|
+
headers: authHeaders(),
|
|
296
|
+
credentials,
|
|
297
|
+
signal
|
|
298
|
+
});
|
|
299
|
+
if (!res.ok) return { ok: false, data: EMPTY_MODELS };
|
|
300
|
+
const body = await res.json();
|
|
301
|
+
if (!Array.isArray(body.models))
|
|
302
|
+
return { ok: false, data: EMPTY_MODELS };
|
|
303
|
+
const models = [];
|
|
304
|
+
for (const m of body.models) {
|
|
305
|
+
const slug = typeof m.slug === "string" ? m.slug : null;
|
|
306
|
+
if (!slug) continue;
|
|
307
|
+
const label = typeof m.label === "string" ? m.label : slug;
|
|
308
|
+
const option = { slug, label };
|
|
309
|
+
if (typeof m.promptUsdPerMillion === "number") {
|
|
310
|
+
option.promptUsdPerMillion = m.promptUsdPerMillion;
|
|
311
|
+
}
|
|
312
|
+
if (typeof m.contextTokens === "number") {
|
|
313
|
+
option.contextTokens = m.contextTokens;
|
|
314
|
+
}
|
|
315
|
+
models.push(option);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
// Empty ⇒ catalog unavailable: report not-ok so the caller retries next
|
|
319
|
+
// mount instead of caching an empty picker for the session.
|
|
320
|
+
ok: models.length > 0,
|
|
321
|
+
data: {
|
|
322
|
+
default: typeof body.default === "string" ? body.default : null,
|
|
323
|
+
models
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
} catch {
|
|
327
|
+
return { ok: false, data: EMPTY_MODELS };
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
async fetchThreads(signal) {
|
|
331
|
+
try {
|
|
332
|
+
const res = await fetch(url("/threads"), {
|
|
333
|
+
method: "GET",
|
|
334
|
+
headers: authHeaders(),
|
|
335
|
+
credentials,
|
|
336
|
+
signal
|
|
337
|
+
});
|
|
338
|
+
if (!res.ok) return null;
|
|
339
|
+
const body = await res.json();
|
|
340
|
+
if (!Array.isArray(body.threads)) return null;
|
|
341
|
+
const out = [];
|
|
342
|
+
for (const t of body.threads) {
|
|
343
|
+
const id = typeof t.id === "string" ? t.id : null;
|
|
344
|
+
if (!id) continue;
|
|
345
|
+
out.push({
|
|
346
|
+
id,
|
|
347
|
+
title: typeof t.title === "string" ? t.title : null,
|
|
348
|
+
createdAt: typeof t.createdAt === "string" ? t.createdAt : "",
|
|
349
|
+
updatedAt: typeof t.updatedAt === "string" ? t.updatedAt : ""
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
async fetchThreadHistory(threadId, signal) {
|
|
358
|
+
try {
|
|
359
|
+
const res = await fetch(
|
|
360
|
+
url(`/threads/${encodeURIComponent(threadId)}/messages`),
|
|
361
|
+
{
|
|
362
|
+
method: "GET",
|
|
363
|
+
headers: authHeaders(),
|
|
364
|
+
credentials,
|
|
365
|
+
signal
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
if (res.status === 404) return { status: "gone" };
|
|
369
|
+
if (!res.ok) return { status: "error" };
|
|
370
|
+
const body = await res.json();
|
|
371
|
+
if (!Array.isArray(body.messages)) return { status: "error" };
|
|
372
|
+
const out = [];
|
|
373
|
+
for (const m of body.messages) {
|
|
374
|
+
const id = typeof m.id === "string" ? m.id : null;
|
|
375
|
+
const role = m.role === "user" || m.role === "assistant" ? m.role : null;
|
|
376
|
+
const text = typeof m.text === "string" ? m.text : null;
|
|
377
|
+
if (id && role && text != null) out.push({ id, role, text });
|
|
378
|
+
}
|
|
379
|
+
const proposals = [];
|
|
380
|
+
if (Array.isArray(body.proposals)) {
|
|
381
|
+
for (const p of body.proposals) {
|
|
382
|
+
const parsed = parseRestoredProposal(p);
|
|
383
|
+
if (parsed) proposals.push(parsed);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { status: "ok", messages: out, proposals };
|
|
387
|
+
} catch {
|
|
388
|
+
if (signal?.aborted) return { status: "error" };
|
|
389
|
+
return { status: "error" };
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
async streamChat(req, onEvent, signal) {
|
|
393
|
+
const res = await fetch(url("/chat"), {
|
|
394
|
+
method: "POST",
|
|
395
|
+
headers: {
|
|
396
|
+
...authHeaders(),
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
Accept: "text/event-stream"
|
|
399
|
+
},
|
|
400
|
+
credentials,
|
|
401
|
+
body: JSON.stringify(req),
|
|
402
|
+
signal
|
|
403
|
+
});
|
|
404
|
+
if (!res.ok) {
|
|
405
|
+
onEvent(await readErrorEvent(res));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (!res.body) {
|
|
409
|
+
onEvent({
|
|
410
|
+
type: "error",
|
|
411
|
+
data: {
|
|
412
|
+
code: "NO_BODY",
|
|
413
|
+
message: "The assistant stream is unavailable"
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
let settled = false;
|
|
419
|
+
await readSSEEvents(res.body, (frame) => {
|
|
420
|
+
const ev = toStreamEvent(frame.eventType ?? null, frame.data);
|
|
421
|
+
if (!ev) return;
|
|
422
|
+
if (ev.type === "done" || ev.type === "error") settled = true;
|
|
423
|
+
onEvent(ev);
|
|
424
|
+
});
|
|
425
|
+
if (!settled) {
|
|
426
|
+
onEvent({
|
|
427
|
+
type: "error",
|
|
428
|
+
data: {
|
|
429
|
+
code: "STREAM_CLOSED",
|
|
430
|
+
message: "The assistant stream ended unexpectedly"
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
async confirmProposal(proposalId) {
|
|
436
|
+
const res = await postJson("/tools/execute", { proposalId });
|
|
437
|
+
if (!res.success) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
error: res.error ?? "The action could not be completed"
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const body = res.data;
|
|
444
|
+
if (body?.success) {
|
|
445
|
+
return { ok: true, output: body.output, retryable: body.retryable };
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
ok: false,
|
|
449
|
+
error: body?.error?.message ?? "The action could not be completed"
|
|
450
|
+
};
|
|
451
|
+
},
|
|
452
|
+
async deleteThread(threadId) {
|
|
453
|
+
try {
|
|
454
|
+
const res = await fetch(url(`/threads/${encodeURIComponent(threadId)}`), {
|
|
455
|
+
method: "DELETE",
|
|
456
|
+
headers: authHeaders(),
|
|
457
|
+
credentials
|
|
458
|
+
});
|
|
459
|
+
return { ok: res.ok || res.status === 404 };
|
|
460
|
+
} catch {
|
|
461
|
+
return { ok: false };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/assistant/client-context.tsx
|
|
468
|
+
import { createContext, useContext } from "react";
|
|
469
|
+
import { jsx } from "react/jsx-runtime";
|
|
470
|
+
var AssistantClientContext = createContext(null);
|
|
471
|
+
function AssistantClientProvider({
|
|
472
|
+
client,
|
|
473
|
+
children
|
|
474
|
+
}) {
|
|
475
|
+
return /* @__PURE__ */ jsx(AssistantClientContext.Provider, { value: client, children });
|
|
476
|
+
}
|
|
477
|
+
function useAssistantClient() {
|
|
478
|
+
const client = useContext(AssistantClientContext);
|
|
479
|
+
if (!client) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
"useAssistantClient must be used within an <AssistantClientProvider>"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return client;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/assistant/useAssistantChat.ts
|
|
488
|
+
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
489
|
+
|
|
490
|
+
// src/assistant/persistence.ts
|
|
491
|
+
var VERSION = "v1";
|
|
492
|
+
function keyFor(userId) {
|
|
493
|
+
return userId ? `assistant:${VERSION}:${userId}` : null;
|
|
494
|
+
}
|
|
495
|
+
function loadThread(userId) {
|
|
496
|
+
const key = keyFor(userId);
|
|
497
|
+
if (!key) return { threadId: null, model: null };
|
|
498
|
+
try {
|
|
499
|
+
const raw = localStorage.getItem(key);
|
|
500
|
+
if (!raw) return { threadId: null, model: null };
|
|
501
|
+
const parsed = JSON.parse(raw);
|
|
502
|
+
return {
|
|
503
|
+
threadId: typeof parsed.threadId === "string" ? parsed.threadId : null,
|
|
504
|
+
model: typeof parsed.model === "string" ? parsed.model : null
|
|
505
|
+
};
|
|
506
|
+
} catch {
|
|
507
|
+
return { threadId: null, model: null };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function saveThread(userId, thread) {
|
|
511
|
+
const key = keyFor(userId);
|
|
512
|
+
if (!key) return;
|
|
513
|
+
try {
|
|
514
|
+
localStorage.setItem(
|
|
515
|
+
key,
|
|
516
|
+
JSON.stringify({ threadId: thread.threadId, model: thread.model })
|
|
517
|
+
);
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/assistant/presentation.ts
|
|
523
|
+
var LOW_BALANCE_THRESHOLD = 1;
|
|
524
|
+
var ADD_CREDITS_CTA = { label: "Add credits", to: "/app/billing" };
|
|
525
|
+
var CONNECT_CTA = {
|
|
526
|
+
label: "Connect an integration",
|
|
527
|
+
to: "/app/integrations"
|
|
528
|
+
};
|
|
529
|
+
function presentError(code, message) {
|
|
530
|
+
switch (code) {
|
|
531
|
+
case "INSUFFICIENT_BALANCE":
|
|
532
|
+
return {
|
|
533
|
+
message: "You're out of credits. Add credits to keep using the assistant.",
|
|
534
|
+
cta: ADD_CREDITS_CTA
|
|
535
|
+
};
|
|
536
|
+
case "MODEL_ACCESS_UNCONFIGURED":
|
|
537
|
+
return {
|
|
538
|
+
message: "Model access isn't configured for your account yet. Please contact support.",
|
|
539
|
+
cta: null
|
|
540
|
+
};
|
|
541
|
+
case "BILLING_UNAVAILABLE":
|
|
542
|
+
return {
|
|
543
|
+
message: "Billing is temporarily unavailable. Try again in a moment.",
|
|
544
|
+
cta: null
|
|
545
|
+
};
|
|
546
|
+
case "TOO_MANY_STREAMS":
|
|
547
|
+
return {
|
|
548
|
+
message: "You have too many assistant requests in flight. Wait a moment and retry.",
|
|
549
|
+
cta: null
|
|
550
|
+
};
|
|
551
|
+
case "THREAD_BUSY":
|
|
552
|
+
case "TURN_IN_PROGRESS":
|
|
553
|
+
return {
|
|
554
|
+
message: "A previous request is still finishing. Try again shortly.",
|
|
555
|
+
cta: null
|
|
556
|
+
};
|
|
557
|
+
case "INTEGRATION_DISCONNECTED":
|
|
558
|
+
return {
|
|
559
|
+
message: `${message} Connect the integration, then ask again.`,
|
|
560
|
+
cta: CONNECT_CTA
|
|
561
|
+
};
|
|
562
|
+
case "TOOL_FAILED":
|
|
563
|
+
case "NETWORK":
|
|
564
|
+
return { message: message || "Something went wrong.", cta: null };
|
|
565
|
+
default:
|
|
566
|
+
return { message: message || "Something went wrong.", cta: null };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function asRecord(args) {
|
|
570
|
+
return args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
571
|
+
}
|
|
572
|
+
function str(v) {
|
|
573
|
+
if (v == null) return "";
|
|
574
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
575
|
+
}
|
|
576
|
+
function nonEmptyStr(v) {
|
|
577
|
+
return typeof v === "string" && v.trim() !== "" ? v : null;
|
|
578
|
+
}
|
|
579
|
+
function parseProposalSkills(v) {
|
|
580
|
+
if (!Array.isArray(v) || v.length === 0) return void 0;
|
|
581
|
+
const out = [];
|
|
582
|
+
for (const item of v) {
|
|
583
|
+
const rec = asRecord(item);
|
|
584
|
+
const name = nonEmptyStr(rec.name);
|
|
585
|
+
if (!name) continue;
|
|
586
|
+
out.push({ name, description: nonEmptyStr(rec.description) });
|
|
587
|
+
}
|
|
588
|
+
return out.length > 0 ? out : void 0;
|
|
589
|
+
}
|
|
590
|
+
function humanizeToolName(name) {
|
|
591
|
+
const spaced = name.replace(/_/g, " ").trim();
|
|
592
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
593
|
+
}
|
|
594
|
+
function describeProposal(proposal) {
|
|
595
|
+
const args = asRecord(proposal.args);
|
|
596
|
+
const workflowYaml = nonEmptyStr(args.yaml);
|
|
597
|
+
const workflowPreview = workflowYaml ? {
|
|
598
|
+
label: "Workflow definition",
|
|
599
|
+
content: workflowYaml,
|
|
600
|
+
kind: "workflow"
|
|
601
|
+
} : null;
|
|
602
|
+
switch (proposal.name) {
|
|
603
|
+
case "create_workflow":
|
|
604
|
+
return { title: "Create workflow", preview: workflowPreview, fields: [] };
|
|
605
|
+
// author_workflow creates a workflow PLUS the new skills it needs in one
|
|
606
|
+
// unit; show the YAML and name each new skill rather than dumping the raw
|
|
607
|
+
// skills JSON (the card is the canonical, readable view of the proposal).
|
|
608
|
+
case "author_workflow":
|
|
609
|
+
return {
|
|
610
|
+
title: "Create workflow",
|
|
611
|
+
preview: workflowPreview,
|
|
612
|
+
fields: [],
|
|
613
|
+
skills: parseProposalSkills(args.skills)
|
|
614
|
+
};
|
|
615
|
+
case "update_workflow":
|
|
616
|
+
return {
|
|
617
|
+
title: "Update workflow",
|
|
618
|
+
preview: workflowPreview,
|
|
619
|
+
fields: [{ label: "Workflow id", value: str(args.id) }]
|
|
620
|
+
};
|
|
621
|
+
case "set_workflow_enabled":
|
|
622
|
+
return {
|
|
623
|
+
title: args.enabled ? "Enable workflow" : "Disable workflow",
|
|
624
|
+
preview: null,
|
|
625
|
+
fields: [{ label: "Workflow id", value: str(args.id) }]
|
|
626
|
+
};
|
|
627
|
+
case "create_skill": {
|
|
628
|
+
const prompt = nonEmptyStr(args.systemPrompt);
|
|
629
|
+
const fields = [
|
|
630
|
+
{ label: "Name", value: str(args.name) }
|
|
631
|
+
];
|
|
632
|
+
if (nonEmptyStr(args.description))
|
|
633
|
+
fields.push({ label: "Description", value: str(args.description) });
|
|
634
|
+
return {
|
|
635
|
+
title: "Create skill",
|
|
636
|
+
preview: prompt ? { label: "Instructions", content: prompt, kind: "text" } : null,
|
|
637
|
+
fields
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
case "update_skill": {
|
|
641
|
+
const prompt = nonEmptyStr(args.systemPrompt);
|
|
642
|
+
const fields = [
|
|
643
|
+
{ label: "Skill id", value: str(args.id) }
|
|
644
|
+
];
|
|
645
|
+
if (nonEmptyStr(args.name))
|
|
646
|
+
fields.push({ label: "Name", value: str(args.name) });
|
|
647
|
+
if (nonEmptyStr(args.description))
|
|
648
|
+
fields.push({ label: "Description", value: str(args.description) });
|
|
649
|
+
return {
|
|
650
|
+
title: "Update skill",
|
|
651
|
+
preview: prompt ? { label: "Instructions", content: prompt, kind: "text" } : null,
|
|
652
|
+
fields
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
case "delete_skill":
|
|
656
|
+
return {
|
|
657
|
+
title: "Delete skill",
|
|
658
|
+
preview: null,
|
|
659
|
+
fields: [{ label: "Skill id", value: str(args.id) }]
|
|
660
|
+
};
|
|
661
|
+
case "create_api_key": {
|
|
662
|
+
const fields = [
|
|
663
|
+
{ label: "Name", value: str(args.name) }
|
|
664
|
+
];
|
|
665
|
+
if (args.product != null)
|
|
666
|
+
fields.push({ label: "Product", value: str(args.product) });
|
|
667
|
+
if (args.budgetUsd != null)
|
|
668
|
+
fields.push({ label: "Budget (USD)", value: str(args.budgetUsd) });
|
|
669
|
+
return { title: "Create API key", preview: null, fields };
|
|
670
|
+
}
|
|
671
|
+
case "revoke_api_key":
|
|
672
|
+
return {
|
|
673
|
+
title: "Revoke API key",
|
|
674
|
+
preview: null,
|
|
675
|
+
fields: [{ label: "Key id", value: str(args.keyId) }]
|
|
676
|
+
};
|
|
677
|
+
case "invoke_integration": {
|
|
678
|
+
const fields = [
|
|
679
|
+
{ label: "Action", value: str(args.path) }
|
|
680
|
+
];
|
|
681
|
+
if (args.input != null)
|
|
682
|
+
fields.push({ label: "Input", value: str(args.input) });
|
|
683
|
+
return { title: "Run integration action", preview: null, fields };
|
|
684
|
+
}
|
|
685
|
+
default: {
|
|
686
|
+
const fields = Object.entries(args).map(([label, value]) => ({
|
|
687
|
+
label,
|
|
688
|
+
value: str(value)
|
|
689
|
+
}));
|
|
690
|
+
return { title: humanizeToolName(proposal.name), preview: null, fields };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function describeOutcome(name, output) {
|
|
695
|
+
const o = asRecord(output);
|
|
696
|
+
switch (name) {
|
|
697
|
+
case "author_workflow":
|
|
698
|
+
case "create_workflow": {
|
|
699
|
+
const wf = asRecord(o.workflow);
|
|
700
|
+
const skillCount = Array.isArray(o.skills) ? o.skills.length : 0;
|
|
701
|
+
if (wf.name) {
|
|
702
|
+
return skillCount > 0 ? `Created workflow "${str(wf.name)}" and ${skillCount} skill${skillCount === 1 ? "" : "s"}.` : `Created workflow "${str(wf.name)}".`;
|
|
703
|
+
}
|
|
704
|
+
return "Workflow created.";
|
|
705
|
+
}
|
|
706
|
+
case "update_workflow": {
|
|
707
|
+
const wf = asRecord(o.workflow);
|
|
708
|
+
return wf.name ? `Updated workflow "${str(wf.name)}".` : "Workflow updated.";
|
|
709
|
+
}
|
|
710
|
+
case "set_workflow_enabled": {
|
|
711
|
+
const wf = asRecord(o.workflow);
|
|
712
|
+
return wf.enabled ? "Workflow enabled." : "Workflow disabled.";
|
|
713
|
+
}
|
|
714
|
+
case "create_api_key":
|
|
715
|
+
return o.prefix ? `Created API key (${str(o.prefix)}\u2026). Copy it from the API Keys page.` : "API key created.";
|
|
716
|
+
case "revoke_api_key":
|
|
717
|
+
return "API key revoked.";
|
|
718
|
+
case "invoke_integration":
|
|
719
|
+
return "Integration action completed.";
|
|
720
|
+
default:
|
|
721
|
+
return "Action completed.";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function describeFailure(output) {
|
|
725
|
+
const o = asRecord(output);
|
|
726
|
+
const errors = Array.isArray(o.errors) ? o.errors : [];
|
|
727
|
+
const joined = errors.map((e) => typeof e === "string" ? e : str(asRecord(e).message)).filter((m) => m.length > 0).join("; ");
|
|
728
|
+
if (joined) return joined;
|
|
729
|
+
if (typeof o.message === "string" && o.message) return o.message;
|
|
730
|
+
if (o.notFound === true) return "That no longer exists.";
|
|
731
|
+
if (o.conflict === true) return "It changed since it was loaded. Try again.";
|
|
732
|
+
return "The action could not be completed.";
|
|
733
|
+
}
|
|
734
|
+
function isLowBalance(balanceUsd) {
|
|
735
|
+
return balanceUsd != null && balanceUsd < LOW_BALANCE_THRESHOLD;
|
|
736
|
+
}
|
|
737
|
+
function resolveConfirmation(name, result) {
|
|
738
|
+
if (result.ok) {
|
|
739
|
+
const out = asRecord(result.output);
|
|
740
|
+
if (out.ok === false && out.code === "NOT_CONNECTED") {
|
|
741
|
+
return {
|
|
742
|
+
statusText: null,
|
|
743
|
+
error: {
|
|
744
|
+
code: "INTEGRATION_DISCONNECTED",
|
|
745
|
+
message: str(out.message) || "That integration isn't connected."
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
if (out.created === false || out.updated === false || out.deleted === false || out.ok === false) {
|
|
750
|
+
return {
|
|
751
|
+
statusText: null,
|
|
752
|
+
error: { code: "TOOL_FAILED", message: describeFailure(result.output) }
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return { statusText: describeOutcome(name, result.output), error: null };
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
statusText: null,
|
|
759
|
+
error: { code: "TOOL_FAILED", message: result.error }
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/assistant/reducer.ts
|
|
764
|
+
var MAX_MESSAGES = 200;
|
|
765
|
+
function capMessages(messages) {
|
|
766
|
+
return messages.length > MAX_MESSAGES ? messages.slice(-MAX_MESSAGES) : messages;
|
|
767
|
+
}
|
|
768
|
+
function initialAssistantState() {
|
|
769
|
+
return {
|
|
770
|
+
ownerId: null,
|
|
771
|
+
threadId: null,
|
|
772
|
+
messages: [],
|
|
773
|
+
status: "idle",
|
|
774
|
+
streamingId: null,
|
|
775
|
+
streamBaseId: null,
|
|
776
|
+
segmentSeq: 0,
|
|
777
|
+
pendingProposals: [],
|
|
778
|
+
usage: null,
|
|
779
|
+
model: null,
|
|
780
|
+
reasoning: null,
|
|
781
|
+
error: null
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function selectVisibleState(state, userId) {
|
|
785
|
+
return state.ownerId === userId ? state : initialAssistantState();
|
|
786
|
+
}
|
|
787
|
+
function dropEmptyStreaming(messages, streamingId) {
|
|
788
|
+
if (!streamingId) return messages;
|
|
789
|
+
const msg = messages.find((m) => m.id === streamingId);
|
|
790
|
+
if (msg && msg.role === "assistant" && msg.text === "") {
|
|
791
|
+
return messages.filter((m) => m.id !== streamingId);
|
|
792
|
+
}
|
|
793
|
+
return messages;
|
|
794
|
+
}
|
|
795
|
+
function appendDelta(messages, streamingId, text) {
|
|
796
|
+
if (!streamingId) return messages;
|
|
797
|
+
return messages.map(
|
|
798
|
+
(m) => m.id === streamingId ? { ...m, text: m.text + text } : m
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
function applyStreamEvent(state, event) {
|
|
802
|
+
switch (event.type) {
|
|
803
|
+
case "thread":
|
|
804
|
+
return {
|
|
805
|
+
...state,
|
|
806
|
+
threadId: event.data.threadId,
|
|
807
|
+
// The model the server actually ran this turn against (lets the picker
|
|
808
|
+
// reflect reality even when the user never explicitly chose one).
|
|
809
|
+
model: event.data.model ?? state.model
|
|
810
|
+
};
|
|
811
|
+
case "delta": {
|
|
812
|
+
if (state.streamingId) {
|
|
813
|
+
return {
|
|
814
|
+
...state,
|
|
815
|
+
messages: appendDelta(
|
|
816
|
+
state.messages,
|
|
817
|
+
state.streamingId,
|
|
818
|
+
event.data.text
|
|
819
|
+
)
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (!state.streamBaseId) return state;
|
|
823
|
+
const segmentSeq = state.segmentSeq + 1;
|
|
824
|
+
const id = `${state.streamBaseId}-s${segmentSeq}`;
|
|
825
|
+
return {
|
|
826
|
+
...state,
|
|
827
|
+
segmentSeq,
|
|
828
|
+
streamingId: id,
|
|
829
|
+
messages: capMessages([
|
|
830
|
+
...state.messages,
|
|
831
|
+
{ id, role: "assistant", text: event.data.text }
|
|
832
|
+
])
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
case "reasoning":
|
|
836
|
+
return {
|
|
837
|
+
...state,
|
|
838
|
+
reasoning: (state.reasoning ?? "") + event.data.text
|
|
839
|
+
};
|
|
840
|
+
case "tool_call": {
|
|
841
|
+
const trimmed = dropEmptyStreaming(state.messages, state.streamingId);
|
|
842
|
+
const chipId = `tool-${event.data.callId}`;
|
|
843
|
+
if (trimmed.some((m) => m.id === chipId)) {
|
|
844
|
+
return { ...state, messages: trimmed, streamingId: null };
|
|
845
|
+
}
|
|
846
|
+
const chip = {
|
|
847
|
+
id: chipId,
|
|
848
|
+
role: "tool",
|
|
849
|
+
text: "",
|
|
850
|
+
tool: {
|
|
851
|
+
name: event.data.name,
|
|
852
|
+
status: "running",
|
|
853
|
+
args: event.data.args
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
return {
|
|
857
|
+
...state,
|
|
858
|
+
streamingId: null,
|
|
859
|
+
messages: capMessages([...trimmed, chip])
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
case "tool_result": {
|
|
863
|
+
const chipId = `tool-${event.data.callId}`;
|
|
864
|
+
const status = event.data.ok ? "ok" : "failed";
|
|
865
|
+
const errText = event.data.ok ? "" : event.data.error?.message ?? "unknown error";
|
|
866
|
+
const outcome = event.data.ok ? { ok: true, result: event.data.output } : { ok: false, error: event.data.error };
|
|
867
|
+
if (state.messages.some((m) => m.id === chipId)) {
|
|
868
|
+
return {
|
|
869
|
+
...state,
|
|
870
|
+
messages: state.messages.map(
|
|
871
|
+
(m) => m.id === chipId ? {
|
|
872
|
+
...m,
|
|
873
|
+
text: errText,
|
|
874
|
+
// Preserve the args the matching tool_call recorded — the
|
|
875
|
+
// result event doesn't carry them.
|
|
876
|
+
tool: {
|
|
877
|
+
name: event.data.name,
|
|
878
|
+
status,
|
|
879
|
+
args: m.tool?.args,
|
|
880
|
+
outcome
|
|
881
|
+
}
|
|
882
|
+
} : m
|
|
883
|
+
)
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const chip = {
|
|
887
|
+
id: chipId,
|
|
888
|
+
role: "tool",
|
|
889
|
+
text: errText,
|
|
890
|
+
tool: { name: event.data.name, status, outcome }
|
|
891
|
+
};
|
|
892
|
+
return { ...state, messages: capMessages([...state.messages, chip]) };
|
|
893
|
+
}
|
|
894
|
+
case "tool_proposal": {
|
|
895
|
+
if (state.pendingProposals.some((p) => p.callId === event.data.callId)) {
|
|
896
|
+
return state;
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
...state,
|
|
900
|
+
pendingProposals: [...state.pendingProposals, event.data]
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
case "usage":
|
|
904
|
+
return {
|
|
905
|
+
...state,
|
|
906
|
+
usage: {
|
|
907
|
+
costUsd: event.data.costUsd,
|
|
908
|
+
balanceUsd: event.data.balanceUsd,
|
|
909
|
+
promptTokens: event.data.promptTokens,
|
|
910
|
+
completionTokens: event.data.completionTokens,
|
|
911
|
+
durationMs: event.data.durationMs ?? null,
|
|
912
|
+
replayed: event.data.replayed ?? false
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
case "done": {
|
|
916
|
+
let messages = dropEmptyStreaming(state.messages, state.streamingId);
|
|
917
|
+
if (event.data.capped) {
|
|
918
|
+
messages = capMessages([
|
|
919
|
+
...messages,
|
|
920
|
+
{
|
|
921
|
+
id: `cap-${event.data.turnId}`,
|
|
922
|
+
role: "status",
|
|
923
|
+
text: "I reached the step limit for this turn. Ask me to continue and I'll pick up where I left off."
|
|
924
|
+
}
|
|
925
|
+
]);
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
...state,
|
|
929
|
+
messages,
|
|
930
|
+
streamingId: null,
|
|
931
|
+
status: state.pendingProposals.length > 0 ? "awaiting_confirm" : "idle"
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
case "error": {
|
|
935
|
+
const messages = dropEmptyStreaming(state.messages, state.streamingId);
|
|
936
|
+
return {
|
|
937
|
+
...state,
|
|
938
|
+
messages,
|
|
939
|
+
streamingId: null,
|
|
940
|
+
status: "idle",
|
|
941
|
+
// A turn that didn't complete cleanly leaves no confirmable action: a
|
|
942
|
+
// proposal buffered before the failure must not stay actionable.
|
|
943
|
+
pendingProposals: [],
|
|
944
|
+
error: { code: event.data.code, message: event.data.message }
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function assistantReducer(state, action) {
|
|
950
|
+
switch (action.type) {
|
|
951
|
+
case "send":
|
|
952
|
+
return {
|
|
953
|
+
...state,
|
|
954
|
+
messages: capMessages([
|
|
955
|
+
...state.messages,
|
|
956
|
+
{ id: action.messageId, role: "user", text: action.text },
|
|
957
|
+
{ id: action.assistantId, role: "assistant", text: "" }
|
|
958
|
+
]),
|
|
959
|
+
status: "streaming",
|
|
960
|
+
streamingId: action.assistantId,
|
|
961
|
+
// The first bubble is segment 0; tool-finalized segments derive their id
|
|
962
|
+
// from this base, so they stay unique across turns.
|
|
963
|
+
streamBaseId: action.assistantId,
|
|
964
|
+
segmentSeq: 0,
|
|
965
|
+
usage: null,
|
|
966
|
+
reasoning: null,
|
|
967
|
+
error: null
|
|
968
|
+
};
|
|
969
|
+
case "stream":
|
|
970
|
+
return applyStreamEvent(state, action.event);
|
|
971
|
+
case "stream_failed": {
|
|
972
|
+
const messages = dropEmptyStreaming(state.messages, state.streamingId);
|
|
973
|
+
return {
|
|
974
|
+
...state,
|
|
975
|
+
messages,
|
|
976
|
+
streamingId: null,
|
|
977
|
+
status: "idle",
|
|
978
|
+
// A failed turn leaves no confirmable action (see the `error` case).
|
|
979
|
+
pendingProposals: [],
|
|
980
|
+
error: action.error
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
case "proposal_resolved": {
|
|
984
|
+
const pendingProposals = state.pendingProposals.filter(
|
|
985
|
+
(p) => p.callId !== action.callId
|
|
986
|
+
);
|
|
987
|
+
const messages = action.status ? capMessages([...state.messages, action.status]) : state.messages;
|
|
988
|
+
return {
|
|
989
|
+
...state,
|
|
990
|
+
messages,
|
|
991
|
+
pendingProposals,
|
|
992
|
+
error: action.error,
|
|
993
|
+
status: pendingProposals.length > 0 ? "awaiting_confirm" : state.status === "awaiting_confirm" ? "idle" : state.status
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
case "proposal_retry_failed": {
|
|
997
|
+
const pendingProposals = state.pendingProposals.map(
|
|
998
|
+
(p) => p.callId === action.callId ? { ...p, retryError: action.message } : p
|
|
999
|
+
);
|
|
1000
|
+
return {
|
|
1001
|
+
...state,
|
|
1002
|
+
pendingProposals,
|
|
1003
|
+
status: pendingProposals.length > 0 ? "awaiting_confirm" : state.status === "awaiting_confirm" ? "idle" : state.status
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
case "stopped":
|
|
1007
|
+
return {
|
|
1008
|
+
...state,
|
|
1009
|
+
messages: dropEmptyStreaming(state.messages, state.streamingId),
|
|
1010
|
+
status: "idle",
|
|
1011
|
+
streamingId: null,
|
|
1012
|
+
pendingProposals: []
|
|
1013
|
+
};
|
|
1014
|
+
case "hydrate":
|
|
1015
|
+
return {
|
|
1016
|
+
...initialAssistantState(),
|
|
1017
|
+
ownerId: action.ownerId,
|
|
1018
|
+
threadId: action.threadId,
|
|
1019
|
+
messages: action.messages
|
|
1020
|
+
};
|
|
1021
|
+
case "restore_history":
|
|
1022
|
+
if (state.ownerId !== action.ownerId || state.threadId !== action.threadId || state.status !== "idle" || state.messages.length > 0 || state.pendingProposals.length > 0) {
|
|
1023
|
+
return state;
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
...state,
|
|
1027
|
+
messages: capMessages(action.messages),
|
|
1028
|
+
pendingProposals: action.proposals,
|
|
1029
|
+
status: action.proposals.length > 0 ? "awaiting_confirm" : state.status
|
|
1030
|
+
};
|
|
1031
|
+
case "thread_gone":
|
|
1032
|
+
if (state.ownerId !== action.ownerId || state.threadId !== action.threadId || state.status !== "idle" || state.messages.length > 0) {
|
|
1033
|
+
return state;
|
|
1034
|
+
}
|
|
1035
|
+
return { ...state, threadId: null };
|
|
1036
|
+
case "history_failed":
|
|
1037
|
+
if (state.ownerId !== action.ownerId || state.threadId !== action.threadId || state.status !== "idle" || state.messages.length > 0) {
|
|
1038
|
+
return state;
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
...initialAssistantState(),
|
|
1042
|
+
ownerId: state.ownerId,
|
|
1043
|
+
error: action.error
|
|
1044
|
+
};
|
|
1045
|
+
case "switch_thread":
|
|
1046
|
+
if (state.threadId === action.threadId) return state;
|
|
1047
|
+
return {
|
|
1048
|
+
...initialAssistantState(),
|
|
1049
|
+
ownerId: state.ownerId,
|
|
1050
|
+
threadId: action.threadId
|
|
1051
|
+
};
|
|
1052
|
+
case "reset":
|
|
1053
|
+
return { ...initialAssistantState(), ownerId: state.ownerId };
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/assistant/useAssistantChat.ts
|
|
1058
|
+
var EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
1059
|
+
var WORKFLOW_MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
1060
|
+
"create_workflow",
|
|
1061
|
+
"author_workflow",
|
|
1062
|
+
"update_workflow",
|
|
1063
|
+
"set_workflow_enabled"
|
|
1064
|
+
]);
|
|
1065
|
+
function statusMessage(text) {
|
|
1066
|
+
return { id: `status-${uuid()}`, role: "status", text };
|
|
1067
|
+
}
|
|
1068
|
+
function uuid() {
|
|
1069
|
+
return crypto.randomUUID();
|
|
1070
|
+
}
|
|
1071
|
+
function useAssistantChat(userId, options) {
|
|
1072
|
+
const [state, dispatch] = useReducer(
|
|
1073
|
+
assistantReducer,
|
|
1074
|
+
userId,
|
|
1075
|
+
(uid) => {
|
|
1076
|
+
return {
|
|
1077
|
+
...initialAssistantState(),
|
|
1078
|
+
ownerId: uid,
|
|
1079
|
+
threadId: loadThread(uid).threadId
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
);
|
|
1083
|
+
const abortRef = useRef(null);
|
|
1084
|
+
const historyAbortRef = useRef(null);
|
|
1085
|
+
const hydratedUserRef = useRef(userId);
|
|
1086
|
+
const streamSeqRef = useRef(0);
|
|
1087
|
+
const confirmSeqRef = useRef(0);
|
|
1088
|
+
const stateRef = useRef(state);
|
|
1089
|
+
const userIdRef = useRef(userId);
|
|
1090
|
+
stateRef.current = state;
|
|
1091
|
+
userIdRef.current = userId;
|
|
1092
|
+
const client = useAssistantClient();
|
|
1093
|
+
const clientRef = useRef(client);
|
|
1094
|
+
clientRef.current = client;
|
|
1095
|
+
const onWorkflowMutationRef = useRef(options?.onWorkflowMutation);
|
|
1096
|
+
onWorkflowMutationRef.current = options?.onWorkflowMutation;
|
|
1097
|
+
const confirmingRef = useRef(/* @__PURE__ */ new Set());
|
|
1098
|
+
const [confirmingIds, setConfirmingIds] = useState(EMPTY_IDS);
|
|
1099
|
+
const sendingRef = useRef(false);
|
|
1100
|
+
const restoringRef = useRef(false);
|
|
1101
|
+
const [restoring, setRestoring] = useState(false);
|
|
1102
|
+
const setRestoringBoth = useCallback((v) => {
|
|
1103
|
+
restoringRef.current = v;
|
|
1104
|
+
setRestoring(v);
|
|
1105
|
+
}, []);
|
|
1106
|
+
const [selectedModel, setSelectedModel] = useState(
|
|
1107
|
+
() => loadThread(userId).model
|
|
1108
|
+
);
|
|
1109
|
+
const selectedModelRef = useRef(selectedModel);
|
|
1110
|
+
selectedModelRef.current = selectedModel;
|
|
1111
|
+
useEffect(() => {
|
|
1112
|
+
if (hydratedUserRef.current === userId) return;
|
|
1113
|
+
streamSeqRef.current += 1;
|
|
1114
|
+
confirmSeqRef.current += 1;
|
|
1115
|
+
abortRef.current?.abort();
|
|
1116
|
+
historyAbortRef.current?.abort();
|
|
1117
|
+
setRestoringBoth(false);
|
|
1118
|
+
sendingRef.current = false;
|
|
1119
|
+
confirmingRef.current.clear();
|
|
1120
|
+
setConfirmingIds(EMPTY_IDS);
|
|
1121
|
+
hydratedUserRef.current = userId;
|
|
1122
|
+
const nextModel = loadThread(userId).model;
|
|
1123
|
+
selectedModelRef.current = nextModel;
|
|
1124
|
+
setSelectedModel(nextModel);
|
|
1125
|
+
dispatch({
|
|
1126
|
+
type: "hydrate",
|
|
1127
|
+
ownerId: userId,
|
|
1128
|
+
threadId: loadThread(userId).threadId,
|
|
1129
|
+
messages: []
|
|
1130
|
+
});
|
|
1131
|
+
}, [userId, setRestoringBoth]);
|
|
1132
|
+
useEffect(() => {
|
|
1133
|
+
const threadId = loadThread(userId).threadId;
|
|
1134
|
+
if (!userId || !threadId) return;
|
|
1135
|
+
const ac = new AbortController();
|
|
1136
|
+
historyAbortRef.current?.abort();
|
|
1137
|
+
historyAbortRef.current = ac;
|
|
1138
|
+
void clientRef.current.fetchThreadHistory(threadId, ac.signal).then((result) => {
|
|
1139
|
+
if (ac.signal.aborted) return;
|
|
1140
|
+
if (result.status === "ok") {
|
|
1141
|
+
if (result.messages.length > 0 || result.proposals.length > 0) {
|
|
1142
|
+
dispatch({
|
|
1143
|
+
type: "restore_history",
|
|
1144
|
+
ownerId: userId,
|
|
1145
|
+
threadId,
|
|
1146
|
+
messages: result.messages,
|
|
1147
|
+
proposals: result.proposals
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
} else if (result.status === "gone") {
|
|
1151
|
+
dispatch({ type: "thread_gone", ownerId: userId, threadId });
|
|
1152
|
+
}
|
|
1153
|
+
}).catch(() => {
|
|
1154
|
+
});
|
|
1155
|
+
return () => ac.abort();
|
|
1156
|
+
}, [userId]);
|
|
1157
|
+
useEffect(() => {
|
|
1158
|
+
if (state.status === "streaming") return;
|
|
1159
|
+
if (state.ownerId !== userIdRef.current) return;
|
|
1160
|
+
saveThread(state.ownerId, {
|
|
1161
|
+
threadId: state.threadId,
|
|
1162
|
+
model: selectedModelRef.current
|
|
1163
|
+
});
|
|
1164
|
+
}, [state.ownerId, state.threadId, state.status]);
|
|
1165
|
+
useEffect(() => {
|
|
1166
|
+
return () => abortRef.current?.abort();
|
|
1167
|
+
}, []);
|
|
1168
|
+
const send = useCallback((message) => {
|
|
1169
|
+
if (sendingRef.current) return;
|
|
1170
|
+
const text = message.trim();
|
|
1171
|
+
const current = stateRef.current;
|
|
1172
|
+
if (current.ownerId !== userIdRef.current) return;
|
|
1173
|
+
if (restoringRef.current) return;
|
|
1174
|
+
if (!text || current.status === "streaming") return;
|
|
1175
|
+
if (current.pendingProposals.length > 0) return;
|
|
1176
|
+
dispatch({
|
|
1177
|
+
type: "send",
|
|
1178
|
+
messageId: uuid(),
|
|
1179
|
+
assistantId: uuid(),
|
|
1180
|
+
text
|
|
1181
|
+
});
|
|
1182
|
+
const seq = ++streamSeqRef.current;
|
|
1183
|
+
const ac = new AbortController();
|
|
1184
|
+
abortRef.current = ac;
|
|
1185
|
+
sendingRef.current = true;
|
|
1186
|
+
clientRef.current.streamChat(
|
|
1187
|
+
{
|
|
1188
|
+
message: text,
|
|
1189
|
+
// null → omit, so the server applies its default model.
|
|
1190
|
+
model: selectedModelRef.current ?? void 0,
|
|
1191
|
+
threadId: current.threadId ?? void 0,
|
|
1192
|
+
turnKey: uuid()
|
|
1193
|
+
},
|
|
1194
|
+
(event) => {
|
|
1195
|
+
if (streamSeqRef.current === seq) dispatch({ type: "stream", event });
|
|
1196
|
+
},
|
|
1197
|
+
ac.signal
|
|
1198
|
+
).catch((err) => {
|
|
1199
|
+
if (ac.signal.aborted || streamSeqRef.current !== seq) return;
|
|
1200
|
+
dispatch({
|
|
1201
|
+
type: "stream_failed",
|
|
1202
|
+
error: {
|
|
1203
|
+
code: "NETWORK",
|
|
1204
|
+
message: err instanceof Error ? err.message : "The connection failed"
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}).finally(() => {
|
|
1208
|
+
if (streamSeqRef.current === seq) sendingRef.current = false;
|
|
1209
|
+
});
|
|
1210
|
+
}, []);
|
|
1211
|
+
const stop = useCallback(() => {
|
|
1212
|
+
streamSeqRef.current += 1;
|
|
1213
|
+
abortRef.current?.abort();
|
|
1214
|
+
sendingRef.current = false;
|
|
1215
|
+
dispatch({ type: "stopped" });
|
|
1216
|
+
}, []);
|
|
1217
|
+
const confirm = useCallback(async (proposal) => {
|
|
1218
|
+
const pid = proposal.proposalId;
|
|
1219
|
+
if (!pid) {
|
|
1220
|
+
dispatch({
|
|
1221
|
+
type: "proposal_resolved",
|
|
1222
|
+
callId: proposal.callId,
|
|
1223
|
+
status: null,
|
|
1224
|
+
error: {
|
|
1225
|
+
code: "TOOL_FAILED",
|
|
1226
|
+
message: "This action can no longer be confirmed."
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (confirmingRef.current.has(pid)) return;
|
|
1232
|
+
confirmingRef.current.add(pid);
|
|
1233
|
+
setConfirmingIds(new Set(confirmingRef.current));
|
|
1234
|
+
const seq = confirmSeqRef.current;
|
|
1235
|
+
try {
|
|
1236
|
+
const result = await clientRef.current.confirmProposal(pid);
|
|
1237
|
+
if (confirmSeqRef.current !== seq) return;
|
|
1238
|
+
if (result.ok && result.retryable) {
|
|
1239
|
+
const { error: error2 } = resolveConfirmation(proposal.name, result);
|
|
1240
|
+
dispatch({
|
|
1241
|
+
type: "proposal_retry_failed",
|
|
1242
|
+
callId: proposal.callId,
|
|
1243
|
+
message: error2?.message ?? "Connect the required integration, then confirm again."
|
|
1244
|
+
});
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const { statusText, error } = resolveConfirmation(proposal.name, result);
|
|
1248
|
+
dispatch({
|
|
1249
|
+
type: "proposal_resolved",
|
|
1250
|
+
callId: proposal.callId,
|
|
1251
|
+
status: statusText ? statusMessage(statusText) : null,
|
|
1252
|
+
error
|
|
1253
|
+
});
|
|
1254
|
+
if (!error && WORKFLOW_MUTATING_TOOLS.has(proposal.name)) {
|
|
1255
|
+
onWorkflowMutationRef.current?.();
|
|
1256
|
+
}
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
if (confirmSeqRef.current !== seq) return;
|
|
1259
|
+
dispatch({
|
|
1260
|
+
type: "proposal_resolved",
|
|
1261
|
+
callId: proposal.callId,
|
|
1262
|
+
status: null,
|
|
1263
|
+
error: {
|
|
1264
|
+
code: "TOOL_FAILED",
|
|
1265
|
+
message: err instanceof Error ? err.message : "The action could not be completed"
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
} finally {
|
|
1269
|
+
if (confirmSeqRef.current === seq) {
|
|
1270
|
+
confirmingRef.current.delete(pid);
|
|
1271
|
+
setConfirmingIds(new Set(confirmingRef.current));
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}, []);
|
|
1275
|
+
const cancel = useCallback((proposal) => {
|
|
1276
|
+
if (proposal.proposalId && confirmingRef.current.has(proposal.proposalId)) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
dispatch({
|
|
1280
|
+
type: "proposal_resolved",
|
|
1281
|
+
callId: proposal.callId,
|
|
1282
|
+
status: statusMessage("Action cancelled."),
|
|
1283
|
+
error: null
|
|
1284
|
+
});
|
|
1285
|
+
}, []);
|
|
1286
|
+
const reset = useCallback(() => {
|
|
1287
|
+
streamSeqRef.current += 1;
|
|
1288
|
+
confirmSeqRef.current += 1;
|
|
1289
|
+
abortRef.current?.abort();
|
|
1290
|
+
historyAbortRef.current?.abort();
|
|
1291
|
+
setRestoringBoth(false);
|
|
1292
|
+
sendingRef.current = false;
|
|
1293
|
+
confirmingRef.current.clear();
|
|
1294
|
+
setConfirmingIds(EMPTY_IDS);
|
|
1295
|
+
dispatch({ type: "reset" });
|
|
1296
|
+
}, [setRestoringBoth]);
|
|
1297
|
+
const switchThread = useCallback(
|
|
1298
|
+
(threadId) => {
|
|
1299
|
+
const current = stateRef.current;
|
|
1300
|
+
const uid = userIdRef.current;
|
|
1301
|
+
if (current.ownerId !== uid) return;
|
|
1302
|
+
if (threadId === current.threadId) return;
|
|
1303
|
+
if (current.status === "streaming" || current.pendingProposals.length > 0) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
streamSeqRef.current += 1;
|
|
1307
|
+
confirmSeqRef.current += 1;
|
|
1308
|
+
abortRef.current?.abort();
|
|
1309
|
+
sendingRef.current = false;
|
|
1310
|
+
confirmingRef.current.clear();
|
|
1311
|
+
setConfirmingIds(EMPTY_IDS);
|
|
1312
|
+
dispatch({ type: "switch_thread", threadId });
|
|
1313
|
+
setRestoringBoth(true);
|
|
1314
|
+
saveThread(uid, { threadId, model: selectedModelRef.current });
|
|
1315
|
+
const ac = new AbortController();
|
|
1316
|
+
historyAbortRef.current?.abort();
|
|
1317
|
+
historyAbortRef.current = ac;
|
|
1318
|
+
void clientRef.current.fetchThreadHistory(threadId, ac.signal).then((result) => {
|
|
1319
|
+
if (ac.signal.aborted) return;
|
|
1320
|
+
if (result.status === "ok") {
|
|
1321
|
+
if (result.messages.length > 0 || result.proposals.length > 0) {
|
|
1322
|
+
dispatch({
|
|
1323
|
+
type: "restore_history",
|
|
1324
|
+
ownerId: uid,
|
|
1325
|
+
threadId,
|
|
1326
|
+
messages: result.messages,
|
|
1327
|
+
proposals: result.proposals
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
} else if (result.status === "gone") {
|
|
1331
|
+
dispatch({ type: "thread_gone", ownerId: uid, threadId });
|
|
1332
|
+
} else {
|
|
1333
|
+
dispatch({
|
|
1334
|
+
type: "history_failed",
|
|
1335
|
+
ownerId: uid,
|
|
1336
|
+
threadId,
|
|
1337
|
+
error: {
|
|
1338
|
+
code: "HISTORY_LOAD_FAILED",
|
|
1339
|
+
message: "Couldn't load that conversation. You're in a new chat \u2014 reopen it from history to try again."
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
setRestoringBoth(false);
|
|
1344
|
+
}).catch(() => {
|
|
1345
|
+
if (!ac.signal.aborted) setRestoringBoth(false);
|
|
1346
|
+
});
|
|
1347
|
+
},
|
|
1348
|
+
[setRestoringBoth]
|
|
1349
|
+
);
|
|
1350
|
+
const setModel = useCallback((model) => {
|
|
1351
|
+
selectedModelRef.current = model;
|
|
1352
|
+
setSelectedModel(model);
|
|
1353
|
+
const currentUserId = userIdRef.current;
|
|
1354
|
+
if (stateRef.current.ownerId === currentUserId) {
|
|
1355
|
+
saveThread(currentUserId, {
|
|
1356
|
+
threadId: stateRef.current.threadId,
|
|
1357
|
+
model
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}, []);
|
|
1361
|
+
useEffect(() => {
|
|
1362
|
+
if (state.error?.code === "MODEL_NOT_ALLOWED" && state.ownerId === userIdRef.current && selectedModelRef.current !== null) {
|
|
1363
|
+
setModel(null);
|
|
1364
|
+
}
|
|
1365
|
+
}, [state.error, state.ownerId, setModel]);
|
|
1366
|
+
const visibleState = selectVisibleState(state, userId);
|
|
1367
|
+
return {
|
|
1368
|
+
state: visibleState,
|
|
1369
|
+
confirmingIds,
|
|
1370
|
+
selectedModel,
|
|
1371
|
+
setModel,
|
|
1372
|
+
send,
|
|
1373
|
+
stop,
|
|
1374
|
+
confirm,
|
|
1375
|
+
cancel,
|
|
1376
|
+
reset,
|
|
1377
|
+
switchThread,
|
|
1378
|
+
restoring
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/assistant/useAssistantModels.ts
|
|
1383
|
+
import { useEffect as useEffect2, useReducer as useReducer2 } from "react";
|
|
1384
|
+
var EMPTY = { default: null, models: [] };
|
|
1385
|
+
var byClient = /* @__PURE__ */ new WeakMap();
|
|
1386
|
+
function cacheFor(client) {
|
|
1387
|
+
let entry = byClient.get(client);
|
|
1388
|
+
if (!entry) {
|
|
1389
|
+
entry = { cache: null, inflight: null };
|
|
1390
|
+
byClient.set(client, entry);
|
|
1391
|
+
}
|
|
1392
|
+
return entry;
|
|
1393
|
+
}
|
|
1394
|
+
function useAssistantModels() {
|
|
1395
|
+
const client = useAssistantClient();
|
|
1396
|
+
const [, bump] = useReducer2((n) => n + 1, 0);
|
|
1397
|
+
useEffect2(() => {
|
|
1398
|
+
const entry = cacheFor(client);
|
|
1399
|
+
if (entry.cache) return;
|
|
1400
|
+
let active = true;
|
|
1401
|
+
entry.inflight ??= client.fetchModels();
|
|
1402
|
+
void entry.inflight.then((result) => {
|
|
1403
|
+
if (result.ok) entry.cache = result.data;
|
|
1404
|
+
entry.inflight = null;
|
|
1405
|
+
if (active) bump();
|
|
1406
|
+
}).catch(() => {
|
|
1407
|
+
entry.inflight = null;
|
|
1408
|
+
});
|
|
1409
|
+
return () => {
|
|
1410
|
+
active = false;
|
|
1411
|
+
};
|
|
1412
|
+
}, [client]);
|
|
1413
|
+
return cacheFor(client).cache ?? EMPTY;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/assistant/useAssistantThreads.ts
|
|
1417
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
|
|
1418
|
+
function useAssistantThreads(userId) {
|
|
1419
|
+
const client = useAssistantClient();
|
|
1420
|
+
const userRef = useRef2(userId);
|
|
1421
|
+
userRef.current = userId;
|
|
1422
|
+
const clientRef = useRef2(client);
|
|
1423
|
+
clientRef.current = client;
|
|
1424
|
+
const abortRef = useRef2(null);
|
|
1425
|
+
const pendingDeletesRef = useRef2(/* @__PURE__ */ new Set());
|
|
1426
|
+
const [state, setState] = useState2(() => ({
|
|
1427
|
+
threads: [],
|
|
1428
|
+
loading: false,
|
|
1429
|
+
loaded: false,
|
|
1430
|
+
ownerUserId: userId,
|
|
1431
|
+
ownerClient: client
|
|
1432
|
+
}));
|
|
1433
|
+
const refresh = useCallback2(() => {
|
|
1434
|
+
const requestedUserId = userRef.current;
|
|
1435
|
+
const requestedClient = clientRef.current;
|
|
1436
|
+
if (!requestedUserId) {
|
|
1437
|
+
setState({
|
|
1438
|
+
threads: [],
|
|
1439
|
+
loading: false,
|
|
1440
|
+
loaded: true,
|
|
1441
|
+
ownerUserId: requestedUserId,
|
|
1442
|
+
ownerClient: requestedClient
|
|
1443
|
+
});
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
abortRef.current?.abort();
|
|
1447
|
+
const ac = new AbortController();
|
|
1448
|
+
abortRef.current = ac;
|
|
1449
|
+
setState((s) => ({
|
|
1450
|
+
...s,
|
|
1451
|
+
loading: true,
|
|
1452
|
+
ownerUserId: requestedUserId,
|
|
1453
|
+
ownerClient: requestedClient
|
|
1454
|
+
}));
|
|
1455
|
+
const isCurrent = () => !ac.signal.aborted && userRef.current === requestedUserId && clientRef.current === requestedClient;
|
|
1456
|
+
void requestedClient.fetchThreads(ac.signal).then((result) => {
|
|
1457
|
+
if (!isCurrent()) return;
|
|
1458
|
+
setState((s) => ({
|
|
1459
|
+
// null = transient failure: keep the prior list, just drop the spinner.
|
|
1460
|
+
// Drop any in-flight/finished deletions so a stale fetch can't resurrect
|
|
1461
|
+
// a row we already removed.
|
|
1462
|
+
threads: (result ?? s.threads).filter(
|
|
1463
|
+
(t) => !pendingDeletesRef.current.has(t.id)
|
|
1464
|
+
),
|
|
1465
|
+
loading: false,
|
|
1466
|
+
loaded: true,
|
|
1467
|
+
ownerUserId: requestedUserId,
|
|
1468
|
+
ownerClient: requestedClient
|
|
1469
|
+
}));
|
|
1470
|
+
}).catch(() => {
|
|
1471
|
+
if (isCurrent()) {
|
|
1472
|
+
setState((s) => ({ ...s, loading: false, loaded: true }));
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
}, []);
|
|
1476
|
+
const remove = useCallback2(
|
|
1477
|
+
async (threadId) => {
|
|
1478
|
+
const requestedClient = clientRef.current;
|
|
1479
|
+
const requestedUserId = userRef.current;
|
|
1480
|
+
if (!requestedClient.deleteThread) return { ok: false };
|
|
1481
|
+
pendingDeletesRef.current.add(threadId);
|
|
1482
|
+
setState(
|
|
1483
|
+
(s) => s.ownerClient === requestedClient && s.ownerUserId === requestedUserId ? { ...s, threads: s.threads.filter((t) => t.id !== threadId) } : s
|
|
1484
|
+
);
|
|
1485
|
+
let res;
|
|
1486
|
+
try {
|
|
1487
|
+
res = await requestedClient.deleteThread(threadId);
|
|
1488
|
+
} catch {
|
|
1489
|
+
res = { ok: false };
|
|
1490
|
+
}
|
|
1491
|
+
if (!res.ok) {
|
|
1492
|
+
pendingDeletesRef.current.delete(threadId);
|
|
1493
|
+
if (userRef.current === requestedUserId && clientRef.current === requestedClient) {
|
|
1494
|
+
refresh();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return res;
|
|
1498
|
+
},
|
|
1499
|
+
[refresh]
|
|
1500
|
+
);
|
|
1501
|
+
useEffect3(() => {
|
|
1502
|
+
return () => abortRef.current?.abort();
|
|
1503
|
+
}, [userId, client]);
|
|
1504
|
+
useEffect3(() => {
|
|
1505
|
+
return () => pendingDeletesRef.current.clear();
|
|
1506
|
+
}, []);
|
|
1507
|
+
const stale = state.ownerUserId !== userId || state.ownerClient !== client;
|
|
1508
|
+
return {
|
|
1509
|
+
threads: stale ? [] : state.threads,
|
|
1510
|
+
loading: stale ? false : state.loading,
|
|
1511
|
+
loaded: stale ? false : state.loaded,
|
|
1512
|
+
refresh,
|
|
1513
|
+
remove,
|
|
1514
|
+
canRemove: typeof client.deleteThread === "function"
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/assistant/AssistantDock.tsx
|
|
1519
|
+
import { MessageSquare } from "lucide-react";
|
|
1520
|
+
import {
|
|
1521
|
+
useEffect as useEffect6,
|
|
1522
|
+
useRef as useRef6
|
|
1523
|
+
} from "react";
|
|
1524
|
+
|
|
1525
|
+
// src/assistant/AssistantPanel.tsx
|
|
1526
|
+
import { History, MessageSquarePlus, Minus, Plus, X } from "lucide-react";
|
|
1527
|
+
import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef4, useState as useState6 } from "react";
|
|
1528
|
+
|
|
1529
|
+
// src/assistant/AssistantHistory.tsx
|
|
1530
|
+
import { Search, Trash2 } from "lucide-react";
|
|
1531
|
+
import { useMemo, useState as useState3 } from "react";
|
|
1532
|
+
|
|
1533
|
+
// src/assistant/time-ago.ts
|
|
1534
|
+
function timeAgo(ts) {
|
|
1535
|
+
const secs = Math.floor((Date.now() - ts) / 1e3);
|
|
1536
|
+
if (secs < 5) return "just now";
|
|
1537
|
+
if (secs < 60) return `${secs}s ago`;
|
|
1538
|
+
const mins = Math.floor(secs / 60);
|
|
1539
|
+
if (mins < 60) return `${mins}m ago`;
|
|
1540
|
+
const hrs = Math.floor(mins / 60);
|
|
1541
|
+
return `${hrs}h ago`;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// src/assistant/AssistantHistory.tsx
|
|
1545
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1546
|
+
function parsedTime(iso) {
|
|
1547
|
+
if (!iso) return null;
|
|
1548
|
+
const ms = Date.parse(iso);
|
|
1549
|
+
return Number.isFinite(ms) ? ms : null;
|
|
1550
|
+
}
|
|
1551
|
+
function AssistantHistory({
|
|
1552
|
+
threads,
|
|
1553
|
+
loaded,
|
|
1554
|
+
activeThreadId,
|
|
1555
|
+
activeBusy,
|
|
1556
|
+
canRemove,
|
|
1557
|
+
onSelect,
|
|
1558
|
+
onDelete
|
|
1559
|
+
}) {
|
|
1560
|
+
const [query, setQuery] = useState3("");
|
|
1561
|
+
const sorted = useMemo(
|
|
1562
|
+
() => [...threads].sort((a, b) => {
|
|
1563
|
+
const ta = parsedTime(a.updatedAt);
|
|
1564
|
+
const tb = parsedTime(b.updatedAt);
|
|
1565
|
+
if (ta === null && tb === null) return 0;
|
|
1566
|
+
if (ta === null) return 1;
|
|
1567
|
+
if (tb === null) return -1;
|
|
1568
|
+
return tb - ta;
|
|
1569
|
+
}),
|
|
1570
|
+
[threads]
|
|
1571
|
+
);
|
|
1572
|
+
const trimmed = query.trim().toLowerCase();
|
|
1573
|
+
const visible = useMemo(
|
|
1574
|
+
() => trimmed ? (
|
|
1575
|
+
// Match the title as displayed, so searching "untitled" finds the
|
|
1576
|
+
// rows that render as "Untitled conversation".
|
|
1577
|
+
sorted.filter(
|
|
1578
|
+
(t) => (t.title ?? "Untitled conversation").toLowerCase().includes(trimmed)
|
|
1579
|
+
)
|
|
1580
|
+
) : sorted,
|
|
1581
|
+
[sorted, trimmed]
|
|
1582
|
+
);
|
|
1583
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col", children: [
|
|
1584
|
+
/* @__PURE__ */ jsx2("div", { className: "border-border border-b p-2", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
1585
|
+
/* @__PURE__ */ jsx2(Search, { className: "-translate-y-1/2 pointer-events-none absolute top-1/2 left-2.5 h-3.5 w-3.5 text-muted-foreground" }),
|
|
1586
|
+
/* @__PURE__ */ jsx2(
|
|
1587
|
+
"input",
|
|
1588
|
+
{
|
|
1589
|
+
type: "search",
|
|
1590
|
+
value: query,
|
|
1591
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1592
|
+
placeholder: "Search conversations",
|
|
1593
|
+
"aria-label": "Search conversations",
|
|
1594
|
+
className: "w-full rounded-md border border-border bg-surface-container-high py-1.5 pr-2 pl-8 text-foreground text-sm placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
1595
|
+
}
|
|
1596
|
+
)
|
|
1597
|
+
] }) }),
|
|
1598
|
+
/* @__PURE__ */ jsx2("div", { className: "min-h-0 flex-1 overflow-y-auto", children: visible.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "px-3 py-6 text-center text-muted-foreground text-xs", children: !loaded ? "Loading\u2026" : trimmed ? "No conversations match your search." : "No past conversations yet." }) : /* @__PURE__ */ jsx2("ul", { className: "py-1", children: visible.map((t) => {
|
|
1599
|
+
const active = t.id === activeThreadId;
|
|
1600
|
+
const ms = parsedTime(t.updatedAt);
|
|
1601
|
+
const busyActive = active && activeBusy;
|
|
1602
|
+
const title = t.title ?? "Untitled conversation";
|
|
1603
|
+
return /* @__PURE__ */ jsxs(
|
|
1604
|
+
"li",
|
|
1605
|
+
{
|
|
1606
|
+
className: `group flex items-center transition-colors hover:bg-muted/60 ${active ? "bg-primary/10" : ""}`,
|
|
1607
|
+
children: [
|
|
1608
|
+
/* @__PURE__ */ jsxs(
|
|
1609
|
+
"button",
|
|
1610
|
+
{
|
|
1611
|
+
type: "button",
|
|
1612
|
+
onClick: () => onSelect(t.id),
|
|
1613
|
+
className: "flex min-w-0 flex-1 flex-col gap-0.5 px-3 py-2 text-left",
|
|
1614
|
+
children: [
|
|
1615
|
+
/* @__PURE__ */ jsx2(
|
|
1616
|
+
"span",
|
|
1617
|
+
{
|
|
1618
|
+
className: `truncate text-sm ${active ? "font-medium text-foreground" : "text-foreground"}`,
|
|
1619
|
+
children: title
|
|
1620
|
+
}
|
|
1621
|
+
),
|
|
1622
|
+
ms != null && /* @__PURE__ */ jsx2("span", { className: "text-[11px] text-muted-foreground", children: timeAgo(ms) })
|
|
1623
|
+
]
|
|
1624
|
+
}
|
|
1625
|
+
),
|
|
1626
|
+
canRemove && /* @__PURE__ */ jsx2(
|
|
1627
|
+
"button",
|
|
1628
|
+
{
|
|
1629
|
+
type: "button",
|
|
1630
|
+
onClick: () => onDelete(t.id),
|
|
1631
|
+
disabled: busyActive,
|
|
1632
|
+
"aria-label": `Delete conversation: ${title}`,
|
|
1633
|
+
title: busyActive ? "Can't delete while this conversation is active" : "Delete conversation",
|
|
1634
|
+
className: "shrink-0 p-2 text-muted-foreground opacity-0 transition [@media(hover:none)]:opacity-100 hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30",
|
|
1635
|
+
children: /* @__PURE__ */ jsx2(Trash2, { className: "h-3.5 w-3.5" })
|
|
1636
|
+
}
|
|
1637
|
+
)
|
|
1638
|
+
]
|
|
1639
|
+
},
|
|
1640
|
+
t.id
|
|
1641
|
+
);
|
|
1642
|
+
}) }) })
|
|
1643
|
+
] });
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/assistant/ProposalCard.tsx
|
|
1647
|
+
import { useState as useState4 } from "react";
|
|
1648
|
+
|
|
1649
|
+
// src/assistant/provider-label.ts
|
|
1650
|
+
var PROVIDER_LABELS = {
|
|
1651
|
+
github: "GitHub",
|
|
1652
|
+
gitlab: "GitLab",
|
|
1653
|
+
slack: "Slack",
|
|
1654
|
+
stripe: "Stripe",
|
|
1655
|
+
notion: "Notion",
|
|
1656
|
+
linear: "Linear",
|
|
1657
|
+
discord: "Discord"
|
|
1658
|
+
};
|
|
1659
|
+
function providerLabel(provider) {
|
|
1660
|
+
const key = provider.toLowerCase();
|
|
1661
|
+
return PROVIDER_LABELS[key] ?? provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/assistant/ProposalCard.tsx
|
|
1665
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1666
|
+
function ProposalCard({
|
|
1667
|
+
proposal,
|
|
1668
|
+
confirming,
|
|
1669
|
+
onConfirm,
|
|
1670
|
+
onCancel,
|
|
1671
|
+
navigate,
|
|
1672
|
+
renderGraph
|
|
1673
|
+
}) {
|
|
1674
|
+
const view = describeProposal(proposal);
|
|
1675
|
+
const [tab, setTab] = useState4("graph");
|
|
1676
|
+
const isWorkflow = view.preview?.kind === "workflow";
|
|
1677
|
+
const showGraph = isWorkflow && !!renderGraph;
|
|
1678
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rounded-lg border border-primary/40 bg-card p-3 text-sm", children: [
|
|
1679
|
+
/* @__PURE__ */ jsx3("p", { className: "font-medium text-foreground", children: view.title }),
|
|
1680
|
+
/* @__PURE__ */ jsx3("p", { className: "text-muted-foreground text-xs", children: "Confirm to run this action on your account." }),
|
|
1681
|
+
view.fields.length > 0 && /* @__PURE__ */ jsx3("dl", { className: "mt-2 space-y-1", children: view.fields.map((f) => /* @__PURE__ */ jsxs2("div", { className: "flex gap-2 text-xs", children: [
|
|
1682
|
+
/* @__PURE__ */ jsx3("dt", { className: "shrink-0 text-muted-foreground", children: f.label }),
|
|
1683
|
+
/* @__PURE__ */ jsx3("dd", { className: "truncate text-foreground", title: f.value, children: f.value })
|
|
1684
|
+
] }, f.label)) }),
|
|
1685
|
+
view.skills && view.skills.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "mt-2", children: [
|
|
1686
|
+
/* @__PURE__ */ jsx3("p", { className: "text-muted-foreground text-xs", children: "New skills" }),
|
|
1687
|
+
/* @__PURE__ */ jsx3("ul", { className: "mt-1 space-y-0.5", children: view.skills.map((s) => /* @__PURE__ */ jsxs2("li", { className: "text-foreground text-xs", children: [
|
|
1688
|
+
/* @__PURE__ */ jsx3("span", { className: "font-medium", children: s.name }),
|
|
1689
|
+
s.description ? /* @__PURE__ */ jsxs2("span", { className: "text-muted-foreground", children: [
|
|
1690
|
+
" \u2014 ",
|
|
1691
|
+
s.description
|
|
1692
|
+
] }) : null
|
|
1693
|
+
] }, s.name)) })
|
|
1694
|
+
] }),
|
|
1695
|
+
view.preview && /* @__PURE__ */ jsxs2("div", { className: "mt-2", children: [
|
|
1696
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
|
|
1697
|
+
/* @__PURE__ */ jsx3("p", { className: "text-muted-foreground text-xs", children: view.preview.label }),
|
|
1698
|
+
showGraph && /* @__PURE__ */ jsxs2("div", { className: "flex gap-2 text-xs", children: [
|
|
1699
|
+
/* @__PURE__ */ jsx3(
|
|
1700
|
+
"button",
|
|
1701
|
+
{
|
|
1702
|
+
type: "button",
|
|
1703
|
+
onClick: () => setTab("graph"),
|
|
1704
|
+
className: tab === "graph" ? "text-foreground" : "text-muted-foreground",
|
|
1705
|
+
children: "Graph"
|
|
1706
|
+
}
|
|
1707
|
+
),
|
|
1708
|
+
/* @__PURE__ */ jsx3(
|
|
1709
|
+
"button",
|
|
1710
|
+
{
|
|
1711
|
+
type: "button",
|
|
1712
|
+
onClick: () => setTab("yaml"),
|
|
1713
|
+
className: tab === "yaml" ? "text-foreground" : "text-muted-foreground",
|
|
1714
|
+
children: "YAML"
|
|
1715
|
+
}
|
|
1716
|
+
)
|
|
1717
|
+
] })
|
|
1718
|
+
] }),
|
|
1719
|
+
showGraph && tab === "graph" ? /* @__PURE__ */ jsx3("div", { className: "mt-1 h-64 overflow-hidden rounded border border-border", children: renderGraph?.(view.preview.content) }) : /* @__PURE__ */ jsx3("pre", { className: "mt-1 max-h-48 overflow-auto rounded border border-border bg-muted/50 p-2 text-xs", children: /* @__PURE__ */ jsx3("code", { children: view.preview.content }) })
|
|
1720
|
+
] }),
|
|
1721
|
+
proposal.requirements && proposal.requirements.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "mt-3 rounded border border-border p-2", children: [
|
|
1722
|
+
/* @__PURE__ */ jsx3("p", { className: "text-muted-foreground text-xs", children: "Integrations" }),
|
|
1723
|
+
/* @__PURE__ */ jsx3("ul", { className: "mt-1 space-y-1", children: proposal.requirements.map((r) => /* @__PURE__ */ jsx3(
|
|
1724
|
+
RequirementRow,
|
|
1725
|
+
{
|
|
1726
|
+
req: r,
|
|
1727
|
+
navigate
|
|
1728
|
+
},
|
|
1729
|
+
`${r.provider}-${r.kind ?? "integration"}`
|
|
1730
|
+
)) }),
|
|
1731
|
+
/* @__PURE__ */ jsx3("p", { className: "mt-1 text-muted-foreground text-xs", children: "Connect the items above, then confirm \u2014 your proposal stays here until you do." })
|
|
1732
|
+
] }),
|
|
1733
|
+
proposal.retryError && /* @__PURE__ */ jsx3("p", { role: "alert", className: "mt-2 text-destructive text-xs", children: proposal.retryError }),
|
|
1734
|
+
/* @__PURE__ */ jsxs2("div", { className: "mt-3 flex gap-2", children: [
|
|
1735
|
+
/* @__PURE__ */ jsx3(
|
|
1736
|
+
"button",
|
|
1737
|
+
{
|
|
1738
|
+
type: "button",
|
|
1739
|
+
onClick: onConfirm,
|
|
1740
|
+
disabled: confirming || !proposal.proposalId,
|
|
1741
|
+
className: "rounded bg-primary px-3 py-1.5 text-primary-foreground text-sm disabled:opacity-50",
|
|
1742
|
+
children: confirming ? "Confirming\u2026" : "Confirm"
|
|
1743
|
+
}
|
|
1744
|
+
),
|
|
1745
|
+
/* @__PURE__ */ jsx3(
|
|
1746
|
+
"button",
|
|
1747
|
+
{
|
|
1748
|
+
type: "button",
|
|
1749
|
+
onClick: onCancel,
|
|
1750
|
+
disabled: confirming,
|
|
1751
|
+
className: "rounded border border-border px-3 py-1.5 text-foreground text-sm disabled:opacity-50",
|
|
1752
|
+
children: "Cancel"
|
|
1753
|
+
}
|
|
1754
|
+
)
|
|
1755
|
+
] })
|
|
1756
|
+
] });
|
|
1757
|
+
}
|
|
1758
|
+
function openConnect(target, navigate) {
|
|
1759
|
+
if (target.startsWith("//")) return;
|
|
1760
|
+
let url;
|
|
1761
|
+
try {
|
|
1762
|
+
url = new URL(target, window.location.origin);
|
|
1763
|
+
} catch {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return;
|
|
1767
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {
|
|
1768
|
+
window.open(url.href, "_blank", "noopener,noreferrer");
|
|
1769
|
+
} else if (navigate) {
|
|
1770
|
+
navigate(target);
|
|
1771
|
+
} else {
|
|
1772
|
+
window.location.assign(url.href);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function RequirementRow({
|
|
1776
|
+
req,
|
|
1777
|
+
navigate
|
|
1778
|
+
}) {
|
|
1779
|
+
const label = providerLabel(req.provider);
|
|
1780
|
+
const isApp = req.kind === "github_app";
|
|
1781
|
+
const kindLabel = isApp ? `${label} App` : label;
|
|
1782
|
+
const statusText = req.connected ? isApp ? "installed" : "connected" : isApp ? "not installed" : "not connected";
|
|
1783
|
+
const canConnect = !req.connected && req.connectUrl !== null;
|
|
1784
|
+
const target = req.connectUrl ?? "/app/integrations";
|
|
1785
|
+
return /* @__PURE__ */ jsxs2("li", { className: "flex items-center justify-between gap-2 text-xs", children: [
|
|
1786
|
+
/* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 items-center gap-2", children: [
|
|
1787
|
+
/* @__PURE__ */ jsx3(ProviderLogo, { provider: req.provider, size: 16 }),
|
|
1788
|
+
/* @__PURE__ */ jsx3("span", { className: "truncate text-foreground", children: kindLabel }),
|
|
1789
|
+
/* @__PURE__ */ jsxs2("span", { className: "flex shrink-0 items-center gap-1", children: [
|
|
1790
|
+
/* @__PURE__ */ jsx3(
|
|
1791
|
+
"span",
|
|
1792
|
+
{
|
|
1793
|
+
"aria-hidden": "true",
|
|
1794
|
+
className: `h-1.5 w-1.5 rounded-full ${req.connected ? "bg-primary" : "border border-muted-foreground"}`
|
|
1795
|
+
}
|
|
1796
|
+
),
|
|
1797
|
+
/* @__PURE__ */ jsx3(
|
|
1798
|
+
"span",
|
|
1799
|
+
{
|
|
1800
|
+
className: req.connected ? "text-primary" : "text-muted-foreground",
|
|
1801
|
+
children: statusText
|
|
1802
|
+
}
|
|
1803
|
+
)
|
|
1804
|
+
] })
|
|
1805
|
+
] }),
|
|
1806
|
+
canConnect && /* @__PURE__ */ jsxs2(
|
|
1807
|
+
"button",
|
|
1808
|
+
{
|
|
1809
|
+
type: "button",
|
|
1810
|
+
onClick: () => openConnect(target, navigate),
|
|
1811
|
+
className: "shrink-0 text-primary",
|
|
1812
|
+
children: [
|
|
1813
|
+
isApp ? "Install" : "Connect",
|
|
1814
|
+
" \u2192"
|
|
1815
|
+
]
|
|
1816
|
+
}
|
|
1817
|
+
)
|
|
1818
|
+
] });
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/assistant/transcript.tsx
|
|
1822
|
+
import { useCallback as useCallback3, useMemo as useMemo2 } from "react";
|
|
1823
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1824
|
+
function assistantIsThinking(state) {
|
|
1825
|
+
if (state.status !== "streaming") return false;
|
|
1826
|
+
const streaming = state.streamingId ? state.messages.find((m) => m.id === state.streamingId) : void 0;
|
|
1827
|
+
return !streaming || streaming.text === "";
|
|
1828
|
+
}
|
|
1829
|
+
var TOOL_STATUS = {
|
|
1830
|
+
running: "running",
|
|
1831
|
+
ok: "done",
|
|
1832
|
+
failed: "error"
|
|
1833
|
+
};
|
|
1834
|
+
function adaptToolResult(outcome) {
|
|
1835
|
+
if (outcome.ok) return { ok: true, result: outcome.result };
|
|
1836
|
+
return { ok: false, message: outcome.error?.message, code: outcome.error?.code };
|
|
1837
|
+
}
|
|
1838
|
+
function adaptTranscript(view) {
|
|
1839
|
+
const messages = [];
|
|
1840
|
+
let turn = null;
|
|
1841
|
+
let currentTurnAssistant = null;
|
|
1842
|
+
const openTurn = (id) => {
|
|
1843
|
+
const message = { id, role: "assistant", content: "", segments: [] };
|
|
1844
|
+
messages.push(message);
|
|
1845
|
+
turn = message;
|
|
1846
|
+
currentTurnAssistant = message;
|
|
1847
|
+
return message;
|
|
1848
|
+
};
|
|
1849
|
+
const appendText = (message, text) => {
|
|
1850
|
+
if (!text.trim()) return;
|
|
1851
|
+
message.segments.push({ kind: "text", content: text });
|
|
1852
|
+
message.content = message.content ? `${message.content}
|
|
1853
|
+
|
|
1854
|
+
${text}` : text;
|
|
1855
|
+
};
|
|
1856
|
+
for (const msg of view.messages) {
|
|
1857
|
+
if (msg.role === "user") {
|
|
1858
|
+
messages.push({ id: msg.id, role: "user", content: msg.text });
|
|
1859
|
+
turn = null;
|
|
1860
|
+
currentTurnAssistant = null;
|
|
1861
|
+
} else if (msg.role === "assistant") {
|
|
1862
|
+
const active = turn ?? openTurn(msg.id);
|
|
1863
|
+
appendText(active, msg.text);
|
|
1864
|
+
currentTurnAssistant = active;
|
|
1865
|
+
} else if (msg.role === "tool") {
|
|
1866
|
+
if (!msg.tool) continue;
|
|
1867
|
+
const active = turn ?? openTurn(`turn-${msg.id}`);
|
|
1868
|
+
currentTurnAssistant = active;
|
|
1869
|
+
active.segments.push({
|
|
1870
|
+
kind: "tool",
|
|
1871
|
+
call: {
|
|
1872
|
+
id: msg.id,
|
|
1873
|
+
name: msg.tool.name,
|
|
1874
|
+
// An unmapped status resolves to "error", not "running": a stuck
|
|
1875
|
+
// spinner would hide a finished or failed tool.
|
|
1876
|
+
status: TOOL_STATUS[msg.tool.status] ?? "error",
|
|
1877
|
+
...msg.tool.args ? { args: msg.tool.args } : {},
|
|
1878
|
+
...msg.tool.outcome ? { result: adaptToolResult(msg.tool.outcome) } : {}
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
} else {
|
|
1882
|
+
messages.push({ id: msg.id, role: "system", content: msg.text });
|
|
1883
|
+
turn = null;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
let proposalHostId = null;
|
|
1887
|
+
if (view.pendingProposals.length > 0) {
|
|
1888
|
+
if (!currentTurnAssistant) {
|
|
1889
|
+
currentTurnAssistant = openTurn(
|
|
1890
|
+
`proposal-host-${view.pendingProposals[0].callId}`
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
proposalHostId = currentTurnAssistant.id;
|
|
1894
|
+
}
|
|
1895
|
+
if (currentTurnAssistant) {
|
|
1896
|
+
if (view.reasoning) currentTurnAssistant.reasoning = view.reasoning;
|
|
1897
|
+
if (view.model) currentTurnAssistant.modelUsed = view.model;
|
|
1898
|
+
if (view.usage) {
|
|
1899
|
+
if (view.usage.completionTokens != null)
|
|
1900
|
+
currentTurnAssistant.completionTokens = view.usage.completionTokens;
|
|
1901
|
+
if (view.usage.promptTokens != null)
|
|
1902
|
+
currentTurnAssistant.promptTokens = view.usage.promptTokens;
|
|
1903
|
+
if (view.usage.durationMs != null)
|
|
1904
|
+
currentTurnAssistant.durationMs = view.usage.durationMs;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const isEmptyShell = (m) => m.role === "assistant" && m.content === "" && (m.segments?.length ?? 0) === 0 && m.reasoning == null && m.modelUsed == null && m.completionTokens == null && m.promptTokens == null && m.durationMs == null && m.id !== proposalHostId;
|
|
1908
|
+
return {
|
|
1909
|
+
messages: messages.filter((m) => !isEmptyShell(m)),
|
|
1910
|
+
proposalHostId,
|
|
1911
|
+
metricsHostId: currentTurnAssistant && !isEmptyShell(currentTurnAssistant) ? currentTurnAssistant.id : null
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
function formatTurnCost(costUsd) {
|
|
1915
|
+
return costUsd < 0.01 ? `$${costUsd.toFixed(4)}` : `$${costUsd.toFixed(2)}`;
|
|
1916
|
+
}
|
|
1917
|
+
function ProposalSlot({
|
|
1918
|
+
proposal,
|
|
1919
|
+
render
|
|
1920
|
+
}) {
|
|
1921
|
+
return /* @__PURE__ */ jsx4(Fragment, { children: render(proposal) });
|
|
1922
|
+
}
|
|
1923
|
+
function AssistantTranscript({
|
|
1924
|
+
view,
|
|
1925
|
+
renderMarkdown,
|
|
1926
|
+
toolRenderers,
|
|
1927
|
+
emptyState
|
|
1928
|
+
}) {
|
|
1929
|
+
const { messages, proposalHostId, metricsHostId } = useMemo2(
|
|
1930
|
+
() => adaptTranscript(view),
|
|
1931
|
+
[view]
|
|
1932
|
+
);
|
|
1933
|
+
const markdown = useCallback3(
|
|
1934
|
+
(content) => renderMarkdown ? renderMarkdown(content) : content,
|
|
1935
|
+
[renderMarkdown]
|
|
1936
|
+
);
|
|
1937
|
+
if (messages.length === 0 && !view.isStreaming) {
|
|
1938
|
+
return /* @__PURE__ */ jsx4(Fragment, { children: emptyState });
|
|
1939
|
+
}
|
|
1940
|
+
return /* @__PURE__ */ jsx4(
|
|
1941
|
+
ChatMessages,
|
|
1942
|
+
{
|
|
1943
|
+
messages,
|
|
1944
|
+
loading: view.isStreaming,
|
|
1945
|
+
agentLabel: "Assistant",
|
|
1946
|
+
renderMarkdown: markdown,
|
|
1947
|
+
toolRenderers,
|
|
1948
|
+
renderEmpty: () => /* @__PURE__ */ jsx4(Fragment, { children: emptyState }),
|
|
1949
|
+
renderExtras: (message) => {
|
|
1950
|
+
const proposals = message.id === proposalHostId && view.pendingProposals.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-3 flex flex-col gap-3", children: view.pendingProposals.map((proposal) => /* @__PURE__ */ jsx4(
|
|
1951
|
+
ProposalSlot,
|
|
1952
|
+
{
|
|
1953
|
+
proposal,
|
|
1954
|
+
render: view.renderProposal
|
|
1955
|
+
},
|
|
1956
|
+
proposal.callId
|
|
1957
|
+
)) }) : null;
|
|
1958
|
+
const cost = message.id === metricsHostId && !view.isStreaming && view.usage?.costUsd != null && !view.usage.replayed ? /* @__PURE__ */ jsxs3("p", { className: "mt-1 text-[11px] text-muted-foreground", children: [
|
|
1959
|
+
formatTurnCost(view.usage.costUsd),
|
|
1960
|
+
" this turn"
|
|
1961
|
+
] }) : null;
|
|
1962
|
+
if (!proposals && !cost) return null;
|
|
1963
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
1964
|
+
proposals,
|
|
1965
|
+
cost
|
|
1966
|
+
] });
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/assistant/usePanelPrefs.ts
|
|
1973
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
|
|
1974
|
+
var MIN_PANEL_WIDTH = 360;
|
|
1975
|
+
var DEFAULT_PANEL_WIDTH = 448;
|
|
1976
|
+
var MAX_PANEL_WIDTH_FRACTION = 0.95;
|
|
1977
|
+
var MIN_FONT_SCALE = 0.875;
|
|
1978
|
+
var MAX_FONT_SCALE = 1.5;
|
|
1979
|
+
var DEFAULT_FONT_SCALE = 1;
|
|
1980
|
+
var FONT_SCALE_STEP = 0.125;
|
|
1981
|
+
var WIDTH_KEY = "assistant.panel.width";
|
|
1982
|
+
var FONT_SCALE_KEY = "assistant.panel.fontScale";
|
|
1983
|
+
function readNumber(key) {
|
|
1984
|
+
try {
|
|
1985
|
+
const raw = window.localStorage.getItem(key);
|
|
1986
|
+
if (raw == null || raw.trim() === "") return null;
|
|
1987
|
+
const n = Number(raw);
|
|
1988
|
+
return Number.isFinite(n) ? n : null;
|
|
1989
|
+
} catch {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function writeNumber(key, value) {
|
|
1994
|
+
try {
|
|
1995
|
+
window.localStorage.setItem(key, String(value));
|
|
1996
|
+
} catch {
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
function maxPanelWidth() {
|
|
2000
|
+
if (typeof window === "undefined") return 9999;
|
|
2001
|
+
return Math.round(window.innerWidth * MAX_PANEL_WIDTH_FRACTION);
|
|
2002
|
+
}
|
|
2003
|
+
function clampWidth(value) {
|
|
2004
|
+
return Math.min(
|
|
2005
|
+
Math.max(Math.round(value), MIN_PANEL_WIDTH),
|
|
2006
|
+
maxPanelWidth()
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
function clampScale(value) {
|
|
2010
|
+
const stepped = Math.round(value / FONT_SCALE_STEP) * FONT_SCALE_STEP;
|
|
2011
|
+
return Math.min(Math.max(stepped, MIN_FONT_SCALE), MAX_FONT_SCALE);
|
|
2012
|
+
}
|
|
2013
|
+
function usePanelWidth() {
|
|
2014
|
+
const [width, setWidthState] = useState5(DEFAULT_PANEL_WIDTH);
|
|
2015
|
+
const [maxWidth, setMaxWidth] = useState5(() => maxPanelWidth());
|
|
2016
|
+
const desiredRef = useRef3(DEFAULT_PANEL_WIDTH);
|
|
2017
|
+
useEffect4(() => {
|
|
2018
|
+
setMaxWidth(maxPanelWidth());
|
|
2019
|
+
const stored = readNumber(WIDTH_KEY);
|
|
2020
|
+
if (stored != null) {
|
|
2021
|
+
desiredRef.current = Math.max(Math.round(stored), MIN_PANEL_WIDTH);
|
|
2022
|
+
setWidthState(clampWidth(stored));
|
|
2023
|
+
}
|
|
2024
|
+
}, []);
|
|
2025
|
+
useEffect4(() => {
|
|
2026
|
+
const onResize = () => {
|
|
2027
|
+
setMaxWidth(maxPanelWidth());
|
|
2028
|
+
setWidthState(clampWidth(desiredRef.current));
|
|
2029
|
+
};
|
|
2030
|
+
window.addEventListener("resize", onResize);
|
|
2031
|
+
return () => window.removeEventListener("resize", onResize);
|
|
2032
|
+
}, []);
|
|
2033
|
+
const setWidth = useCallback4((next) => {
|
|
2034
|
+
const clamped = clampWidth(next);
|
|
2035
|
+
desiredRef.current = clamped;
|
|
2036
|
+
writeNumber(WIDTH_KEY, clamped);
|
|
2037
|
+
setWidthState(clamped);
|
|
2038
|
+
}, []);
|
|
2039
|
+
const previewWidth = useCallback4((next) => {
|
|
2040
|
+
setWidthState(clampWidth(next));
|
|
2041
|
+
}, []);
|
|
2042
|
+
const nudgeWidth = useCallback4((deltaPx) => {
|
|
2043
|
+
const clamped = clampWidth(desiredRef.current + deltaPx);
|
|
2044
|
+
desiredRef.current = clamped;
|
|
2045
|
+
writeNumber(WIDTH_KEY, clamped);
|
|
2046
|
+
setWidthState(clamped);
|
|
2047
|
+
}, []);
|
|
2048
|
+
return { width, maxWidth, setWidth, previewWidth, nudgeWidth };
|
|
2049
|
+
}
|
|
2050
|
+
function useFontScale() {
|
|
2051
|
+
const [scale, setScaleState] = useState5(DEFAULT_FONT_SCALE);
|
|
2052
|
+
useEffect4(() => {
|
|
2053
|
+
const stored = readNumber(FONT_SCALE_KEY);
|
|
2054
|
+
if (stored != null) setScaleState(clampScale(stored));
|
|
2055
|
+
}, []);
|
|
2056
|
+
const step = useCallback4((delta) => {
|
|
2057
|
+
setScaleState((prev) => {
|
|
2058
|
+
const clamped = clampScale(prev + delta);
|
|
2059
|
+
writeNumber(FONT_SCALE_KEY, clamped);
|
|
2060
|
+
return clamped;
|
|
2061
|
+
});
|
|
2062
|
+
}, []);
|
|
2063
|
+
return {
|
|
2064
|
+
scale,
|
|
2065
|
+
increase: () => step(FONT_SCALE_STEP),
|
|
2066
|
+
decrease: () => step(-FONT_SCALE_STEP),
|
|
2067
|
+
canIncrease: scale < MAX_FONT_SCALE - 1e-9,
|
|
2068
|
+
canDecrease: scale > MIN_FONT_SCALE + 1e-9
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
function useIsDesktop() {
|
|
2072
|
+
const [isDesktop, setIsDesktop] = useState5(
|
|
2073
|
+
() => typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(min-width: 768px)").matches : true
|
|
2074
|
+
);
|
|
2075
|
+
useEffect4(() => {
|
|
2076
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
const mq = window.matchMedia("(min-width: 768px)");
|
|
2080
|
+
const update = () => setIsDesktop(mq.matches);
|
|
2081
|
+
update();
|
|
2082
|
+
mq.addEventListener("change", update);
|
|
2083
|
+
return () => mq.removeEventListener("change", update);
|
|
2084
|
+
}, []);
|
|
2085
|
+
return isDesktop;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/assistant/AssistantPanel.tsx
|
|
2089
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2090
|
+
var EMPTY_STATE = "Ask me to create a workflow, check your usage, or manage your API keys.";
|
|
2091
|
+
function defaultFormatMoney(usd) {
|
|
2092
|
+
if (usd == null) return "\u2014";
|
|
2093
|
+
return new Intl.NumberFormat("en-US", {
|
|
2094
|
+
style: "currency",
|
|
2095
|
+
currency: "USD"
|
|
2096
|
+
}).format(usd);
|
|
2097
|
+
}
|
|
2098
|
+
function providerOf(slug) {
|
|
2099
|
+
const i = slug.indexOf("/");
|
|
2100
|
+
return i > 0 ? slug.slice(0, i) : "other";
|
|
2101
|
+
}
|
|
2102
|
+
function toPickerModels(models, selected) {
|
|
2103
|
+
const row = (slug, label, contextTokens) => ({
|
|
2104
|
+
id: slug,
|
|
2105
|
+
name: label ?? slug,
|
|
2106
|
+
provider: providerOf(slug),
|
|
2107
|
+
supportsTools: true,
|
|
2108
|
+
supportsReasoning: false,
|
|
2109
|
+
featured: false,
|
|
2110
|
+
...contextTokens != null ? { contextLength: contextTokens } : {}
|
|
2111
|
+
});
|
|
2112
|
+
const mapped = models.models.map(
|
|
2113
|
+
(m) => row(m.slug, m.label, m.contextTokens)
|
|
2114
|
+
);
|
|
2115
|
+
for (const slug of [models.default, selected]) {
|
|
2116
|
+
if (slug && !mapped.some((m) => m.id === slug)) mapped.push(row(slug));
|
|
2117
|
+
}
|
|
2118
|
+
return mapped;
|
|
2119
|
+
}
|
|
2120
|
+
function WorkingIndicator() {
|
|
2121
|
+
return /* @__PURE__ */ jsxs4("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground text-xs", children: [
|
|
2122
|
+
/* @__PURE__ */ jsx5("span", { className: "h-1.5 w-1.5 animate-pulse rounded-full bg-primary" }),
|
|
2123
|
+
"Working\u2026"
|
|
2124
|
+
] });
|
|
2125
|
+
}
|
|
2126
|
+
function nextModelSelection(id, defaultSlug) {
|
|
2127
|
+
if (defaultSlug != null && id === defaultSlug) return null;
|
|
2128
|
+
return id || null;
|
|
2129
|
+
}
|
|
2130
|
+
function AssistantPanel({
|
|
2131
|
+
chat,
|
|
2132
|
+
userId,
|
|
2133
|
+
onClose,
|
|
2134
|
+
navigate,
|
|
2135
|
+
balanceUsd = null,
|
|
2136
|
+
formatMoney = defaultFormatMoney,
|
|
2137
|
+
renderGraph,
|
|
2138
|
+
renderMarkdown,
|
|
2139
|
+
toolRenderers,
|
|
2140
|
+
renderTranscript
|
|
2141
|
+
}) {
|
|
2142
|
+
const models = useAssistantModels();
|
|
2143
|
+
const threads = useAssistantThreads(userId);
|
|
2144
|
+
const font = useFontScale();
|
|
2145
|
+
const [view, setView] = useState6("chat");
|
|
2146
|
+
const historyButtonRef = useRef4(null);
|
|
2147
|
+
const logRef = useRef4(null);
|
|
2148
|
+
const pickerModels = useMemo3(
|
|
2149
|
+
() => toPickerModels(models, chat.selectedModel),
|
|
2150
|
+
[models, chat.selectedModel]
|
|
2151
|
+
);
|
|
2152
|
+
const pickerValue = chat.selectedModel ?? models.default ?? "";
|
|
2153
|
+
const { state } = chat;
|
|
2154
|
+
const chatRef = useRef4(chat);
|
|
2155
|
+
chatRef.current = chat;
|
|
2156
|
+
useEffect5(() => {
|
|
2157
|
+
if (view !== "history") return;
|
|
2158
|
+
const search = logRef.current?.querySelector(
|
|
2159
|
+
'input[type="search"]'
|
|
2160
|
+
);
|
|
2161
|
+
(search ?? logRef.current)?.focus();
|
|
2162
|
+
}, [view]);
|
|
2163
|
+
useEffect5(() => {
|
|
2164
|
+
if (view !== "history") return;
|
|
2165
|
+
const onKeyDownCapture = (e) => {
|
|
2166
|
+
if (e.key !== "Escape") return;
|
|
2167
|
+
const target = e.target;
|
|
2168
|
+
if (!logRef.current?.contains(target) && !historyButtonRef.current?.contains(target)) {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
e.stopImmediatePropagation();
|
|
2172
|
+
setView("chat");
|
|
2173
|
+
historyButtonRef.current?.focus();
|
|
2174
|
+
};
|
|
2175
|
+
document.addEventListener("keydown", onKeyDownCapture, true);
|
|
2176
|
+
return () => document.removeEventListener("keydown", onKeyDownCapture, true);
|
|
2177
|
+
}, [view]);
|
|
2178
|
+
const effectiveBalance = state.usage?.balanceUsd ?? balanceUsd;
|
|
2179
|
+
const errorView = state.error ? presentError(state.error.code, state.error.message) : null;
|
|
2180
|
+
const low = isLowBalance(effectiveBalance) && !errorView;
|
|
2181
|
+
const streaming = state.status === "streaming";
|
|
2182
|
+
const firstUserText = state.messages.find((m) => m.role === "user")?.text.trim();
|
|
2183
|
+
const titleChars = firstUserText ? Array.from(firstUserText) : [];
|
|
2184
|
+
const conversationTitle = firstUserText ? titleChars.length > 60 ? `${titleChars.slice(0, 60).join("")}\u2026` : firstUserText : null;
|
|
2185
|
+
const renderProposal = (proposal) => /* @__PURE__ */ jsx5(
|
|
2186
|
+
ProposalCard,
|
|
2187
|
+
{
|
|
2188
|
+
proposal,
|
|
2189
|
+
confirming: proposal.proposalId ? chat.confirmingIds.has(proposal.proposalId) : false,
|
|
2190
|
+
onConfirm: () => chat.confirm(proposal),
|
|
2191
|
+
onCancel: () => chat.cancel(proposal),
|
|
2192
|
+
navigate,
|
|
2193
|
+
renderGraph
|
|
2194
|
+
}
|
|
2195
|
+
);
|
|
2196
|
+
const isThinking = assistantIsThinking(state);
|
|
2197
|
+
const transcriptView = {
|
|
2198
|
+
messages: state.messages,
|
|
2199
|
+
reasoning: state.reasoning,
|
|
2200
|
+
streamingId: state.streamingId,
|
|
2201
|
+
model: state.model,
|
|
2202
|
+
isStreaming: streaming,
|
|
2203
|
+
isThinking,
|
|
2204
|
+
pendingProposals: state.pendingProposals,
|
|
2205
|
+
usage: state.usage,
|
|
2206
|
+
renderProposal
|
|
2207
|
+
};
|
|
2208
|
+
const showHistory = () => {
|
|
2209
|
+
threads.refresh();
|
|
2210
|
+
setView("history");
|
|
2211
|
+
};
|
|
2212
|
+
const toggleHistory = () => {
|
|
2213
|
+
if (view === "history") setView("chat");
|
|
2214
|
+
else showHistory();
|
|
2215
|
+
};
|
|
2216
|
+
const deleteThread = async (threadId) => {
|
|
2217
|
+
const pre = chatRef.current.state;
|
|
2218
|
+
if (pre.threadId === threadId && pre.status !== "idle") return;
|
|
2219
|
+
if (!window.confirm("Delete this conversation? This can't be undone.")) {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const res = await threads.remove(threadId);
|
|
2223
|
+
const live = chatRef.current.state;
|
|
2224
|
+
if (res.ok && live.threadId === threadId && live.status === "idle") {
|
|
2225
|
+
chatRef.current.reset();
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
return /* @__PURE__ */ jsxs4("div", { className: "relative flex h-full flex-col bg-background", children: [
|
|
2229
|
+
/* @__PURE__ */ jsx5("div", { className: "border-border border-b", children: /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-2 px-4 pt-3 pb-2.5", children: [
|
|
2230
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-col", children: [
|
|
2231
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-baseline gap-2", children: [
|
|
2232
|
+
/* @__PURE__ */ jsx5("span", { className: "font-medium text-foreground text-sm", children: "Assistant" }),
|
|
2233
|
+
/* @__PURE__ */ jsx5(
|
|
2234
|
+
"span",
|
|
2235
|
+
{
|
|
2236
|
+
"aria-label": "Your credit balance",
|
|
2237
|
+
className: "text-muted-foreground text-xs",
|
|
2238
|
+
children: formatMoney(effectiveBalance)
|
|
2239
|
+
}
|
|
2240
|
+
)
|
|
2241
|
+
] }),
|
|
2242
|
+
conversationTitle && /* @__PURE__ */ jsx5(
|
|
2243
|
+
"span",
|
|
2244
|
+
{
|
|
2245
|
+
className: "truncate text-muted-foreground text-xs",
|
|
2246
|
+
title: conversationTitle,
|
|
2247
|
+
children: conversationTitle
|
|
2248
|
+
}
|
|
2249
|
+
)
|
|
2250
|
+
] }),
|
|
2251
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex shrink-0 items-center gap-1", children: [
|
|
2252
|
+
/* @__PURE__ */ jsxs4(
|
|
2253
|
+
"div",
|
|
2254
|
+
{
|
|
2255
|
+
className: "flex items-center overflow-hidden rounded-md border border-border",
|
|
2256
|
+
role: "group",
|
|
2257
|
+
"aria-label": "Text size",
|
|
2258
|
+
children: [
|
|
2259
|
+
/* @__PURE__ */ jsx5(
|
|
2260
|
+
"button",
|
|
2261
|
+
{
|
|
2262
|
+
type: "button",
|
|
2263
|
+
onClick: font.decrease,
|
|
2264
|
+
disabled: !font.canDecrease,
|
|
2265
|
+
"aria-label": "Decrease text size",
|
|
2266
|
+
className: "px-1.5 py-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent",
|
|
2267
|
+
children: /* @__PURE__ */ jsx5(Minus, { className: "h-3.5 w-3.5" })
|
|
2268
|
+
}
|
|
2269
|
+
),
|
|
2270
|
+
/* @__PURE__ */ jsx5(
|
|
2271
|
+
"button",
|
|
2272
|
+
{
|
|
2273
|
+
type: "button",
|
|
2274
|
+
onClick: font.increase,
|
|
2275
|
+
disabled: !font.canIncrease,
|
|
2276
|
+
"aria-label": "Increase text size",
|
|
2277
|
+
className: "border-border border-l px-1.5 py-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent",
|
|
2278
|
+
children: /* @__PURE__ */ jsx5(Plus, { className: "h-3.5 w-3.5" })
|
|
2279
|
+
}
|
|
2280
|
+
)
|
|
2281
|
+
]
|
|
2282
|
+
}
|
|
2283
|
+
),
|
|
2284
|
+
/* @__PURE__ */ jsx5(
|
|
2285
|
+
"button",
|
|
2286
|
+
{
|
|
2287
|
+
ref: historyButtonRef,
|
|
2288
|
+
type: "button",
|
|
2289
|
+
onClick: toggleHistory,
|
|
2290
|
+
"aria-label": "Chat history",
|
|
2291
|
+
"aria-pressed": view === "history",
|
|
2292
|
+
className: `rounded-md p-1.5 transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring ${view === "history" ? "bg-muted text-foreground" : "text-muted-foreground"}`,
|
|
2293
|
+
children: /* @__PURE__ */ jsx5(History, { className: "h-4 w-4" })
|
|
2294
|
+
}
|
|
2295
|
+
),
|
|
2296
|
+
/* @__PURE__ */ jsx5(
|
|
2297
|
+
"button",
|
|
2298
|
+
{
|
|
2299
|
+
type: "button",
|
|
2300
|
+
onClick: () => {
|
|
2301
|
+
chat.reset();
|
|
2302
|
+
setView("chat");
|
|
2303
|
+
},
|
|
2304
|
+
"aria-label": "New chat",
|
|
2305
|
+
title: "New chat",
|
|
2306
|
+
className: "rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2307
|
+
children: /* @__PURE__ */ jsx5(MessageSquarePlus, { className: "h-4 w-4" })
|
|
2308
|
+
}
|
|
2309
|
+
),
|
|
2310
|
+
/* @__PURE__ */ jsx5(
|
|
2311
|
+
"button",
|
|
2312
|
+
{
|
|
2313
|
+
type: "button",
|
|
2314
|
+
onClick: onClose,
|
|
2315
|
+
"aria-label": "Close assistant",
|
|
2316
|
+
className: "rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2317
|
+
children: /* @__PURE__ */ jsx5(X, { className: "h-4 w-4" })
|
|
2318
|
+
}
|
|
2319
|
+
)
|
|
2320
|
+
] })
|
|
2321
|
+
] }) }),
|
|
2322
|
+
/* @__PURE__ */ jsx5(
|
|
2323
|
+
"div",
|
|
2324
|
+
{
|
|
2325
|
+
ref: logRef,
|
|
2326
|
+
tabIndex: -1,
|
|
2327
|
+
"aria-label": "Conversation",
|
|
2328
|
+
role: view === "chat" ? "log" : void 0,
|
|
2329
|
+
"aria-live": view === "chat" ? "polite" : void 0,
|
|
2330
|
+
className: "min-h-0 flex-1 overflow-y-auto focus:outline-none",
|
|
2331
|
+
children: view === "history" ? /* @__PURE__ */ jsx5(
|
|
2332
|
+
AssistantHistory,
|
|
2333
|
+
{
|
|
2334
|
+
threads: threads.threads,
|
|
2335
|
+
loaded: threads.loaded,
|
|
2336
|
+
activeThreadId: state.threadId,
|
|
2337
|
+
activeBusy: state.status !== "idle",
|
|
2338
|
+
canRemove: threads.canRemove,
|
|
2339
|
+
onSelect: (id) => {
|
|
2340
|
+
chat.switchThread(id);
|
|
2341
|
+
setView("chat");
|
|
2342
|
+
},
|
|
2343
|
+
onDelete: (id) => void deleteThread(id)
|
|
2344
|
+
}
|
|
2345
|
+
) : (
|
|
2346
|
+
// The text-size control zooms the transcript only — not the history
|
|
2347
|
+
// view's search box and buttons. `zoom` scales every descendant
|
|
2348
|
+
// uniformly regardless of which renderer draws the conversation; an
|
|
2349
|
+
// inline `font-size` would not (the transcript's text utilities set
|
|
2350
|
+
// absolute rem sizes), and `transform: scale` would break the scroll
|
|
2351
|
+
// container by keeping the original layout box.
|
|
2352
|
+
/* @__PURE__ */ jsx5("div", { className: "px-2 py-3", style: { zoom: font.scale }, children: renderTranscript ? renderTranscript(transcriptView) : /* @__PURE__ */ jsx5(
|
|
2353
|
+
AssistantTranscript,
|
|
2354
|
+
{
|
|
2355
|
+
view: transcriptView,
|
|
2356
|
+
renderMarkdown,
|
|
2357
|
+
toolRenderers,
|
|
2358
|
+
emptyState: /* @__PURE__ */ jsx5("p", { className: "px-4 py-8 text-center text-muted-foreground text-sm", children: EMPTY_STATE })
|
|
2359
|
+
}
|
|
2360
|
+
) })
|
|
2361
|
+
)
|
|
2362
|
+
}
|
|
2363
|
+
),
|
|
2364
|
+
errorView && /* @__PURE__ */ jsxs4(
|
|
2365
|
+
"div",
|
|
2366
|
+
{
|
|
2367
|
+
role: "alert",
|
|
2368
|
+
className: "mx-4 mb-2 rounded-lg border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm",
|
|
2369
|
+
children: [
|
|
2370
|
+
/* @__PURE__ */ jsx5("p", { className: "text-foreground", children: errorView.message }),
|
|
2371
|
+
errorView.cta && /* @__PURE__ */ jsxs4(
|
|
2372
|
+
"button",
|
|
2373
|
+
{
|
|
2374
|
+
type: "button",
|
|
2375
|
+
onClick: () => navigate?.(errorView.cta?.to ?? ""),
|
|
2376
|
+
className: "mt-1 text-primary text-xs",
|
|
2377
|
+
children: [
|
|
2378
|
+
errorView.cta.label,
|
|
2379
|
+
" \u2192"
|
|
2380
|
+
]
|
|
2381
|
+
}
|
|
2382
|
+
)
|
|
2383
|
+
]
|
|
2384
|
+
}
|
|
2385
|
+
),
|
|
2386
|
+
low && /* @__PURE__ */ jsxs4(
|
|
2387
|
+
"div",
|
|
2388
|
+
{
|
|
2389
|
+
role: "status",
|
|
2390
|
+
className: "mx-4 mb-2 rounded-lg border border-border bg-muted/40 px-3 py-2 text-sm",
|
|
2391
|
+
children: [
|
|
2392
|
+
/* @__PURE__ */ jsx5("p", { className: "text-foreground", children: "Your credit balance is running low." }),
|
|
2393
|
+
/* @__PURE__ */ jsx5(
|
|
2394
|
+
"button",
|
|
2395
|
+
{
|
|
2396
|
+
type: "button",
|
|
2397
|
+
onClick: () => navigate?.("/app/billing"),
|
|
2398
|
+
className: "mt-1 text-primary text-xs",
|
|
2399
|
+
children: "Add credits \u2192"
|
|
2400
|
+
}
|
|
2401
|
+
)
|
|
2402
|
+
]
|
|
2403
|
+
}
|
|
2404
|
+
),
|
|
2405
|
+
/* @__PURE__ */ jsxs4("div", { className: "border-border border-t p-2", children: [
|
|
2406
|
+
streaming && /* @__PURE__ */ jsx5("div", { className: "px-2 pb-1.5", "aria-label": "Assistant is working", children: /* @__PURE__ */ jsx5(WorkingIndicator, {}) }),
|
|
2407
|
+
/* @__PURE__ */ jsx5(
|
|
2408
|
+
ChatComposer,
|
|
2409
|
+
{
|
|
2410
|
+
onSend: (message) => {
|
|
2411
|
+
setView("chat");
|
|
2412
|
+
chat.send(message);
|
|
2413
|
+
},
|
|
2414
|
+
onCancel: chat.stop,
|
|
2415
|
+
isStreaming: streaming,
|
|
2416
|
+
disabled: chat.restoring || state.status === "awaiting_confirm",
|
|
2417
|
+
placeholder: "Message the assistant\u2026",
|
|
2418
|
+
controls: pickerModels.length > 0 ? /* @__PURE__ */ jsx5(
|
|
2419
|
+
ModelPicker,
|
|
2420
|
+
{
|
|
2421
|
+
value: pickerValue,
|
|
2422
|
+
onChange: (id) => chat.setModel(nextModelSelection(id, models.default)),
|
|
2423
|
+
models: pickerModels
|
|
2424
|
+
}
|
|
2425
|
+
) : /* @__PURE__ */ jsx5("span", { className: "px-1 text-muted-foreground text-xs", children: "Default model" })
|
|
2426
|
+
}
|
|
2427
|
+
)
|
|
2428
|
+
] })
|
|
2429
|
+
] });
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// src/assistant/launcher.tsx
|
|
2433
|
+
import {
|
|
2434
|
+
createContext as createContext2,
|
|
2435
|
+
useCallback as useCallback5,
|
|
2436
|
+
useContext as useContext2,
|
|
2437
|
+
useMemo as useMemo4,
|
|
2438
|
+
useState as useState7
|
|
2439
|
+
} from "react";
|
|
2440
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
2441
|
+
var AssistantLauncherContext = createContext2(null);
|
|
2442
|
+
function AssistantLauncherProvider({
|
|
2443
|
+
children
|
|
2444
|
+
}) {
|
|
2445
|
+
const [open, setOpen] = useState7(false);
|
|
2446
|
+
const [seed, setSeed] = useState7(null);
|
|
2447
|
+
const openAssistant = useCallback5((next) => {
|
|
2448
|
+
if (next != null) setSeed(next);
|
|
2449
|
+
setOpen(true);
|
|
2450
|
+
}, []);
|
|
2451
|
+
const closeAssistant = useCallback5(() => setOpen(false), []);
|
|
2452
|
+
const clearSeed = useCallback5(() => setSeed(null), []);
|
|
2453
|
+
const value = useMemo4(
|
|
2454
|
+
() => ({ open, seed, openAssistant, closeAssistant, clearSeed }),
|
|
2455
|
+
[open, seed, openAssistant, closeAssistant, clearSeed]
|
|
2456
|
+
);
|
|
2457
|
+
return /* @__PURE__ */ jsx6(AssistantLauncherContext.Provider, { value, children });
|
|
2458
|
+
}
|
|
2459
|
+
function useAssistantLauncher() {
|
|
2460
|
+
const ctx = useContext2(AssistantLauncherContext);
|
|
2461
|
+
if (!ctx) {
|
|
2462
|
+
throw new Error(
|
|
2463
|
+
"useAssistantLauncher must be used within an AssistantLauncherProvider"
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
return ctx;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// src/assistant/ResizeHandle.tsx
|
|
2470
|
+
import { useRef as useRef5 } from "react";
|
|
2471
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
2472
|
+
function ResizeHandle({
|
|
2473
|
+
width,
|
|
2474
|
+
maxWidth,
|
|
2475
|
+
onPreview,
|
|
2476
|
+
onCommit,
|
|
2477
|
+
onNudge
|
|
2478
|
+
}) {
|
|
2479
|
+
const dragRef = useRef5(null);
|
|
2480
|
+
const onPointerDown = (e) => {
|
|
2481
|
+
if (e.button !== 0) return;
|
|
2482
|
+
e.preventDefault();
|
|
2483
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
2484
|
+
dragRef.current = {
|
|
2485
|
+
startX: e.clientX,
|
|
2486
|
+
startWidth: width,
|
|
2487
|
+
lastWidth: width
|
|
2488
|
+
};
|
|
2489
|
+
};
|
|
2490
|
+
const onPointerMove = (e) => {
|
|
2491
|
+
const drag = dragRef.current;
|
|
2492
|
+
if (!drag) return;
|
|
2493
|
+
const next = drag.startWidth + (drag.startX - e.clientX);
|
|
2494
|
+
if (Math.abs(next - drag.lastWidth) < 1) return;
|
|
2495
|
+
drag.lastWidth = next;
|
|
2496
|
+
onPreview(next);
|
|
2497
|
+
};
|
|
2498
|
+
const endDrag = (e) => {
|
|
2499
|
+
const drag = dragRef.current;
|
|
2500
|
+
if (!drag) return;
|
|
2501
|
+
dragRef.current = null;
|
|
2502
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
2503
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
2504
|
+
}
|
|
2505
|
+
onCommit(drag.lastWidth);
|
|
2506
|
+
};
|
|
2507
|
+
const onKeyDown = (e) => {
|
|
2508
|
+
const STEP = 24;
|
|
2509
|
+
if (e.key === "ArrowLeft") {
|
|
2510
|
+
e.preventDefault();
|
|
2511
|
+
onNudge(STEP);
|
|
2512
|
+
} else if (e.key === "ArrowRight") {
|
|
2513
|
+
e.preventDefault();
|
|
2514
|
+
onNudge(-STEP);
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
return (
|
|
2518
|
+
// biome-ignore lint/a11y/useSemanticElements: a focusable drag handle is an ARIA window-splitter (role=separator); no native HTML element provides this.
|
|
2519
|
+
/* @__PURE__ */ jsx7(
|
|
2520
|
+
"div",
|
|
2521
|
+
{
|
|
2522
|
+
role: "separator",
|
|
2523
|
+
"aria-orientation": "vertical",
|
|
2524
|
+
"aria-label": "Resize assistant panel",
|
|
2525
|
+
"aria-valuemin": MIN_PANEL_WIDTH,
|
|
2526
|
+
"aria-valuemax": maxWidth,
|
|
2527
|
+
"aria-valuenow": width,
|
|
2528
|
+
tabIndex: 0,
|
|
2529
|
+
onPointerDown,
|
|
2530
|
+
onPointerMove,
|
|
2531
|
+
onPointerUp: endDrag,
|
|
2532
|
+
onPointerCancel: endDrag,
|
|
2533
|
+
onKeyDown,
|
|
2534
|
+
className: "group absolute inset-y-0 left-0 z-10 flex w-2 -translate-x-1/2 cursor-ew-resize touch-none items-center justify-center focus:outline-none",
|
|
2535
|
+
children: /* @__PURE__ */ jsx7(
|
|
2536
|
+
"span",
|
|
2537
|
+
{
|
|
2538
|
+
"aria-hidden": "true",
|
|
2539
|
+
className: "h-10 w-1 rounded-full bg-border transition-colors group-hover:bg-primary group-focus:bg-primary"
|
|
2540
|
+
}
|
|
2541
|
+
)
|
|
2542
|
+
}
|
|
2543
|
+
)
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/assistant/AssistantDock.tsx
|
|
2548
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2549
|
+
function focusableWithin(container) {
|
|
2550
|
+
const selector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
2551
|
+
return Array.from(container.querySelectorAll(selector)).filter(
|
|
2552
|
+
(el) => el.getClientRects().length > 0
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
function AssistantDock({
|
|
2556
|
+
userId,
|
|
2557
|
+
navigate,
|
|
2558
|
+
balanceUsd = null,
|
|
2559
|
+
formatMoney,
|
|
2560
|
+
renderGraph,
|
|
2561
|
+
onWorkflowMutation,
|
|
2562
|
+
renderMarkdown,
|
|
2563
|
+
toolRenderers,
|
|
2564
|
+
renderTranscript
|
|
2565
|
+
}) {
|
|
2566
|
+
const { open, openAssistant, closeAssistant } = useAssistantLauncher();
|
|
2567
|
+
const chat = useAssistantChat(userId, { onWorkflowMutation });
|
|
2568
|
+
const isDesktop = useIsDesktop();
|
|
2569
|
+
const { width, maxWidth, setWidth, previewWidth, nudgeWidth } = usePanelWidth();
|
|
2570
|
+
const launcherRef = useRef6(null);
|
|
2571
|
+
const dialogRef = useRef6(null);
|
|
2572
|
+
const returnFocusRef = useRef6(null);
|
|
2573
|
+
const wasOpenRef = useRef6(false);
|
|
2574
|
+
useEffect6(() => {
|
|
2575
|
+
if (!open) return;
|
|
2576
|
+
const onKeyDown = (e) => {
|
|
2577
|
+
if (e.key === "Escape") closeAssistant();
|
|
2578
|
+
};
|
|
2579
|
+
document.addEventListener("keydown", onKeyDown);
|
|
2580
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
2581
|
+
}, [open, closeAssistant]);
|
|
2582
|
+
useEffect6(() => {
|
|
2583
|
+
if (open) {
|
|
2584
|
+
if (!wasOpenRef.current && !returnFocusRef.current) {
|
|
2585
|
+
returnFocusRef.current = document.activeElement;
|
|
2586
|
+
}
|
|
2587
|
+
wasOpenRef.current = true;
|
|
2588
|
+
const el = dialogRef.current;
|
|
2589
|
+
if (el) (focusableWithin(el)[0] ?? el).focus();
|
|
2590
|
+
} else if (wasOpenRef.current) {
|
|
2591
|
+
wasOpenRef.current = false;
|
|
2592
|
+
const target = returnFocusRef.current?.isConnected ? returnFocusRef.current : launcherRef.current;
|
|
2593
|
+
target?.focus();
|
|
2594
|
+
returnFocusRef.current = null;
|
|
2595
|
+
}
|
|
2596
|
+
}, [open]);
|
|
2597
|
+
const openDialog = () => {
|
|
2598
|
+
returnFocusRef.current = document.activeElement;
|
|
2599
|
+
openAssistant();
|
|
2600
|
+
};
|
|
2601
|
+
if (!open) {
|
|
2602
|
+
return /* @__PURE__ */ jsx8(
|
|
2603
|
+
"button",
|
|
2604
|
+
{
|
|
2605
|
+
ref: launcherRef,
|
|
2606
|
+
type: "button",
|
|
2607
|
+
onClick: openDialog,
|
|
2608
|
+
"aria-label": "Open assistant",
|
|
2609
|
+
className: "fixed right-4 bottom-4 z-40 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-colors hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring",
|
|
2610
|
+
children: /* @__PURE__ */ jsx8(MessageSquare, { className: "h-6 w-6" })
|
|
2611
|
+
}
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
const trapTab = (e) => {
|
|
2615
|
+
if (e.key !== "Tab") return;
|
|
2616
|
+
const dialog = dialogRef.current;
|
|
2617
|
+
if (!dialog) return;
|
|
2618
|
+
const focusables = focusableWithin(dialog);
|
|
2619
|
+
if (focusables.length === 0) {
|
|
2620
|
+
e.preventDefault();
|
|
2621
|
+
dialog.focus();
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
const first = focusables[0];
|
|
2625
|
+
const last = focusables[focusables.length - 1];
|
|
2626
|
+
const active = document.activeElement;
|
|
2627
|
+
const inside = active instanceof Node && dialog.contains(active);
|
|
2628
|
+
if (e.shiftKey) {
|
|
2629
|
+
if (!inside || active === first || active === dialog) {
|
|
2630
|
+
e.preventDefault();
|
|
2631
|
+
last.focus();
|
|
2632
|
+
}
|
|
2633
|
+
} else if (!inside || active === last) {
|
|
2634
|
+
e.preventDefault();
|
|
2635
|
+
first.focus();
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
2639
|
+
/* @__PURE__ */ jsx8(
|
|
2640
|
+
"div",
|
|
2641
|
+
{
|
|
2642
|
+
"aria-hidden": "true",
|
|
2643
|
+
className: "fixed inset-0 z-40 bg-black/40",
|
|
2644
|
+
onClick: () => closeAssistant()
|
|
2645
|
+
}
|
|
2646
|
+
),
|
|
2647
|
+
/* @__PURE__ */ jsxs5(
|
|
2648
|
+
"div",
|
|
2649
|
+
{
|
|
2650
|
+
ref: dialogRef,
|
|
2651
|
+
role: "dialog",
|
|
2652
|
+
"aria-label": "Assistant",
|
|
2653
|
+
"aria-modal": "true",
|
|
2654
|
+
tabIndex: -1,
|
|
2655
|
+
onKeyDown: trapTab,
|
|
2656
|
+
style: isDesktop ? { width: `${width}px` } : void 0,
|
|
2657
|
+
className: "fixed inset-y-0 right-0 z-50 flex w-full flex-col border-border border-l shadow-xl focus:outline-none",
|
|
2658
|
+
children: [
|
|
2659
|
+
/* @__PURE__ */ jsx8(
|
|
2660
|
+
AssistantPanel,
|
|
2661
|
+
{
|
|
2662
|
+
chat,
|
|
2663
|
+
userId,
|
|
2664
|
+
onClose: () => closeAssistant(),
|
|
2665
|
+
navigate,
|
|
2666
|
+
balanceUsd,
|
|
2667
|
+
formatMoney,
|
|
2668
|
+
renderGraph,
|
|
2669
|
+
renderMarkdown,
|
|
2670
|
+
toolRenderers,
|
|
2671
|
+
renderTranscript
|
|
2672
|
+
},
|
|
2673
|
+
userId ?? "anon"
|
|
2674
|
+
),
|
|
2675
|
+
isDesktop && /* @__PURE__ */ jsx8(
|
|
2676
|
+
ResizeHandle,
|
|
2677
|
+
{
|
|
2678
|
+
width,
|
|
2679
|
+
maxWidth,
|
|
2680
|
+
onPreview: previewWidth,
|
|
2681
|
+
onCommit: setWidth,
|
|
2682
|
+
onNudge: nudgeWidth
|
|
2683
|
+
}
|
|
2684
|
+
)
|
|
2685
|
+
]
|
|
2686
|
+
}
|
|
2687
|
+
)
|
|
2688
|
+
] });
|
|
2689
|
+
}
|
|
2690
|
+
export {
|
|
2691
|
+
AssistantClientProvider,
|
|
2692
|
+
AssistantDock,
|
|
2693
|
+
AssistantLauncherProvider,
|
|
2694
|
+
AssistantPanel,
|
|
2695
|
+
AssistantTranscript,
|
|
2696
|
+
ProposalCard,
|
|
2697
|
+
adaptTranscript,
|
|
2698
|
+
assistantIsThinking,
|
|
2699
|
+
createAssistantClient,
|
|
2700
|
+
useAssistantChat,
|
|
2701
|
+
useAssistantClient,
|
|
2702
|
+
useAssistantLauncher,
|
|
2703
|
+
useAssistantModels,
|
|
2704
|
+
useAssistantThreads
|
|
2705
|
+
};
|
|
2706
|
+
//# sourceMappingURL=index.js.map
|