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