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