@noodleseed/assistant 1.1.1 → 1.2.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/README.md +90 -7
- package/dist/chunk-J2YDKISA.js +1113 -0
- package/dist/chunk-J2YDKISA.js.map +1 -0
- package/dist/chunk-WUDHE5BD.js +636 -0
- package/dist/chunk-WUDHE5BD.js.map +1 -0
- package/dist/client-CwvdK4O6.d.cts +165 -0
- package/dist/client-DkAfqcSb.d.ts +165 -0
- package/dist/client.cjs +662 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +2 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +9 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +1273 -242
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -12
- package/dist/index.d.ts +14 -12
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1273 -242
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -1
- package/dist/react.d.ts +2 -1
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +2 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +6 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/package.json +10 -1
- package/dist/chunk-IM7SAHNE.js +0 -706
- package/dist/chunk-IM7SAHNE.js.map +0 -1
package/dist/react.cjs
CHANGED
|
@@ -207,75 +207,1220 @@ function radiusValue(radius) {
|
|
|
207
207
|
return 14;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// src/browser-context.ts
|
|
211
|
+
function browserClientContext() {
|
|
212
|
+
let timeZone;
|
|
213
|
+
try {
|
|
214
|
+
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
const locale = globalThis.navigator?.language;
|
|
218
|
+
return {
|
|
219
|
+
...locale ? { locale } : {},
|
|
220
|
+
...timeZone ? { timeZone } : {}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/events.ts
|
|
225
|
+
function toAssistantClientEvent(event) {
|
|
226
|
+
const value = event.data;
|
|
227
|
+
switch (event.event) {
|
|
228
|
+
case "content":
|
|
229
|
+
if (typeof value.delta === "string" && hasOptionalTurnId(value)) {
|
|
230
|
+
return event;
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
case "tool_proposed":
|
|
234
|
+
if (hasString(value, "id") && hasString(value, "tool") && hasOptionalJson(value.arguments) && hasOptionalString(value.expiresAt) && (value.requiresConfirmation === void 0 || value.requiresConfirmation === true) && hasOptionalTurnId(value)) {
|
|
235
|
+
return event;
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
case "input_requested":
|
|
239
|
+
if (hasString(value, "id") && hasString(value, "message") && isRecord(value.requestedSchema) && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
|
|
240
|
+
return event;
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
case "interaction_resolved":
|
|
244
|
+
if (hasString(value, "id") && (value.action === void 0 || isInteractionAction(value.action)) && hasOptionalTurnId(value)) {
|
|
245
|
+
return event;
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
case "tool_completed":
|
|
249
|
+
if (hasString(value, "id") && hasString(value, "tool") && isJsonValue(value.result, 0) && (value.replayed === void 0 || value.replayed === true) && hasOptionalTurnId(value)) {
|
|
250
|
+
return event;
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case "view_available":
|
|
254
|
+
if (isViewAvailableDetail(value)) return event;
|
|
255
|
+
break;
|
|
256
|
+
case "error":
|
|
257
|
+
if (hasString(value, "code") && (value.status === void 0 || typeof value.status === "number") && (value.retryable === void 0 || typeof value.retryable === "boolean") && hasOptionalTurnId(value)) {
|
|
258
|
+
return event;
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
case "done":
|
|
262
|
+
if (hasOptionalTurnId(value)) return event;
|
|
263
|
+
break;
|
|
264
|
+
case "interaction_proposed":
|
|
265
|
+
if (hasString(value, "id") && (hasString(value, "tool") || hasString(value, "title")) && hasOptionalJson(value.arguments)) {
|
|
266
|
+
return event;
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
return { event: "unrecognized", data: { name: event.event, payload: value } };
|
|
271
|
+
}
|
|
272
|
+
function isViewAvailableDetail(value) {
|
|
273
|
+
return hasString(value, "id") && hasString(value, "tool") && typeof value.resourceUri === "string" && value.resourceUri.startsWith("ui://") && hasOptionalString(value.title) && (value.replayed === void 0 || value.replayed === true) && isJsonValue(value.result, 0) && hasOptionalTurnId(value);
|
|
274
|
+
}
|
|
275
|
+
function hasOptionalTurnId(value) {
|
|
276
|
+
return hasOptionalString(value.turnId);
|
|
277
|
+
}
|
|
278
|
+
function hasString(value, key) {
|
|
279
|
+
const entry = value[key];
|
|
280
|
+
return typeof entry === "string" && entry.length > 0;
|
|
281
|
+
}
|
|
282
|
+
function hasOptionalString(value) {
|
|
283
|
+
return value === void 0 || typeof value === "string";
|
|
284
|
+
}
|
|
285
|
+
function hasOptionalJson(value) {
|
|
286
|
+
return value === void 0 || isJsonValue(value, 0);
|
|
287
|
+
}
|
|
288
|
+
function isInteractionAction(value) {
|
|
289
|
+
return value === "accept" || value === "decline" || value === "cancel";
|
|
290
|
+
}
|
|
291
|
+
function isJsonValue(value, depth) {
|
|
292
|
+
if (depth > 16) return false;
|
|
293
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
294
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
295
|
+
if (Array.isArray(value)) {
|
|
296
|
+
return value.length <= 256 && value.every((entry) => isJsonValue(entry, depth + 1));
|
|
297
|
+
}
|
|
298
|
+
if (!isRecord(value)) return false;
|
|
299
|
+
const entries = Object.entries(value);
|
|
300
|
+
return entries.length <= 256 && entries.every(([, entry]) => isJsonValue(entry, depth + 1));
|
|
301
|
+
}
|
|
302
|
+
function isRecord(value) {
|
|
303
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/model-context.ts
|
|
307
|
+
var MAX_BYTES = 16 * 1024;
|
|
308
|
+
var MAX_DEPTH = 8;
|
|
309
|
+
var MAX_ENTRIES = 128;
|
|
310
|
+
var SENSITIVE_KEY = /(?:secret|token|api[-_]?key|password|credential|authorization|cookie)/i;
|
|
311
|
+
var CREDENTIAL_SHAPED_TEXT = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\bsk-[A-Za-z0-9_-]{20,}\b|\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{20,}\b|\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b)/i;
|
|
312
|
+
function copyAssistantModelContext(update) {
|
|
313
|
+
const copied = copyJsonValue(update, "$", 0, /* @__PURE__ */ new Set());
|
|
314
|
+
assertModelContextShape(copied);
|
|
315
|
+
const encoded = JSON.stringify(copied);
|
|
316
|
+
if (new TextEncoder().encode(encoded).byteLength > MAX_BYTES) {
|
|
317
|
+
throw new Error("Model context must not exceed 16 KiB");
|
|
318
|
+
}
|
|
319
|
+
return copied;
|
|
320
|
+
}
|
|
321
|
+
function assertModelContextShape(value) {
|
|
322
|
+
if (!isPlainRecord(value)) throw new Error("Model context must be a JSON object");
|
|
323
|
+
for (const key of Object.keys(value)) {
|
|
324
|
+
if (key !== "content" && key !== "structuredContent") {
|
|
325
|
+
throw new Error(`Model context contains unknown field ${key}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (value.content !== void 0) {
|
|
329
|
+
if (!Array.isArray(value.content)) throw new Error("Model context content must be an array");
|
|
330
|
+
for (const part of value.content) {
|
|
331
|
+
if (!isPlainRecord(part) || Object.keys(part).some((key) => key !== "type" && key !== "text") || part.type !== "text" || typeof part.text !== "string") {
|
|
332
|
+
throw new Error("Model context content supports text parts only");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (value.structuredContent !== void 0 && !isPlainRecord(value.structuredContent)) {
|
|
337
|
+
throw new Error("Model context structuredContent must be an object");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function copyJsonValue(value, path, depth, ancestors) {
|
|
341
|
+
if (depth > MAX_DEPTH) throw new Error(`Model context exceeds maximum depth at ${path}`);
|
|
342
|
+
if (typeof value === "string") {
|
|
343
|
+
if (CREDENTIAL_SHAPED_TEXT.test(value)) {
|
|
344
|
+
throw new Error(`Model context contains credential-shaped text at ${path}`);
|
|
345
|
+
}
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
349
|
+
return value;
|
|
350
|
+
}
|
|
351
|
+
if (typeof value !== "object") throw new Error(`Model context contains non-JSON data at ${path}`);
|
|
352
|
+
if (hasToJsonProperty(value)) {
|
|
353
|
+
throw new Error(`Model context must not define or inherit toJSON at ${path}`);
|
|
354
|
+
}
|
|
355
|
+
const prototype = Object.getPrototypeOf(value);
|
|
356
|
+
if (Array.isArray(value)) {
|
|
357
|
+
if (prototype !== Array.prototype) {
|
|
358
|
+
throw new Error(`Model context contains a non-JSON object at ${path}`);
|
|
359
|
+
}
|
|
360
|
+
} else if (prototype !== Object.prototype && prototype !== null) {
|
|
361
|
+
throw new Error(`Model context contains a non-JSON object at ${path}`);
|
|
362
|
+
}
|
|
363
|
+
if (ancestors.has(value)) throw new Error(`Model context contains a cycle at ${path}`);
|
|
364
|
+
if (Array.isArray(value)) {
|
|
365
|
+
if (value.length > MAX_ENTRIES) {
|
|
366
|
+
throw new Error(`Model context has more than 128 entries at ${path}`);
|
|
367
|
+
}
|
|
368
|
+
const copy2 = [];
|
|
369
|
+
ancestors.add(value);
|
|
370
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
371
|
+
const entry = Object.hasOwn(value, index) ? value[index] : null;
|
|
372
|
+
copy2.push(copyJsonValue(entry, `${path}.${index}`, depth + 1, ancestors));
|
|
373
|
+
}
|
|
374
|
+
ancestors.delete(value);
|
|
375
|
+
return copy2;
|
|
376
|
+
}
|
|
377
|
+
const entries = Object.entries(value);
|
|
378
|
+
if (entries.length > MAX_ENTRIES) {
|
|
379
|
+
throw new Error(`Model context has more than 128 entries at ${path}`);
|
|
380
|
+
}
|
|
381
|
+
const copy = /* @__PURE__ */ Object.create(null);
|
|
382
|
+
ancestors.add(value);
|
|
383
|
+
for (const [key, entry] of entries) {
|
|
384
|
+
if (SENSITIVE_KEY.test(key)) {
|
|
385
|
+
throw new Error(`Model context contains sensitive key ${path}.${key}`);
|
|
386
|
+
}
|
|
387
|
+
copy[key] = copyJsonValue(entry, `${path}.${key}`, depth + 1, ancestors);
|
|
388
|
+
}
|
|
389
|
+
ancestors.delete(value);
|
|
390
|
+
return copy;
|
|
391
|
+
}
|
|
392
|
+
function isPlainRecord(value) {
|
|
393
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
394
|
+
const prototype = Object.getPrototypeOf(value);
|
|
395
|
+
return prototype === Object.prototype || prototype === null;
|
|
396
|
+
}
|
|
397
|
+
function hasToJsonProperty(value) {
|
|
398
|
+
let current = value;
|
|
399
|
+
const visited = /* @__PURE__ */ new Set();
|
|
400
|
+
while (current !== null && !visited.has(current)) {
|
|
401
|
+
visited.add(current);
|
|
402
|
+
if (Object.getOwnPropertyDescriptor(current, "toJSON") !== void 0) return true;
|
|
403
|
+
current = Object.getPrototypeOf(current);
|
|
404
|
+
}
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
210
408
|
// src/transport.ts
|
|
211
409
|
var AssistantTransportError = class extends Error {
|
|
212
410
|
code = "invalid_response";
|
|
213
|
-
constructor(
|
|
214
|
-
super(
|
|
411
|
+
constructor(message, options) {
|
|
412
|
+
super(message, options);
|
|
215
413
|
this.name = "AssistantTransportError";
|
|
216
414
|
}
|
|
217
415
|
};
|
|
218
416
|
async function consumeAssistantEvents(response, onEvent) {
|
|
219
|
-
|
|
220
|
-
let sawText = false;
|
|
221
|
-
const deliver = (event) => {
|
|
222
|
-
delivered += 1;
|
|
223
|
-
onEvent(event);
|
|
224
|
-
};
|
|
417
|
+
const stream = new AssistantEventStream(onEvent);
|
|
225
418
|
if (!response.body) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (!sawText && chunk.trim().length > 0) sawText = true;
|
|
237
|
-
pending += chunk;
|
|
238
|
-
pending = emitFrames(pending, deliver, done);
|
|
239
|
-
if (done) break;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (delivered === 0 && sawText) {
|
|
243
|
-
throw new AssistantTransportError(response.headers.get("content-type") ?? "");
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
function emitFrames(input, onEvent, flush) {
|
|
247
|
-
const normalized = input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
248
|
-
const frames = normalized.split("\n\n");
|
|
249
|
-
const pending = flush ? "" : frames.pop() ?? "";
|
|
250
|
-
for (const frame of frames) {
|
|
251
|
-
const event = parseFrame(frame);
|
|
252
|
-
if (event) onEvent(event);
|
|
419
|
+
let text;
|
|
420
|
+
try {
|
|
421
|
+
text = await response.text();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (isAbortError(error)) throw error;
|
|
424
|
+
throw invalidStream("assistant event stream could not be read", error);
|
|
425
|
+
}
|
|
426
|
+
stream.push(text);
|
|
427
|
+
stream.finish(response.headers.get("content-type") ?? "");
|
|
428
|
+
return;
|
|
253
429
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
430
|
+
const reader = response.body.getReader();
|
|
431
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
432
|
+
while (true) {
|
|
433
|
+
let result;
|
|
434
|
+
try {
|
|
435
|
+
result = await reader.read();
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (isAbortError(error)) throw error;
|
|
438
|
+
throw invalidStream("assistant event stream ended unexpectedly", error);
|
|
439
|
+
}
|
|
440
|
+
let chunk;
|
|
441
|
+
try {
|
|
442
|
+
chunk = decoder.decode(result.value, { stream: !result.done });
|
|
443
|
+
} catch (error) {
|
|
444
|
+
throw invalidStream("assistant event stream contained invalid UTF-8", error);
|
|
445
|
+
}
|
|
446
|
+
stream.push(chunk);
|
|
447
|
+
if (result.done) break;
|
|
257
448
|
}
|
|
258
|
-
|
|
449
|
+
stream.finish(response.headers.get("content-type") ?? "");
|
|
259
450
|
}
|
|
451
|
+
var AssistantEventStream = class {
|
|
452
|
+
#onEvent;
|
|
453
|
+
#pending = "";
|
|
454
|
+
#terminal;
|
|
455
|
+
constructor(onEvent) {
|
|
456
|
+
this.#onEvent = onEvent;
|
|
457
|
+
}
|
|
458
|
+
push(chunk) {
|
|
459
|
+
this.#pending += chunk;
|
|
460
|
+
const protectedCarriageReturn = this.#pending.endsWith("\r");
|
|
461
|
+
const normalizable = protectedCarriageReturn ? this.#pending.slice(0, -1) : this.#pending;
|
|
462
|
+
const frames = normalizeLineEndings(normalizable).split("\n\n");
|
|
463
|
+
this.#pending = `${frames.pop() ?? ""}${protectedCarriageReturn ? "\r" : ""}`;
|
|
464
|
+
for (const frame of frames) this.#acceptFrame(frame);
|
|
465
|
+
}
|
|
466
|
+
finish(contentType) {
|
|
467
|
+
if (this.#pending.trim().length > 0) {
|
|
468
|
+
throw invalidStream("assistant event stream ended with an unterminated frame");
|
|
469
|
+
}
|
|
470
|
+
if (!this.#terminal) {
|
|
471
|
+
throw invalidStream(
|
|
472
|
+
`assistant response did not contain a terminal done event (content-type: ${contentType || "unknown"})`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
this.#onEvent(this.#terminal);
|
|
476
|
+
}
|
|
477
|
+
#acceptFrame(frame) {
|
|
478
|
+
const event = parseFrame(frame);
|
|
479
|
+
if (!event) return;
|
|
480
|
+
if (this.#terminal) {
|
|
481
|
+
throw invalidStream("assistant event stream contained an event after done");
|
|
482
|
+
}
|
|
483
|
+
if (event.event === "done") {
|
|
484
|
+
this.#terminal = event;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
this.#onEvent(event);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
260
490
|
function parseFrame(frame) {
|
|
261
|
-
let event
|
|
491
|
+
let event;
|
|
262
492
|
const data = [];
|
|
493
|
+
let hasFields = false;
|
|
263
494
|
for (const line of frame.split("\n")) {
|
|
264
|
-
if (line.startsWith(":")) continue;
|
|
495
|
+
if (!line || line.startsWith(":")) continue;
|
|
496
|
+
hasFields = true;
|
|
265
497
|
const separator = line.indexOf(":");
|
|
266
498
|
const field = separator < 0 ? line : line.slice(0, separator);
|
|
267
499
|
const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
|
|
268
500
|
if (field === "event") event = value;
|
|
269
501
|
if (field === "data") data.push(value);
|
|
270
502
|
}
|
|
271
|
-
if (
|
|
503
|
+
if (!hasFields) return void 0;
|
|
504
|
+
if (!event || data.length === 0) {
|
|
505
|
+
throw invalidStream("assistant event stream contained a malformed frame");
|
|
506
|
+
}
|
|
507
|
+
let parsed;
|
|
508
|
+
try {
|
|
509
|
+
parsed = JSON.parse(data.join("\n"));
|
|
510
|
+
} catch (error) {
|
|
511
|
+
throw invalidStream("assistant event stream contained malformed JSON", error);
|
|
512
|
+
}
|
|
513
|
+
if (event === "done" && !isValidDonePayload(parsed)) {
|
|
514
|
+
throw invalidStream("assistant event stream contained an invalid done event");
|
|
515
|
+
}
|
|
516
|
+
return { event, data: isRecord2(parsed) ? parsed : { value: parsed } };
|
|
517
|
+
}
|
|
518
|
+
function normalizeLineEndings(input) {
|
|
519
|
+
return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
520
|
+
}
|
|
521
|
+
function isValidDonePayload(value) {
|
|
522
|
+
return isRecord2(value) && (value.turnId === void 0 || typeof value.turnId === "string" && value.turnId.length > 0);
|
|
523
|
+
}
|
|
524
|
+
function isRecord2(value) {
|
|
525
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
526
|
+
}
|
|
527
|
+
function invalidStream(message, cause) {
|
|
528
|
+
return new AssistantTransportError(message, cause === void 0 ? void 0 : { cause });
|
|
529
|
+
}
|
|
530
|
+
function isAbortError(error) {
|
|
531
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/client.ts
|
|
535
|
+
var AssistantClientError = class extends Error {
|
|
536
|
+
detail;
|
|
537
|
+
constructor(detail, message, options) {
|
|
538
|
+
super(message, options);
|
|
539
|
+
this.name = "AssistantClientError";
|
|
540
|
+
this.detail = detail;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
var DefaultAssistantClient = class {
|
|
544
|
+
#sessionEndpoint;
|
|
545
|
+
#fetch;
|
|
546
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
547
|
+
#clientContext;
|
|
548
|
+
#session;
|
|
549
|
+
#context;
|
|
550
|
+
#modelContext;
|
|
551
|
+
#active;
|
|
552
|
+
constructor(options) {
|
|
553
|
+
if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
|
|
554
|
+
this.#sessionEndpoint = options.sessionEndpoint;
|
|
555
|
+
this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
556
|
+
this.#context = options.context ? { ...options.context } : void 0;
|
|
557
|
+
this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
|
|
558
|
+
this.#clientContext = options.clientContext;
|
|
559
|
+
}
|
|
560
|
+
subscribe(listener) {
|
|
561
|
+
this.#listeners.add(listener);
|
|
562
|
+
return () => this.#listeners.delete(listener);
|
|
563
|
+
}
|
|
564
|
+
async sendMessage(text) {
|
|
565
|
+
const message = text.trim();
|
|
566
|
+
if (!message) return;
|
|
567
|
+
await this.#singleFlight(async (signal) => {
|
|
568
|
+
await this.#sendTurn(message, true, signal, this.#modelContext);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
async respond(id, resolution) {
|
|
572
|
+
if (!id) {
|
|
573
|
+
throw clientError("invalid_request", "interaction id is required", false);
|
|
574
|
+
}
|
|
575
|
+
await this.#singleFlight(async (signal) => {
|
|
576
|
+
const session = this.#session;
|
|
577
|
+
if (!session) {
|
|
578
|
+
throw clientError("confirmation_expired", "assistant session is not active", false);
|
|
579
|
+
}
|
|
580
|
+
const modernEndpoint = session.endpoints.interactions;
|
|
581
|
+
if (!modernEndpoint && resolution.action !== "accept") {
|
|
582
|
+
throw clientError(
|
|
583
|
+
"unsupported_service",
|
|
584
|
+
"the assistant service does not support declining or cancelling interactions",
|
|
585
|
+
false
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const endpoint = modernEndpoint ?? session.endpoints.toolConfirmations;
|
|
589
|
+
const body = modernEndpoint ? {
|
|
590
|
+
id,
|
|
591
|
+
action: resolution.action,
|
|
592
|
+
...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
|
|
593
|
+
} : { id };
|
|
594
|
+
this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
|
|
595
|
+
const response = await this.#request(
|
|
596
|
+
endpoint,
|
|
597
|
+
{
|
|
598
|
+
method: "POST",
|
|
599
|
+
headers: authorizedHeaders(session.token, "text/event-stream"),
|
|
600
|
+
body: JSON.stringify(body),
|
|
601
|
+
signal
|
|
602
|
+
},
|
|
603
|
+
"confirmation_failed"
|
|
604
|
+
);
|
|
605
|
+
if (response.status === 401) {
|
|
606
|
+
this.#session = void 0;
|
|
607
|
+
this.#emit({ event: "session_expired", data: {} });
|
|
608
|
+
}
|
|
609
|
+
if (!response.ok) {
|
|
610
|
+
const serverCode = await readStableErrorCode(response);
|
|
611
|
+
const expired = response.status === 401 || response.status === 409;
|
|
612
|
+
throw clientError(
|
|
613
|
+
serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
|
|
614
|
+
`Assistant interaction failed (${response.status})`,
|
|
615
|
+
false,
|
|
616
|
+
response.status
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
await this.#consume(response);
|
|
620
|
+
this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
updateContext(context) {
|
|
624
|
+
this.#context = { ...context };
|
|
625
|
+
this.#emit({ event: "context_changed", data: { context: this.#context } });
|
|
626
|
+
}
|
|
627
|
+
updateModelContext(update) {
|
|
628
|
+
this.#modelContext = copyAssistantModelContext(update);
|
|
629
|
+
this.#emit({ event: "model_context_changed", data: { modelContext: this.#modelContext } });
|
|
630
|
+
}
|
|
631
|
+
resetSession() {
|
|
632
|
+
this.#session = void 0;
|
|
633
|
+
this.#emit({ event: "session_reset", data: {} });
|
|
634
|
+
}
|
|
635
|
+
abort() {
|
|
636
|
+
this.#active?.abort();
|
|
637
|
+
}
|
|
638
|
+
async #sendTurn(message, mayRetry, signal, modelContext) {
|
|
639
|
+
const session = this.#session ?? await this.#createSession(signal);
|
|
640
|
+
if (mayRetry) this.#emit({ event: "message_started", data: { message } });
|
|
641
|
+
const clientContext = this.#resolveClientContext();
|
|
642
|
+
const response = await this.#request(
|
|
643
|
+
session.endpoints.turns,
|
|
644
|
+
{
|
|
645
|
+
method: "POST",
|
|
646
|
+
headers: authorizedHeaders(session.token, "text/event-stream"),
|
|
647
|
+
body: JSON.stringify({
|
|
648
|
+
message,
|
|
649
|
+
...clientContext ? { clientContext } : {},
|
|
650
|
+
...modelContext ? { modelContext } : {}
|
|
651
|
+
}),
|
|
652
|
+
signal
|
|
653
|
+
},
|
|
654
|
+
"turn_failed"
|
|
655
|
+
);
|
|
656
|
+
if (response.status === 401 && mayRetry) {
|
|
657
|
+
this.#session = void 0;
|
|
658
|
+
this.#emit({ event: "session_expired", data: {} });
|
|
659
|
+
await this.#sendTurn(message, false, signal, modelContext);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!response.ok) {
|
|
663
|
+
throw clientError(
|
|
664
|
+
"turn_failed",
|
|
665
|
+
`Assistant turn failed (${response.status})`,
|
|
666
|
+
false,
|
|
667
|
+
response.status
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
await this.#consume(response);
|
|
671
|
+
this.#emit({ event: "message_completed", data: {} });
|
|
672
|
+
}
|
|
673
|
+
#resolveClientContext() {
|
|
674
|
+
let value;
|
|
675
|
+
try {
|
|
676
|
+
value = typeof this.#clientContext === "function" ? this.#clientContext() : this.#clientContext;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
throw clientError(
|
|
679
|
+
"turn_failed",
|
|
680
|
+
"Assistant client context provider failed",
|
|
681
|
+
false,
|
|
682
|
+
void 0,
|
|
683
|
+
{
|
|
684
|
+
cause: error
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (!value) return void 0;
|
|
689
|
+
const context = {
|
|
690
|
+
...typeof value.locale === "string" && value.locale ? { locale: value.locale } : {},
|
|
691
|
+
...typeof value.timeZone === "string" && value.timeZone ? { timeZone: value.timeZone } : {}
|
|
692
|
+
};
|
|
693
|
+
return Object.keys(context).length > 0 ? context : void 0;
|
|
694
|
+
}
|
|
695
|
+
async #createSession(signal) {
|
|
696
|
+
const response = await this.#request(
|
|
697
|
+
this.#sessionEndpoint,
|
|
698
|
+
{
|
|
699
|
+
method: "POST",
|
|
700
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
701
|
+
body: JSON.stringify(this.#context ? { context: this.#context } : {}),
|
|
702
|
+
credentials: "same-origin",
|
|
703
|
+
signal
|
|
704
|
+
},
|
|
705
|
+
"session_failed"
|
|
706
|
+
);
|
|
707
|
+
if (!response.ok) {
|
|
708
|
+
throw clientError(
|
|
709
|
+
"session_failed",
|
|
710
|
+
`Assistant session failed (${response.status})`,
|
|
711
|
+
true,
|
|
712
|
+
response.status
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
let value;
|
|
716
|
+
try {
|
|
717
|
+
value = await response.json();
|
|
718
|
+
} catch (error) {
|
|
719
|
+
throw clientError(
|
|
720
|
+
"session_failed",
|
|
721
|
+
"Assistant session returned invalid JSON",
|
|
722
|
+
true,
|
|
723
|
+
void 0,
|
|
724
|
+
{
|
|
725
|
+
cause: error
|
|
726
|
+
}
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
const session = parseSession(value);
|
|
730
|
+
this.#session = session;
|
|
731
|
+
this.#emit({
|
|
732
|
+
event: "session_started",
|
|
733
|
+
data: {
|
|
734
|
+
expiresAt: session.expiresAt,
|
|
735
|
+
...session.configuration ? { configuration: session.configuration } : {}
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
return session;
|
|
739
|
+
}
|
|
740
|
+
async #consume(response) {
|
|
741
|
+
let streamError;
|
|
742
|
+
try {
|
|
743
|
+
await consumeAssistantEvents(response, (event) => {
|
|
744
|
+
const clientEvent = toAssistantClientEvent(event);
|
|
745
|
+
this.#emit(clientEvent);
|
|
746
|
+
if (clientEvent.event !== "error" || typeof clientEvent.data.code !== "string") return;
|
|
747
|
+
streamError = {
|
|
748
|
+
code: clientEvent.data.code,
|
|
749
|
+
...typeof clientEvent.data.status === "number" ? { status: clientEvent.data.status } : {},
|
|
750
|
+
retryable: clientEvent.data.retryable === true
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (error instanceof AssistantTransportError) {
|
|
755
|
+
throw clientError(error.code, error.message, false, void 0, { cause: error });
|
|
756
|
+
}
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
if (streamError) {
|
|
760
|
+
throw clientError(
|
|
761
|
+
streamError.code,
|
|
762
|
+
`Assistant stream failed (${streamError.code})`,
|
|
763
|
+
streamError.retryable,
|
|
764
|
+
streamError.status
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async #request(input, init, failureCode) {
|
|
769
|
+
try {
|
|
770
|
+
return await this.#fetch(input, init);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
if (init.signal?.aborted) throw error;
|
|
773
|
+
throw clientError(failureCode, "Assistant request failed", true, void 0, { cause: error });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async #singleFlight(operation) {
|
|
777
|
+
if (this.#active) {
|
|
778
|
+
throw clientError("request_in_progress", "an assistant request is already in progress", true);
|
|
779
|
+
}
|
|
780
|
+
const controller = new AbortController();
|
|
781
|
+
this.#active = controller;
|
|
782
|
+
try {
|
|
783
|
+
await operation(controller.signal);
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (controller.signal.aborted) {
|
|
786
|
+
throw clientError("request_aborted", "assistant request was aborted", true, void 0, {
|
|
787
|
+
cause: error
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
throw error;
|
|
791
|
+
} finally {
|
|
792
|
+
this.#active = void 0;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
#emit(event) {
|
|
796
|
+
for (const listener of this.#listeners) {
|
|
797
|
+
try {
|
|
798
|
+
listener(event);
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
function createAssistantClient(options) {
|
|
805
|
+
return new DefaultAssistantClient(options);
|
|
806
|
+
}
|
|
807
|
+
function authorizedHeaders(token, accept) {
|
|
808
|
+
return {
|
|
809
|
+
Authorization: `Bearer ${token}`,
|
|
810
|
+
"Content-Type": "application/json",
|
|
811
|
+
Accept: accept
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function parseSession(value) {
|
|
815
|
+
if (!isRecord3(value) || !isRecord3(value.endpoints)) {
|
|
816
|
+
throw clientError("session_failed", "Assistant session response is invalid", true);
|
|
817
|
+
}
|
|
818
|
+
const interactions = value.endpoints.interactions;
|
|
819
|
+
if (typeof value.token !== "string" || typeof value.expiresAt !== "string" || typeof value.endpoints.turns !== "string" || typeof value.endpoints.toolConfirmations !== "string" || interactions !== void 0 && typeof interactions !== "string") {
|
|
820
|
+
throw clientError("session_failed", "Assistant session response is invalid", true);
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
token: value.token,
|
|
824
|
+
expiresAt: value.expiresAt,
|
|
825
|
+
endpoints: {
|
|
826
|
+
turns: value.endpoints.turns,
|
|
827
|
+
toolConfirmations: value.endpoints.toolConfirmations,
|
|
828
|
+
...typeof interactions === "string" ? { interactions } : {}
|
|
829
|
+
},
|
|
830
|
+
...isRecord3(value.configuration) ? { configuration: value.configuration } : {}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function isRecord3(value) {
|
|
834
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
835
|
+
}
|
|
836
|
+
async function readStableErrorCode(response) {
|
|
837
|
+
let value;
|
|
272
838
|
try {
|
|
273
|
-
|
|
839
|
+
value = await response.json();
|
|
274
840
|
} catch {
|
|
275
841
|
return void 0;
|
|
276
842
|
}
|
|
843
|
+
if (!isRecord3(value) || typeof value.code !== "string") return void 0;
|
|
844
|
+
return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
|
|
845
|
+
}
|
|
846
|
+
function clientError(code, message, retryable, status, options) {
|
|
847
|
+
return new AssistantClientError(
|
|
848
|
+
{ code, ...status === void 0 ? {} : { status }, retryable },
|
|
849
|
+
message,
|
|
850
|
+
options
|
|
851
|
+
);
|
|
277
852
|
}
|
|
278
853
|
|
|
854
|
+
// src/input-request-card.ts
|
|
855
|
+
function createInputRequestCard(options) {
|
|
856
|
+
const card = document.createElement("section");
|
|
857
|
+
card.className = "tool-proposal input-request";
|
|
858
|
+
card.dataset.kind = "input";
|
|
859
|
+
const form = document.createElement("form");
|
|
860
|
+
const heading = document.createElement("strong");
|
|
861
|
+
heading.textContent = options.message;
|
|
862
|
+
form.append(heading);
|
|
863
|
+
const properties = record(options.requestedSchema.properties);
|
|
864
|
+
const required = new Set(
|
|
865
|
+
Array.isArray(options.requestedSchema.required) ? options.requestedSchema.required.filter(
|
|
866
|
+
(value) => typeof value === "string"
|
|
867
|
+
) : []
|
|
868
|
+
);
|
|
869
|
+
const controls = /* @__PURE__ */ new Map();
|
|
870
|
+
const touched = /* @__PURE__ */ new WeakSet();
|
|
871
|
+
for (const [name, rawProperty] of Object.entries(properties)) {
|
|
872
|
+
const property = record(rawProperty);
|
|
873
|
+
const label = document.createElement("label");
|
|
874
|
+
const caption = document.createElement("span");
|
|
875
|
+
caption.textContent = typeof property.title === "string" && property.title ? property.title : humanize(name);
|
|
876
|
+
const control = createControl(name, property, required.has(name));
|
|
877
|
+
const markTouched = () => {
|
|
878
|
+
touched.add(control);
|
|
879
|
+
updateControlValidity(control, property, required.has(name), touched.has(control));
|
|
880
|
+
};
|
|
881
|
+
control.addEventListener("input", markTouched);
|
|
882
|
+
control.addEventListener("change", markTouched);
|
|
883
|
+
label.append(caption, control);
|
|
884
|
+
form.append(label);
|
|
885
|
+
controls.set(name, control);
|
|
886
|
+
}
|
|
887
|
+
const actions = document.createElement("div");
|
|
888
|
+
actions.className = "proposal-actions";
|
|
889
|
+
const submit = button(options.labels.submit, "submit");
|
|
890
|
+
const decline = button(options.labels.decline, "button");
|
|
891
|
+
const cancel = button(options.labels.cancel, "button");
|
|
892
|
+
const buttons = [submit, decline, cancel];
|
|
893
|
+
const resolve = async (response) => {
|
|
894
|
+
buttons.forEach((item) => {
|
|
895
|
+
item.disabled = true;
|
|
896
|
+
});
|
|
897
|
+
try {
|
|
898
|
+
await options.respond(response);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
buttons.forEach((item) => {
|
|
901
|
+
item.disabled = false;
|
|
902
|
+
});
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
form.addEventListener("submit", (event) => {
|
|
907
|
+
event.preventDefault();
|
|
908
|
+
for (const [name, control] of controls) {
|
|
909
|
+
updateControlValidity(
|
|
910
|
+
control,
|
|
911
|
+
record(properties[name]),
|
|
912
|
+
required.has(name),
|
|
913
|
+
touched.has(control)
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
if (!form.reportValidity()) return;
|
|
917
|
+
void resolve({
|
|
918
|
+
action: "accept",
|
|
919
|
+
content: readContent(controls, properties, required, touched)
|
|
920
|
+
}).catch(() => {
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
decline.addEventListener("click", () => {
|
|
924
|
+
void resolve({ action: "decline" }).catch(() => {
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
cancel.addEventListener("click", () => {
|
|
928
|
+
void resolve({ action: "cancel" }).catch(() => {
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
actions.append(submit, decline, cancel);
|
|
932
|
+
form.append(actions);
|
|
933
|
+
card.append(form);
|
|
934
|
+
return card;
|
|
935
|
+
}
|
|
936
|
+
function createControl(name, property, required) {
|
|
937
|
+
const choices = stringChoices(property);
|
|
938
|
+
if (choices || property.type === "array") {
|
|
939
|
+
const select = document.createElement("select");
|
|
940
|
+
select.name = name;
|
|
941
|
+
select.multiple = property.type === "array";
|
|
942
|
+
const availableChoices = choices ?? arrayChoices(property);
|
|
943
|
+
if (!select.multiple && (!required || !availableChoices.some((choice) => choice.value === property.default))) {
|
|
944
|
+
select.append(placeholderOption(availableChoices));
|
|
945
|
+
}
|
|
946
|
+
for (const choice of availableChoices) {
|
|
947
|
+
select.append(option(choice.title, choice.value));
|
|
948
|
+
}
|
|
949
|
+
applySelectDefault(select, property.default);
|
|
950
|
+
updateControlValidity(select, property, required, false);
|
|
951
|
+
return select;
|
|
952
|
+
}
|
|
953
|
+
const input = document.createElement("input");
|
|
954
|
+
input.name = name;
|
|
955
|
+
input.type = inputType(property);
|
|
956
|
+
input.required = required && inputNeedsNonEmptyValue(property);
|
|
957
|
+
if (property.type === "integer") input.step = "1";
|
|
958
|
+
if (typeof property.minimum === "number") input.min = String(property.minimum);
|
|
959
|
+
if (typeof property.maximum === "number") input.max = String(property.maximum);
|
|
960
|
+
if (typeof property.minLength === "number") input.minLength = property.minLength;
|
|
961
|
+
if (typeof property.maxLength === "number") input.maxLength = property.maxLength;
|
|
962
|
+
if (property.format === "date-time") input.step = "0.001";
|
|
963
|
+
applyInputDefault(input, property);
|
|
964
|
+
return input;
|
|
965
|
+
}
|
|
966
|
+
function readContent(controls, properties, required, touched) {
|
|
967
|
+
const content = {};
|
|
968
|
+
for (const [name, control] of controls) {
|
|
969
|
+
const property = record(properties[name]);
|
|
970
|
+
if (control instanceof HTMLSelectElement && control.multiple) {
|
|
971
|
+
const values = [...control.selectedOptions].map((option2) => option2.value);
|
|
972
|
+
if (shouldIncludeArray(property, required.has(name), touched.has(control), values.length)) {
|
|
973
|
+
content[name] = values;
|
|
974
|
+
}
|
|
975
|
+
} else if (control instanceof HTMLInputElement && control.type === "checkbox") {
|
|
976
|
+
if (required.has(name) || hasDefault(property) || touched.has(control)) {
|
|
977
|
+
content[name] = control.checked;
|
|
978
|
+
}
|
|
979
|
+
} else if (control instanceof HTMLSelectElement) {
|
|
980
|
+
if (!isPlaceholderSelected(control)) content[name] = control.value;
|
|
981
|
+
} else if (control.value !== "" || property.type === "string" && (required.has(name) || hasDefault(property) || touched.has(control))) {
|
|
982
|
+
if (property.format === "date-time") {
|
|
983
|
+
const value = dateTimeLocalToRfc3339(control.value);
|
|
984
|
+
if (value !== void 0) content[name] = value;
|
|
985
|
+
} else {
|
|
986
|
+
content[name] = property.type === "number" || property.type === "integer" ? Number(control.value) : control.value;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return content;
|
|
991
|
+
}
|
|
992
|
+
function updateControlValidity(control, property, required, touched) {
|
|
993
|
+
control.setCustomValidity("");
|
|
994
|
+
if (!(control instanceof HTMLSelectElement)) return;
|
|
995
|
+
if (!control.multiple) {
|
|
996
|
+
control.required = required;
|
|
997
|
+
if (required && isPlaceholderSelected(control)) {
|
|
998
|
+
control.setCustomValidity("Please select an option.");
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const count = control.selectedOptions.length;
|
|
1003
|
+
const included = shouldIncludeArray(property, required, touched, count);
|
|
1004
|
+
const minimum = integerBound(property.minItems);
|
|
1005
|
+
const maximum = integerBound(property.maxItems);
|
|
1006
|
+
control.required = included && minimum !== void 0 && minimum > 0;
|
|
1007
|
+
if (included && minimum !== void 0 && count < minimum) {
|
|
1008
|
+
control.setCustomValidity(`Select at least ${minimum} option${minimum === 1 ? "" : "s"}.`);
|
|
1009
|
+
} else if (included && maximum !== void 0 && count > maximum) {
|
|
1010
|
+
control.setCustomValidity(`Select at most ${maximum} option${maximum === 1 ? "" : "s"}.`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function shouldIncludeArray(property, required, touched, selectedCount) {
|
|
1014
|
+
return required || hasDefault(property) || touched || selectedCount > 0;
|
|
1015
|
+
}
|
|
1016
|
+
function hasDefault(property) {
|
|
1017
|
+
return Object.hasOwn(property, "default");
|
|
1018
|
+
}
|
|
1019
|
+
function integerBound(value) {
|
|
1020
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
1021
|
+
}
|
|
1022
|
+
function inputNeedsNonEmptyValue(property) {
|
|
1023
|
+
if (property.type === "number" || property.type === "integer") return true;
|
|
1024
|
+
if (property.type !== "string") return false;
|
|
1025
|
+
return property.format === "date" || property.format === "date-time" || property.format === "email" || property.format === "uri" || typeof property.minLength === "number" && property.minLength > 0;
|
|
1026
|
+
}
|
|
1027
|
+
function isPlaceholderSelected(select) {
|
|
1028
|
+
return select.selectedOptions[0]?.dataset.placeholder === "true";
|
|
1029
|
+
}
|
|
1030
|
+
function stringChoices(property) {
|
|
1031
|
+
if (Array.isArray(property.enum) && property.enum.every((value) => typeof value === "string")) {
|
|
1032
|
+
return property.enum.map((value) => ({ value, title: value }));
|
|
1033
|
+
}
|
|
1034
|
+
return titledChoices(property.oneOf) ?? titledChoices(property.anyOf);
|
|
1035
|
+
}
|
|
1036
|
+
function arrayChoices(property) {
|
|
1037
|
+
return stringChoices(record(property.items)) ?? [];
|
|
1038
|
+
}
|
|
1039
|
+
function inputType(property) {
|
|
1040
|
+
if (property.type === "boolean") return "checkbox";
|
|
1041
|
+
if (property.type === "number" || property.type === "integer") return "number";
|
|
1042
|
+
if (property.format === "date") return "date";
|
|
1043
|
+
if (property.format === "date-time") return "datetime-local";
|
|
1044
|
+
if (property.format === "email") return "email";
|
|
1045
|
+
if (property.format === "uri") return "url";
|
|
1046
|
+
return "text";
|
|
1047
|
+
}
|
|
1048
|
+
function titledChoices(value) {
|
|
1049
|
+
if (!Array.isArray(value)) return void 0;
|
|
1050
|
+
const choices = value.map((item) => record(item));
|
|
1051
|
+
if (!choices.every((item) => typeof item.const === "string")) return void 0;
|
|
1052
|
+
return choices.map((item) => ({
|
|
1053
|
+
value: String(item.const),
|
|
1054
|
+
title: typeof item.title === "string" ? item.title : String(item.const)
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
function applySelectDefault(select, value) {
|
|
1058
|
+
if (select.multiple) {
|
|
1059
|
+
const selected = new Set(
|
|
1060
|
+
Array.isArray(value) ? value.filter((item) => typeof item === "string") : []
|
|
1061
|
+
);
|
|
1062
|
+
for (const item of select.options) item.selected = selected.has(item.value);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (typeof value === "string") select.value = value;
|
|
1066
|
+
}
|
|
1067
|
+
function applyInputDefault(input, property) {
|
|
1068
|
+
const value = property.default;
|
|
1069
|
+
if (property.type === "boolean") {
|
|
1070
|
+
if (typeof value === "boolean") input.checked = value;
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (property.type === "number" || property.type === "integer") {
|
|
1074
|
+
if (typeof value === "number" && Number.isFinite(value)) input.value = String(value);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof value !== "string") return;
|
|
1078
|
+
if (property.format === "date-time") {
|
|
1079
|
+
input.value = rfc3339ToDateTimeLocal(value) ?? "";
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
input.value = value;
|
|
1083
|
+
}
|
|
1084
|
+
function rfc3339ToDateTimeLocal(value) {
|
|
1085
|
+
const instant = new Date(value);
|
|
1086
|
+
if (Number.isNaN(instant.valueOf())) return void 0;
|
|
1087
|
+
return [
|
|
1088
|
+
String(instant.getFullYear()).padStart(4, "0"),
|
|
1089
|
+
"-",
|
|
1090
|
+
twoDigits(instant.getMonth() + 1),
|
|
1091
|
+
"-",
|
|
1092
|
+
twoDigits(instant.getDate()),
|
|
1093
|
+
"T",
|
|
1094
|
+
twoDigits(instant.getHours()),
|
|
1095
|
+
":",
|
|
1096
|
+
twoDigits(instant.getMinutes()),
|
|
1097
|
+
":",
|
|
1098
|
+
twoDigits(instant.getSeconds()),
|
|
1099
|
+
".",
|
|
1100
|
+
String(instant.getMilliseconds()).padStart(3, "0")
|
|
1101
|
+
].join("");
|
|
1102
|
+
}
|
|
1103
|
+
function dateTimeLocalToRfc3339(value) {
|
|
1104
|
+
const instant = new Date(value);
|
|
1105
|
+
return Number.isNaN(instant.valueOf()) ? void 0 : instant.toISOString();
|
|
1106
|
+
}
|
|
1107
|
+
function twoDigits(value) {
|
|
1108
|
+
return String(value).padStart(2, "0");
|
|
1109
|
+
}
|
|
1110
|
+
function button(label, type) {
|
|
1111
|
+
const element = document.createElement("button");
|
|
1112
|
+
element.type = type;
|
|
1113
|
+
element.textContent = label;
|
|
1114
|
+
return element;
|
|
1115
|
+
}
|
|
1116
|
+
function option(label, value) {
|
|
1117
|
+
const element = document.createElement("option");
|
|
1118
|
+
element.textContent = label;
|
|
1119
|
+
element.value = value;
|
|
1120
|
+
return element;
|
|
1121
|
+
}
|
|
1122
|
+
function placeholderOption(choices) {
|
|
1123
|
+
let value = "";
|
|
1124
|
+
while (choices.some((choice) => choice.value === value)) value += "\0";
|
|
1125
|
+
const element = option("", value);
|
|
1126
|
+
element.dataset.placeholder = "true";
|
|
1127
|
+
return element;
|
|
1128
|
+
}
|
|
1129
|
+
function humanize(value) {
|
|
1130
|
+
const words = value.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ");
|
|
1131
|
+
return words ? `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}` : value;
|
|
1132
|
+
}
|
|
1133
|
+
function record(value) {
|
|
1134
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/interaction-card.ts
|
|
1138
|
+
function createInteractionCard(options) {
|
|
1139
|
+
const card = document.createElement("section");
|
|
1140
|
+
card.className = "tool-proposal";
|
|
1141
|
+
const label = document.createElement("strong");
|
|
1142
|
+
label.textContent = options.tool;
|
|
1143
|
+
card.append(label);
|
|
1144
|
+
if (options.arguments && Object.keys(options.arguments).length > 0) {
|
|
1145
|
+
card.append(createArgumentList(options.arguments));
|
|
1146
|
+
}
|
|
1147
|
+
const actions = document.createElement("div");
|
|
1148
|
+
actions.className = "proposal-actions";
|
|
1149
|
+
const buttons = [];
|
|
1150
|
+
for (const [labelText, action] of [
|
|
1151
|
+
[options.labels.accept, "accept"],
|
|
1152
|
+
[options.labels.decline, "decline"],
|
|
1153
|
+
[options.labels.cancel, "cancel"]
|
|
1154
|
+
]) {
|
|
1155
|
+
const button2 = document.createElement("button");
|
|
1156
|
+
button2.type = "button";
|
|
1157
|
+
button2.textContent = labelText;
|
|
1158
|
+
buttons.push(button2);
|
|
1159
|
+
button2.addEventListener("click", () => {
|
|
1160
|
+
for (const candidate of buttons) candidate.disabled = true;
|
|
1161
|
+
void options.respond(action).catch(() => {
|
|
1162
|
+
for (const candidate of buttons) candidate.disabled = false;
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
actions.append(button2);
|
|
1166
|
+
}
|
|
1167
|
+
card.append(actions);
|
|
1168
|
+
return card;
|
|
1169
|
+
}
|
|
1170
|
+
function createArgumentList(arguments_) {
|
|
1171
|
+
const details = document.createElement("dl");
|
|
1172
|
+
details.className = "proposal-arguments";
|
|
1173
|
+
for (const [name, value] of Object.entries(arguments_)) {
|
|
1174
|
+
const term = document.createElement("dt");
|
|
1175
|
+
term.textContent = humanizeArgumentName(name);
|
|
1176
|
+
const description = document.createElement("dd");
|
|
1177
|
+
description.textContent = formatArgumentValue(value);
|
|
1178
|
+
details.append(term, description);
|
|
1179
|
+
}
|
|
1180
|
+
return details;
|
|
1181
|
+
}
|
|
1182
|
+
function humanizeArgumentName(value) {
|
|
1183
|
+
const words = value.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ");
|
|
1184
|
+
return words ? `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}` : value;
|
|
1185
|
+
}
|
|
1186
|
+
function formatArgumentValue(value) {
|
|
1187
|
+
if (value === null) return "None";
|
|
1188
|
+
if (typeof value === "string") return value;
|
|
1189
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1190
|
+
return JSON.stringify(value) ?? "";
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/element-event-controller.ts
|
|
1194
|
+
var AssistantElementEventController = class {
|
|
1195
|
+
#host;
|
|
1196
|
+
#assistantBody;
|
|
1197
|
+
#completedTool;
|
|
1198
|
+
#proposalCards = /* @__PURE__ */ new Map();
|
|
1199
|
+
constructor(host) {
|
|
1200
|
+
this.#host = host;
|
|
1201
|
+
}
|
|
1202
|
+
handle(event) {
|
|
1203
|
+
if (event.event === "view_available") {
|
|
1204
|
+
this.#host.element.dispatchEvent(
|
|
1205
|
+
new CustomEvent("assistant-view-available", {
|
|
1206
|
+
detail: event.data
|
|
1207
|
+
})
|
|
1208
|
+
);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (event.event === "unrecognized") return;
|
|
1212
|
+
const { event: name, data } = event;
|
|
1213
|
+
if (name === "session_started") {
|
|
1214
|
+
if (isRecord4(data.configuration)) {
|
|
1215
|
+
this.#host.applyConfiguration(data.configuration);
|
|
1216
|
+
}
|
|
1217
|
+
this.#host.element.dispatchEvent(
|
|
1218
|
+
new CustomEvent("assistant-session-started", {
|
|
1219
|
+
detail: { expiresAt: String(data.expiresAt ?? "") }
|
|
1220
|
+
})
|
|
1221
|
+
);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (name === "session_expired") {
|
|
1225
|
+
this.#host.element.dispatchEvent(new CustomEvent("assistant-session-expired"));
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (name === "session_reset") {
|
|
1229
|
+
this.resetConversation();
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (name === "context_changed") {
|
|
1233
|
+
this.#host.element.dispatchEvent(
|
|
1234
|
+
new CustomEvent("assistant-context-changed", { detail: { context: data.context } })
|
|
1235
|
+
);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (name === "model_context_changed") {
|
|
1239
|
+
this.#host.element.dispatchEvent(
|
|
1240
|
+
new CustomEvent("assistant-model-context-changed", {
|
|
1241
|
+
detail: { modelContext: data.modelContext }
|
|
1242
|
+
})
|
|
1243
|
+
);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (name === "message_started") {
|
|
1247
|
+
this.#assistantBody = void 0;
|
|
1248
|
+
this.#host.appendMessage("user", String(data.message ?? ""));
|
|
1249
|
+
this.#host.element.dispatchEvent(new CustomEvent("assistant-message-started"));
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (name === "message_completed") {
|
|
1253
|
+
this.#assistantBody = void 0;
|
|
1254
|
+
this.#host.element.dispatchEvent(new CustomEvent("assistant-message-completed"));
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (name === "interaction_started") {
|
|
1258
|
+
this.#assistantBody = void 0;
|
|
1259
|
+
this.#completedTool = void 0;
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (name === "interaction_completed") {
|
|
1263
|
+
this.#finishInteraction(String(data.id ?? ""), String(data.action ?? "accept"));
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (name === "content") {
|
|
1267
|
+
const body = this.#assistantBody ?? this.#host.appendMessage("assistant", "");
|
|
1268
|
+
this.#assistantBody = body;
|
|
1269
|
+
if (body) body.textContent += String(data.delta ?? "");
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (name === "tool_proposed" || name === "interaction_proposed") {
|
|
1273
|
+
this.#appendToolProposal(
|
|
1274
|
+
String(data.id ?? ""),
|
|
1275
|
+
String(data.tool ?? ("title" in data ? data.title : void 0) ?? "Action"),
|
|
1276
|
+
isRecord4(data.arguments) ? data.arguments : void 0
|
|
1277
|
+
);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (name === "input_requested" && isRecord4(data.requestedSchema)) {
|
|
1281
|
+
this.#appendInputRequest(
|
|
1282
|
+
String(data.id ?? ""),
|
|
1283
|
+
String(data.message ?? "More information is required"),
|
|
1284
|
+
data.requestedSchema
|
|
1285
|
+
);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (name === "tool_completed") {
|
|
1289
|
+
this.#completedTool = data;
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (name === "error") {
|
|
1293
|
+
this.#host.dispatchError({
|
|
1294
|
+
code: String(data.code ?? "stream_failed"),
|
|
1295
|
+
...typeof data.status === "number" ? { status: data.status } : {},
|
|
1296
|
+
retryable: data.retryable === true
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
resetConversation() {
|
|
1301
|
+
this.#host.messages()?.replaceChildren();
|
|
1302
|
+
this.#proposalCards.clear();
|
|
1303
|
+
this.#assistantBody = void 0;
|
|
1304
|
+
this.#completedTool = void 0;
|
|
1305
|
+
this.#host.element.dispatchEvent(new CustomEvent("assistant-session-reset"));
|
|
1306
|
+
}
|
|
1307
|
+
#finishInteraction(id, action) {
|
|
1308
|
+
if (this.#completedTool && !this.#assistantBody) {
|
|
1309
|
+
this.#host.appendMessage("assistant", JSON.stringify(this.#completedTool.result ?? {}) ?? "");
|
|
1310
|
+
}
|
|
1311
|
+
this.#proposalCards.get(id)?.remove();
|
|
1312
|
+
this.#proposalCards.delete(id);
|
|
1313
|
+
if (action === "accept") {
|
|
1314
|
+
this.#host.element.dispatchEvent(
|
|
1315
|
+
new CustomEvent("assistant-tool-completed", { detail: { id } })
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
this.#host.element.dispatchEvent(
|
|
1319
|
+
new CustomEvent("assistant-interaction-resolved", { detail: { id, action } })
|
|
1320
|
+
);
|
|
1321
|
+
this.#assistantBody = void 0;
|
|
1322
|
+
this.#completedTool = void 0;
|
|
1323
|
+
}
|
|
1324
|
+
#appendToolProposal(id, tool, arguments_) {
|
|
1325
|
+
const messages = this.#host.messages();
|
|
1326
|
+
if (!messages || !id) return;
|
|
1327
|
+
this.#proposalCards.get(id)?.remove();
|
|
1328
|
+
const labels = this.#host.labels();
|
|
1329
|
+
const card = createInteractionCard({
|
|
1330
|
+
tool,
|
|
1331
|
+
...arguments_ ? { arguments: arguments_ } : {},
|
|
1332
|
+
labels: {
|
|
1333
|
+
accept: labels.confirm,
|
|
1334
|
+
decline: "Decline",
|
|
1335
|
+
cancel: labels.cancel
|
|
1336
|
+
},
|
|
1337
|
+
respond: (action) => this.#host.respond(id, { action })
|
|
1338
|
+
});
|
|
1339
|
+
messages.append(card);
|
|
1340
|
+
this.#proposalCards.set(id, card);
|
|
1341
|
+
this.#host.element.dispatchEvent(
|
|
1342
|
+
new CustomEvent("assistant-tool-proposed", { detail: { id, tool, arguments: arguments_ } })
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
#appendInputRequest(id, message, requestedSchema) {
|
|
1346
|
+
const messages = this.#host.messages();
|
|
1347
|
+
if (!messages || !id) return;
|
|
1348
|
+
this.#proposalCards.get(id)?.remove();
|
|
1349
|
+
const labels = this.#host.labels();
|
|
1350
|
+
const card = createInputRequestCard({
|
|
1351
|
+
message,
|
|
1352
|
+
requestedSchema,
|
|
1353
|
+
labels: {
|
|
1354
|
+
submit: labels.confirm,
|
|
1355
|
+
decline: "Decline",
|
|
1356
|
+
cancel: labels.cancel
|
|
1357
|
+
},
|
|
1358
|
+
respond: (response) => this.#host.respond(id, response)
|
|
1359
|
+
});
|
|
1360
|
+
messages.append(card);
|
|
1361
|
+
this.#proposalCards.set(id, card);
|
|
1362
|
+
this.#host.element.dispatchEvent(
|
|
1363
|
+
new CustomEvent("assistant-input-requested", {
|
|
1364
|
+
detail: { id, message, requestedSchema }
|
|
1365
|
+
})
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
function isRecord4(value) {
|
|
1370
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/element-styles.ts
|
|
1374
|
+
var ASSISTANT_ELEMENT_STYLES = `<style>
|
|
1375
|
+
:host { color-scheme: light dark; font-family: var(--ns-assistant-font-family, var(--ns-assistant-default-font-family)); font-size: var(--ns-assistant-base-font-size, var(--ns-assistant-default-base-font-size)); line-height: var(--ns-assistant-line-height, var(--ns-assistant-default-line-height)); z-index: var(--ns-assistant-z-index, var(--ns-assistant-default-z-index)); }
|
|
1376
|
+
:host([data-mode="floating"]) { position: fixed; bottom: 24px; right: 24px; }
|
|
1377
|
+
:host([data-mode="floating"][data-position="bottom-left"]) { right: auto; left: 24px; }
|
|
1378
|
+
.launcher { width: 54px; height: 54px; border: 0; border-radius: var(--ns-assistant-launcher-radius, var(--ns-assistant-default-launcher-radius)); color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); cursor: pointer; }
|
|
1379
|
+
.launcher img { width: 24px; height: 24px; object-fit: contain; }
|
|
1380
|
+
.panel { display: none; width: min(var(--ns-assistant-panel-width, var(--ns-assistant-default-panel-width)), calc(100vw - 32px)); min-height: var(--ns-assistant-min-height, var(--ns-assistant-default-min-height)); max-height: min(var(--ns-assistant-max-height, var(--ns-assistant-default-max-height)), calc(100vh - 32px)); overflow: hidden; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-panel, var(--ns-assistant-default-panel)); border-radius: var(--ns-assistant-panel-radius, var(--ns-assistant-default-panel-radius)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); }
|
|
1381
|
+
:host([open]) .panel { display: grid; grid-template-rows: auto auto 1fr auto; }
|
|
1382
|
+
:host([open]) .launcher { display: none; }
|
|
1383
|
+
header { display: flex; gap: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); align-items: center; padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.2); border-bottom: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
|
|
1384
|
+
header strong { flex: 1; }
|
|
1385
|
+
.brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
|
|
1386
|
+
button { font: inherit; }
|
|
1387
|
+
.close, .send { border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); cursor: pointer; }
|
|
1388
|
+
.close { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; font-size: 24px; }
|
|
1389
|
+
.welcome { padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.5); }
|
|
1390
|
+
.welcome h2 { margin: 0; font-size: 1.25em; }
|
|
1391
|
+
.welcome p { margin: 6px 0 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
|
|
1392
|
+
.suggested-prompts { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
|
|
1393
|
+
.suggested-prompts:empty { display: none; }
|
|
1394
|
+
.suggested-prompts button { padding: 7px 10px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: transparent; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 999px; cursor: pointer; }
|
|
1395
|
+
.messages { min-height: 120px; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); overflow: auto; }
|
|
1396
|
+
.message { width: fit-content; max-width: 84%; margin: 8px 0; padding: 10px 12px; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); white-space: pre-wrap; }
|
|
1397
|
+
.message .avatar { float: left; width: 24px; height: 24px; margin-right: 8px; border-radius: 50%; object-fit: cover; }
|
|
1398
|
+
.message time { display: block; margin-top: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .75em; }
|
|
1399
|
+
.message.user { margin-left: auto; color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
|
|
1400
|
+
.tool-proposal { display: grid; gap: 10px; margin: 10px 0; padding: 12px; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); }
|
|
1401
|
+
.proposal-arguments { display: grid; grid-template-columns: minmax(90px, auto) 1fr; gap: 4px 10px; margin: 0; font-size: .9em; }
|
|
1402
|
+
.proposal-arguments dt { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
|
|
1403
|
+
.proposal-arguments dd { margin: 0; overflow-wrap: anywhere; }
|
|
1404
|
+
.proposal-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: end; }
|
|
1405
|
+
.tool-proposal button { padding: 8px 10px; border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); cursor: pointer; }
|
|
1406
|
+
.tool-proposal button:first-of-type { color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
|
|
1407
|
+
.input-request form { display: grid; gap: 10px; padding: 0; border: 0; }
|
|
1408
|
+
.input-request label { display: grid; gap: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .9em; }
|
|
1409
|
+
.input-request input, .input-request select { width: 100%; box-sizing: border-box; padding: 8px 10px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: var(--ns-assistant-input-radius, var(--ns-assistant-default-input-radius)); font: inherit; }
|
|
1410
|
+
.input-request input[type="checkbox"] { width: auto; justify-self: start; }
|
|
1411
|
+
form { display: flex; gap: 8px; align-items: end; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); border-top: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
|
|
1412
|
+
textarea { flex: 1; resize: none; padding: 10px 12px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); border: 0; border-radius: var(--ns-assistant-input-radius, var(--ns-assistant-default-input-radius)); font: inherit; }
|
|
1413
|
+
textarea:focus-visible, button:focus-visible { outline: 3px solid var(--ns-assistant-focus, var(--ns-assistant-default-focus)); outline-offset: 2px; }
|
|
1414
|
+
.send { padding: 10px 14px; color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
|
|
1415
|
+
.legal { display: flex; gap: 12px; justify-content: center; padding: 0 12px 10px; font-size: .8em; }
|
|
1416
|
+
.legal:empty { display: none; }
|
|
1417
|
+
.legal a { color: var(--ns-assistant-link, var(--ns-assistant-default-link)); }
|
|
1418
|
+
:host([data-density="compact"]) .welcome { padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
|
|
1419
|
+
@media (max-width: 640px) { :host([mobile-fullscreen][open]) { inset: 0; } :host([mobile-fullscreen][open]) .panel { width: 100vw; min-height: 100vh; max-height: 100vh; border-radius: 0; } }
|
|
1420
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
|
|
1421
|
+
@media (max-width: 560px) { :host([data-mode="floating"]) { inset: auto 12px 12px; } :host([open]) .panel { width: calc(100vw - 24px); max-height: calc(100vh - 24px); } }
|
|
1422
|
+
</style>`;
|
|
1423
|
+
|
|
279
1424
|
// src/element.ts
|
|
280
1425
|
var ASSISTANT_TAG_NAME = "noodle-assistant";
|
|
281
1426
|
var HTMLElementBase = globalThis.HTMLElement ?? class {
|
|
@@ -283,11 +1428,29 @@ var HTMLElementBase = globalThis.HTMLElement ?? class {
|
|
|
283
1428
|
var NoodleAssistantElement = class extends HTMLElementBase {
|
|
284
1429
|
static observedAttributes = ["open", "theme"];
|
|
285
1430
|
#appearance = resolveAppearance();
|
|
286
|
-
#
|
|
1431
|
+
#client;
|
|
1432
|
+
#clientEndpoint = "";
|
|
1433
|
+
#unsubscribeClient;
|
|
287
1434
|
#messages;
|
|
288
1435
|
#media;
|
|
289
1436
|
#context;
|
|
1437
|
+
#modelContext;
|
|
290
1438
|
sessionEndpoint = "";
|
|
1439
|
+
#events = new AssistantElementEventController({
|
|
1440
|
+
element: this,
|
|
1441
|
+
messages: () => this.#messages,
|
|
1442
|
+
appendMessage: (role, text) => this.#appendMessage(role, text),
|
|
1443
|
+
applyConfiguration: (configuration) => {
|
|
1444
|
+
this.#appearance = resolveAppearance(configuration);
|
|
1445
|
+
this.#render();
|
|
1446
|
+
},
|
|
1447
|
+
labels: () => ({
|
|
1448
|
+
confirm: this.#appearance.labels.confirm,
|
|
1449
|
+
cancel: this.#appearance.labels.cancel
|
|
1450
|
+
}),
|
|
1451
|
+
respond: (id, response) => this.respond(id, response),
|
|
1452
|
+
dispatchError: (detail) => this.#dispatchError(detail)
|
|
1453
|
+
});
|
|
291
1454
|
connectedCallback() {
|
|
292
1455
|
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
293
1456
|
if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
|
|
@@ -300,6 +1463,11 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
300
1463
|
this.dispatchEvent(new CustomEvent("assistant-ready"));
|
|
301
1464
|
}
|
|
302
1465
|
disconnectedCallback() {
|
|
1466
|
+
this.#client?.abort();
|
|
1467
|
+
this.#unsubscribeClient?.();
|
|
1468
|
+
this.#client = void 0;
|
|
1469
|
+
this.#clientEndpoint = "";
|
|
1470
|
+
this.#unsubscribeClient = void 0;
|
|
303
1471
|
this.#media?.removeEventListener("change", this.#handleSystemTheme);
|
|
304
1472
|
document.removeEventListener("keydown", this.#handleDocumentKeydown);
|
|
305
1473
|
document.removeEventListener("pointerdown", this.#handleDocumentPointerDown);
|
|
@@ -322,71 +1490,44 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
322
1490
|
this.shadowRoot?.querySelector("textarea")?.focus();
|
|
323
1491
|
}
|
|
324
1492
|
resetSession() {
|
|
325
|
-
this.#
|
|
326
|
-
|
|
327
|
-
|
|
1493
|
+
if (this.#client) {
|
|
1494
|
+
this.#client.resetSession();
|
|
1495
|
+
} else {
|
|
1496
|
+
this.#events.resetConversation();
|
|
1497
|
+
}
|
|
328
1498
|
}
|
|
329
1499
|
updateContext(context) {
|
|
330
1500
|
this.#context = { ...context };
|
|
331
|
-
|
|
1501
|
+
if (this.#client) {
|
|
1502
|
+
this.#client.updateContext(this.#context);
|
|
1503
|
+
} else {
|
|
1504
|
+
this.dispatchEvent(
|
|
1505
|
+
new CustomEvent("assistant-context-changed", { detail: { context: this.#context } })
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/** Replace the compact renderer summary attached to subsequent message turns. */
|
|
1510
|
+
updateModelContext(update) {
|
|
1511
|
+
this.#modelContext = copyAssistantModelContext(update);
|
|
1512
|
+
if (this.#client) {
|
|
1513
|
+
this.#client.updateModelContext(this.#modelContext);
|
|
1514
|
+
} else {
|
|
1515
|
+
this.dispatchEvent(
|
|
1516
|
+
new CustomEvent("assistant-model-context-changed", {
|
|
1517
|
+
detail: { modelContext: this.#modelContext }
|
|
1518
|
+
})
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
332
1521
|
}
|
|
333
1522
|
async sendMessage(text) {
|
|
334
1523
|
const message = text.trim();
|
|
335
1524
|
if (!message) return;
|
|
336
|
-
if (!this.#session) this.#session = await this.#createSession();
|
|
337
|
-
this.#appendMessage("user", message);
|
|
338
|
-
this.dispatchEvent(new CustomEvent("assistant-message-started"));
|
|
339
|
-
await this.#sendTurn(message, true);
|
|
340
|
-
}
|
|
341
|
-
async #sendTurn(message, mayRetry) {
|
|
342
|
-
if (!this.#session) this.#session = await this.#createSession();
|
|
343
|
-
const response = await fetch(this.#session.endpoints.turns, {
|
|
344
|
-
method: "POST",
|
|
345
|
-
headers: {
|
|
346
|
-
Authorization: `Bearer ${this.#session.token}`,
|
|
347
|
-
"Content-Type": "application/json",
|
|
348
|
-
Accept: "text/event-stream"
|
|
349
|
-
},
|
|
350
|
-
body: JSON.stringify({ message })
|
|
351
|
-
});
|
|
352
|
-
if (response.status === 401 && mayRetry) {
|
|
353
|
-
this.#session = void 0;
|
|
354
|
-
this.dispatchEvent(new CustomEvent("assistant-session-expired"));
|
|
355
|
-
await this.#sendTurn(message, false);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (!response.ok) {
|
|
359
|
-
this.#dispatchError({ code: "turn_failed", status: response.status, retryable: false });
|
|
360
|
-
throw new Error(`Assistant turn failed (${response.status})`);
|
|
361
|
-
}
|
|
362
|
-
let assistantBody;
|
|
363
1525
|
try {
|
|
364
|
-
await
|
|
365
|
-
if (event.event === "content") {
|
|
366
|
-
const body = assistantBody ?? this.#appendMessage("assistant", "");
|
|
367
|
-
assistantBody = body;
|
|
368
|
-
if (body) body.textContent += String(event.data.delta ?? "");
|
|
369
|
-
}
|
|
370
|
-
if (event.event === "tool_proposed") {
|
|
371
|
-
this.#appendToolProposal(
|
|
372
|
-
String(event.data.id ?? ""),
|
|
373
|
-
String(event.data.tool ?? "Action")
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
if (event.event === "error") {
|
|
377
|
-
this.#dispatchError({
|
|
378
|
-
code: String(event.data.code ?? "stream_failed"),
|
|
379
|
-
retryable: event.data.retryable === true
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
});
|
|
1526
|
+
await this.#ensureClient().sendMessage(message);
|
|
383
1527
|
} catch (error) {
|
|
384
|
-
|
|
385
|
-
this.#dispatchError({ code: error.code, retryable: false });
|
|
386
|
-
}
|
|
1528
|
+
this.#dispatchClientError(error, "turn_failed");
|
|
387
1529
|
throw error;
|
|
388
1530
|
}
|
|
389
|
-
this.dispatchEvent(new CustomEvent("assistant-message-completed"));
|
|
390
1531
|
}
|
|
391
1532
|
/**
|
|
392
1533
|
* Every listener the element attaches to its own DOM must go through this guard: internal UI
|
|
@@ -399,46 +1540,15 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
399
1540
|
});
|
|
400
1541
|
}
|
|
401
1542
|
async confirmTool(id) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
"Content-Type": "application/json",
|
|
411
|
-
Accept: "text/event-stream"
|
|
412
|
-
},
|
|
413
|
-
body: JSON.stringify({ id })
|
|
414
|
-
});
|
|
415
|
-
if (!response.ok) {
|
|
416
|
-
const expired = response.status === 401 || response.status === 409;
|
|
417
|
-
if (response.status === 401) {
|
|
418
|
-
this.#session = void 0;
|
|
419
|
-
this.dispatchEvent(new CustomEvent("assistant-session-expired"));
|
|
420
|
-
}
|
|
421
|
-
this.#dispatchError({
|
|
422
|
-
code: expired ? "confirmation_expired" : "confirmation_failed",
|
|
423
|
-
status: response.status,
|
|
424
|
-
retryable: false
|
|
425
|
-
});
|
|
426
|
-
throw new Error(`Assistant confirmation failed (${response.status})`);
|
|
427
|
-
}
|
|
428
|
-
let completed;
|
|
429
|
-
let narrationBody;
|
|
430
|
-
await consumeAssistantEvents(response, (event) => {
|
|
431
|
-
if (event.event === "tool_completed") completed = event.data;
|
|
432
|
-
if (event.event === "content") {
|
|
433
|
-
const body = narrationBody ?? this.#appendMessage("assistant", "");
|
|
434
|
-
narrationBody = body;
|
|
435
|
-
if (body) body.textContent += String(event.data.delta ?? "");
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
if (completed && !narrationBody) {
|
|
439
|
-
this.#appendMessage("assistant", JSON.stringify(completed.result ?? {}));
|
|
1543
|
+
await this.respond(id, { action: "accept" });
|
|
1544
|
+
}
|
|
1545
|
+
async respond(id, response) {
|
|
1546
|
+
try {
|
|
1547
|
+
await this.#ensureClient().respond(id, response);
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
this.#dispatchClientError(error, "confirmation_failed");
|
|
1550
|
+
throw error;
|
|
440
1551
|
}
|
|
441
|
-
this.dispatchEvent(new CustomEvent("assistant-tool-completed", { detail: { id } }));
|
|
442
1552
|
}
|
|
443
1553
|
#handleSystemTheme = () => this.#applyTheme();
|
|
444
1554
|
#handleDocumentKeydown = (event) => {
|
|
@@ -451,39 +1561,20 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
451
1561
|
this.close();
|
|
452
1562
|
}
|
|
453
1563
|
};
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (!response.ok) {
|
|
469
|
-
this.#dispatchError({ code: "session_failed", status: response.status, retryable: true });
|
|
470
|
-
throw new Error(`Assistant session failed (${response.status})`);
|
|
471
|
-
}
|
|
472
|
-
let session;
|
|
473
|
-
try {
|
|
474
|
-
session = await response.json();
|
|
475
|
-
} catch (error) {
|
|
476
|
-
this.#dispatchError({ code: "session_failed", retryable: true });
|
|
477
|
-
throw error;
|
|
478
|
-
}
|
|
479
|
-
if (session.configuration) {
|
|
480
|
-
this.#appearance = resolveAppearance(session.configuration);
|
|
481
|
-
this.#render();
|
|
482
|
-
}
|
|
483
|
-
this.dispatchEvent(
|
|
484
|
-
new CustomEvent("assistant-session-started", { detail: { expiresAt: session.expiresAt } })
|
|
485
|
-
);
|
|
486
|
-
return session;
|
|
1564
|
+
#ensureClient() {
|
|
1565
|
+
if (this.#client && this.#clientEndpoint === this.sessionEndpoint) return this.#client;
|
|
1566
|
+
this.#client?.abort();
|
|
1567
|
+
this.#unsubscribeClient?.();
|
|
1568
|
+
const client = createAssistantClient({
|
|
1569
|
+
sessionEndpoint: this.sessionEndpoint,
|
|
1570
|
+
...this.#context ? { context: this.#context } : {},
|
|
1571
|
+
clientContext: browserClientContext,
|
|
1572
|
+
...this.#modelContext ? { modelContext: this.#modelContext } : {}
|
|
1573
|
+
});
|
|
1574
|
+
this.#client = client;
|
|
1575
|
+
this.#clientEndpoint = this.sessionEndpoint;
|
|
1576
|
+
this.#unsubscribeClient = client.subscribe((event) => this.#events.handle(event));
|
|
1577
|
+
return client;
|
|
487
1578
|
}
|
|
488
1579
|
#appendMessage(role, text) {
|
|
489
1580
|
if (!this.#messages) return void 0;
|
|
@@ -514,31 +1605,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
514
1605
|
#dispatchError(detail) {
|
|
515
1606
|
this.dispatchEvent(new CustomEvent("assistant-error", { detail }));
|
|
516
1607
|
}
|
|
517
|
-
#
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const label = document.createElement("strong");
|
|
522
|
-
label.textContent = tool;
|
|
523
|
-
const confirm = document.createElement("button");
|
|
524
|
-
confirm.type = "button";
|
|
525
|
-
confirm.textContent = this.#appearance.labels.confirm;
|
|
526
|
-
confirm.addEventListener("click", () => {
|
|
527
|
-
confirm.disabled = true;
|
|
528
|
-
void this.confirmTool(id).catch(() => {
|
|
529
|
-
confirm.disabled = false;
|
|
530
|
-
this.dispatchEvent(
|
|
531
|
-
new CustomEvent("assistant-error", { detail: { code: "confirmation_failed" } })
|
|
532
|
-
);
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
const cancel = document.createElement("button");
|
|
536
|
-
cancel.type = "button";
|
|
537
|
-
cancel.textContent = this.#appearance.labels.cancel;
|
|
538
|
-
cancel.addEventListener("click", () => card.remove());
|
|
539
|
-
card.append(label, confirm, cancel);
|
|
540
|
-
this.#messages.append(card);
|
|
541
|
-
this.dispatchEvent(new CustomEvent("assistant-tool-proposed", { detail: { id, tool } }));
|
|
1608
|
+
#dispatchClientError(error, fallbackCode) {
|
|
1609
|
+
this.#dispatchError(
|
|
1610
|
+
error instanceof AssistantClientError ? error.detail : { code: fallbackCode, retryable: false }
|
|
1611
|
+
);
|
|
542
1612
|
}
|
|
543
1613
|
#resolvedMode() {
|
|
544
1614
|
const configured = this.getAttribute("theme") ?? this.#appearance.theme.defaultMode;
|
|
@@ -579,6 +1649,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
579
1649
|
}
|
|
580
1650
|
#render() {
|
|
581
1651
|
if (!this.shadowRoot) return;
|
|
1652
|
+
const conversation = this.#messages ? [...this.#messages.childNodes] : [];
|
|
582
1653
|
const appearance = this.#appearance;
|
|
583
1654
|
this.setAttribute("role", "complementary");
|
|
584
1655
|
this.setAttribute("aria-label", appearance.brand.name);
|
|
@@ -588,7 +1659,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
588
1659
|
this.dataset.position = appearance.layout.position;
|
|
589
1660
|
this.dataset.density = appearance.layout.density;
|
|
590
1661
|
this.toggleAttribute("mobile-fullscreen", appearance.layout.mobileFullscreen);
|
|
591
|
-
this.shadowRoot.innerHTML = `${
|
|
1662
|
+
this.shadowRoot.innerHTML = `${ASSISTANT_ELEMENT_STYLES}
|
|
592
1663
|
<button class="launcher" type="button" aria-label="${escapeAttribute(appearance.labels.open)}"><slot name="launcher-icon">\u2726</slot></button>
|
|
593
1664
|
<section class="panel" aria-label="${escapeAttribute(appearance.brand.name)}">
|
|
594
1665
|
<header><slot name="header-leading"></slot><img class="brand-logo" alt=""><strong></strong><slot name="header-actions"></slot><button class="close" type="button" aria-label="${escapeAttribute(appearance.labels.close)}">\xD7</button></header>
|
|
@@ -627,11 +1698,11 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
627
1698
|
queryRequired(this.shadowRoot, ".send").textContent = appearance.labels.send;
|
|
628
1699
|
const prompts = queryRequired(this.shadowRoot, ".suggested-prompts");
|
|
629
1700
|
for (const prompt of appearance.suggestedPrompts) {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
prompts.append(
|
|
1701
|
+
const button2 = document.createElement("button");
|
|
1702
|
+
button2.type = "button";
|
|
1703
|
+
button2.textContent = prompt;
|
|
1704
|
+
button2.addEventListener("click", () => this.#sendGuarded(prompt));
|
|
1705
|
+
prompts.append(button2);
|
|
635
1706
|
}
|
|
636
1707
|
if (!appearance.behavior.showLauncher)
|
|
637
1708
|
queryRequired(this.shadowRoot, ".launcher").hidden = true;
|
|
@@ -651,6 +1722,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
|
|
|
651
1722
|
legal.append(link);
|
|
652
1723
|
}
|
|
653
1724
|
this.#messages = this.shadowRoot.querySelector(".messages") ?? void 0;
|
|
1725
|
+
this.#messages?.append(...conversation);
|
|
654
1726
|
this.shadowRoot.querySelector(".launcher")?.addEventListener("click", () => this.open());
|
|
655
1727
|
this.shadowRoot.querySelector(".close")?.addEventListener("click", () => this.close());
|
|
656
1728
|
this.shadowRoot.querySelector("form")?.addEventListener("submit", (event) => {
|
|
@@ -683,47 +1755,6 @@ function queryRequired(root, selector) {
|
|
|
683
1755
|
if (!element) throw new Error(`assistant renderer is missing ${selector}`);
|
|
684
1756
|
return element;
|
|
685
1757
|
}
|
|
686
|
-
var styles = `<style>
|
|
687
|
-
:host { color-scheme: light dark; font-family: var(--ns-assistant-font-family, var(--ns-assistant-default-font-family)); font-size: var(--ns-assistant-base-font-size, var(--ns-assistant-default-base-font-size)); line-height: var(--ns-assistant-line-height, var(--ns-assistant-default-line-height)); z-index: var(--ns-assistant-z-index, var(--ns-assistant-default-z-index)); }
|
|
688
|
-
:host([data-mode="floating"]) { position: fixed; bottom: 24px; right: 24px; }
|
|
689
|
-
:host([data-mode="floating"][data-position="bottom-left"]) { right: auto; left: 24px; }
|
|
690
|
-
.launcher { width: 54px; height: 54px; border: 0; border-radius: var(--ns-assistant-launcher-radius, var(--ns-assistant-default-launcher-radius)); color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); cursor: pointer; }
|
|
691
|
-
.launcher img { width: 24px; height: 24px; object-fit: contain; }
|
|
692
|
-
.panel { display: none; width: min(var(--ns-assistant-panel-width, var(--ns-assistant-default-panel-width)), calc(100vw - 32px)); min-height: var(--ns-assistant-min-height, var(--ns-assistant-default-min-height)); max-height: min(var(--ns-assistant-max-height, var(--ns-assistant-default-max-height)), calc(100vh - 32px)); overflow: hidden; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-panel, var(--ns-assistant-default-panel)); border-radius: var(--ns-assistant-panel-radius, var(--ns-assistant-default-panel-radius)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); }
|
|
693
|
-
:host([open]) .panel { display: grid; grid-template-rows: auto auto 1fr auto; }
|
|
694
|
-
:host([open]) .launcher { display: none; }
|
|
695
|
-
header { display: flex; gap: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); align-items: center; padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.2); border-bottom: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
|
|
696
|
-
header strong { flex: 1; }
|
|
697
|
-
.brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
|
|
698
|
-
button { font: inherit; }
|
|
699
|
-
.close, .send { border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); cursor: pointer; }
|
|
700
|
-
.close { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; font-size: 24px; }
|
|
701
|
-
.welcome { padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.5); }
|
|
702
|
-
.welcome h2 { margin: 0; font-size: 1.25em; }
|
|
703
|
-
.welcome p { margin: 6px 0 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
|
|
704
|
-
.suggested-prompts { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
|
|
705
|
-
.suggested-prompts:empty { display: none; }
|
|
706
|
-
.suggested-prompts button { padding: 7px 10px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: transparent; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 999px; cursor: pointer; }
|
|
707
|
-
.messages { min-height: 120px; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); overflow: auto; }
|
|
708
|
-
.message { width: fit-content; max-width: 84%; margin: 8px 0; padding: 10px 12px; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); white-space: pre-wrap; }
|
|
709
|
-
.message .avatar { float: left; width: 24px; height: 24px; margin-right: 8px; border-radius: 50%; object-fit: cover; }
|
|
710
|
-
.message time { display: block; margin-top: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .75em; }
|
|
711
|
-
.message.user { margin-left: auto; color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
|
|
712
|
-
.tool-proposal { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; align-items: center; margin: 10px 0; padding: 12px; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); }
|
|
713
|
-
.tool-proposal button { padding: 8px 10px; border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); cursor: pointer; }
|
|
714
|
-
.tool-proposal button:first-of-type { color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
|
|
715
|
-
form { display: flex; gap: 8px; align-items: end; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); border-top: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
|
|
716
|
-
textarea { flex: 1; resize: none; padding: 10px 12px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); border: 0; border-radius: var(--ns-assistant-input-radius, var(--ns-assistant-default-input-radius)); font: inherit; }
|
|
717
|
-
textarea:focus-visible, button:focus-visible { outline: 3px solid var(--ns-assistant-focus, var(--ns-assistant-default-focus)); outline-offset: 2px; }
|
|
718
|
-
.send { padding: 10px 14px; color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
|
|
719
|
-
.legal { display: flex; gap: 12px; justify-content: center; padding: 0 12px 10px; font-size: .8em; }
|
|
720
|
-
.legal:empty { display: none; }
|
|
721
|
-
.legal a { color: var(--ns-assistant-link, var(--ns-assistant-default-link)); }
|
|
722
|
-
:host([data-density="compact"]) .welcome { padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
|
|
723
|
-
@media (max-width: 640px) { :host([mobile-fullscreen][open]) { inset: 0; } :host([mobile-fullscreen][open]) .panel { width: 100vw; min-height: 100vh; max-height: 100vh; border-radius: 0; } }
|
|
724
|
-
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
|
|
725
|
-
@media (max-width: 560px) { :host([data-mode="floating"]) { inset: auto 12px 12px; } :host([open]) .panel { width: calc(100vw - 24px); max-height: calc(100vh - 24px); } }
|
|
726
|
-
</style>`;
|
|
727
1758
|
|
|
728
1759
|
// src/react.ts
|
|
729
1760
|
var NoodleAssistant = (0, import_react.forwardRef)(
|