@noodleseed/assistant 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,698 @@
1
+ // src/appearance.ts
2
+ var common = {
3
+ fontFamily: "inherit",
4
+ monoFontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
5
+ baseFontSize: 15,
6
+ messageFontSize: 15,
7
+ lineHeight: 1.5,
8
+ panelRadius: 20,
9
+ cardRadius: 14,
10
+ inputRadius: 14,
11
+ buttonRadius: 12,
12
+ launcherRadius: 999,
13
+ spacing: 12,
14
+ backdropBlur: 18,
15
+ motionScale: 1
16
+ };
17
+ var DEFAULT_APPEARANCE = {
18
+ brand: { name: "Assistant", logo: {}, avatar: {}, mark: {} },
19
+ theme: {
20
+ defaultMode: "auto",
21
+ light: {
22
+ ...common,
23
+ page: "#F7F7F8",
24
+ panel: "#FFFFFF",
25
+ elevated: "#FFFFFF",
26
+ input: "#F1F1F3",
27
+ hover: "#ECECEF",
28
+ selected: "#E4E2FF",
29
+ text: "#18181B",
30
+ mutedText: "#71717A",
31
+ accent: "#5B4CF0",
32
+ accentText: "#FFFFFF",
33
+ link: "#4A3BD5",
34
+ divider: "#E4E4E7",
35
+ focus: "#5B4CF0",
36
+ success: "#16835D",
37
+ warning: "#A45B00",
38
+ danger: "#C92A45",
39
+ overlay: "#18181B66",
40
+ code: "#F1F1F3",
41
+ widgetFrame: "#E4E4E7",
42
+ shadow: "0 24px 80px rgba(24, 24, 27, 0.18)"
43
+ },
44
+ dark: {
45
+ ...common,
46
+ page: "#111113",
47
+ panel: "#19191D",
48
+ elevated: "#222228",
49
+ input: "#26262C",
50
+ hover: "#303038",
51
+ selected: "#37315F",
52
+ text: "#FAFAFA",
53
+ mutedText: "#A1A1AA",
54
+ accent: "#A99FFF",
55
+ accentText: "#17131F",
56
+ link: "#B9B1FF",
57
+ divider: "#34343B",
58
+ focus: "#B9B1FF",
59
+ success: "#58C69A",
60
+ warning: "#F2B45F",
61
+ danger: "#FF8094",
62
+ overlay: "#00000099",
63
+ code: "#26262C",
64
+ widgetFrame: "#34343B",
65
+ shadow: "0 24px 90px rgba(0, 0, 0, 0.55)"
66
+ }
67
+ },
68
+ layout: {
69
+ mode: "floating",
70
+ position: "bottom-right",
71
+ panelWidth: 400,
72
+ panelMinHeight: 420,
73
+ panelMaxHeight: 720,
74
+ zIndex: 2147483e3,
75
+ density: "comfortable",
76
+ mobileFullscreen: true
77
+ },
78
+ behavior: {
79
+ startOpen: false,
80
+ closeOnEscape: true,
81
+ closeOnOutsideClick: false,
82
+ showLauncher: true,
83
+ showHeader: true,
84
+ showAvatars: true,
85
+ showTimestamps: false
86
+ },
87
+ labels: {
88
+ welcomeHeading: "How can I help?",
89
+ welcomeMessage: "",
90
+ composerPlaceholder: "Ask a question\u2026",
91
+ send: "Send",
92
+ stop: "Stop",
93
+ close: "Close assistant",
94
+ open: "Open assistant",
95
+ confirm: "Confirm",
96
+ cancel: "Cancel",
97
+ retry: "Try again",
98
+ unavailable: "The assistant is temporarily unavailable.",
99
+ sessionExpired: "Your assistant session expired. Reopen it to continue."
100
+ },
101
+ suggestedPrompts: [],
102
+ locale: "en",
103
+ direction: "auto"
104
+ };
105
+ function resolveAppearance(input = {}) {
106
+ const branding = input.branding;
107
+ const assistant = input.assistant;
108
+ return {
109
+ brand: {
110
+ ...DEFAULT_APPEARANCE.brand,
111
+ ...branding?.name ? { name: branding.name } : {},
112
+ logo: themedAsset(branding?.logo),
113
+ avatar: themedAsset(branding?.avatar),
114
+ mark: themedAsset(branding?.mark)
115
+ },
116
+ theme: {
117
+ defaultMode: branding?.colorScheme ?? DEFAULT_APPEARANCE.theme.defaultMode,
118
+ light: resolveTheme("light", branding),
119
+ dark: resolveTheme("dark", branding)
120
+ },
121
+ layout: {
122
+ ...DEFAULT_APPEARANCE.layout,
123
+ ...branding?.density ? { density: branding.density } : {},
124
+ ...assistant?.layout
125
+ },
126
+ behavior: { ...DEFAULT_APPEARANCE.behavior, ...assistant?.behavior },
127
+ labels: { ...DEFAULT_APPEARANCE.labels, ...assistant?.labels },
128
+ suggestedPrompts: assistant?.suggestedPrompts ?? DEFAULT_APPEARANCE.suggestedPrompts,
129
+ ...assistant?.privacyUrl ? { privacyUrl: assistant.privacyUrl } : {},
130
+ ...assistant?.termsUrl ? { termsUrl: assistant.termsUrl } : {},
131
+ locale: assistant?.locale ?? DEFAULT_APPEARANCE.locale,
132
+ direction: assistant?.direction ?? DEFAULT_APPEARANCE.direction
133
+ };
134
+ }
135
+ function themedAsset(asset) {
136
+ return asset ? { light: asset.uri, dark: asset.darkUri ?? asset.uri } : {};
137
+ }
138
+ function resolveTheme(mode, branding) {
139
+ const defaults = DEFAULT_APPEARANCE.theme[mode];
140
+ const surface = mode === "light" ? branding?.surface : branding?.surfaceDark;
141
+ const portable = branding?.theme?.[mode];
142
+ const radius = branding?.radius ? radiusValue(branding.radius) : void 0;
143
+ return {
144
+ ...defaults,
145
+ ...branding?.accent ? { accent: branding.accent } : {},
146
+ ...surface ? { page: surface, panel: surface } : {},
147
+ ...mapPortableTheme(portable),
148
+ ...radius === void 0 ? {} : {
149
+ panelRadius: radius,
150
+ cardRadius: radius,
151
+ inputRadius: Math.max(0, radius - 2),
152
+ buttonRadius: Math.max(0, radius - 2)
153
+ },
154
+ ...branding?.typography === "serif" ? { fontFamily: "ui-serif, Georgia, serif" } : branding?.typography === "mono" ? { fontFamily: defaults.monoFontFamily } : {}
155
+ };
156
+ }
157
+ function mapPortableTheme(input) {
158
+ if (!input) return {};
159
+ return {
160
+ ...input.surface ? { page: input.surface, panel: input.surface } : {},
161
+ ...input.surfaceRaised ? { elevated: input.surfaceRaised } : {},
162
+ ...input.surfaceMuted ? { input: input.surfaceMuted, hover: input.surfaceMuted, selected: input.surfaceMuted } : {},
163
+ ...input.text ? { text: input.text } : {},
164
+ ...input.textMuted ? { mutedText: input.textMuted } : {},
165
+ ...input.accent ? { accent: input.accent } : {},
166
+ ...input.accentText ? { accentText: input.accentText } : {},
167
+ ...input.link ? { link: input.link } : {},
168
+ ...input.border ? { divider: input.border, widgetFrame: input.border } : {},
169
+ ...input.focus ? { focus: input.focus } : {},
170
+ ...input.success ? { success: input.success } : {},
171
+ ...input.warning ? { warning: input.warning } : {},
172
+ ...input.danger ? { danger: input.danger } : {},
173
+ ...input.code ? { code: input.code } : {}
174
+ };
175
+ }
176
+ function radiusValue(radius) {
177
+ if (radius === "none") return 0;
178
+ if (radius === "sm") return 8;
179
+ if (radius === "lg") return 18;
180
+ return 14;
181
+ }
182
+
183
+ // src/transport.ts
184
+ var AssistantTransportError = class extends Error {
185
+ code = "invalid_response";
186
+ constructor(contentType) {
187
+ super(`assistant response was not an event stream (content-type: ${contentType || "unknown"})`);
188
+ this.name = "AssistantTransportError";
189
+ }
190
+ };
191
+ async function consumeAssistantEvents(response, onEvent) {
192
+ let delivered = 0;
193
+ let sawText = false;
194
+ const deliver = (event) => {
195
+ delivered += 1;
196
+ onEvent(event);
197
+ };
198
+ if (!response.body) {
199
+ const text = await response.text();
200
+ sawText = text.trim().length > 0;
201
+ emitFrames(text, deliver, true);
202
+ } else {
203
+ const reader = response.body.getReader();
204
+ const decoder = new TextDecoder();
205
+ let pending = "";
206
+ while (true) {
207
+ const { done, value } = await reader.read();
208
+ const chunk = decoder.decode(value, { stream: !done });
209
+ if (!sawText && chunk.trim().length > 0) sawText = true;
210
+ pending += chunk;
211
+ pending = emitFrames(pending, deliver, done);
212
+ if (done) break;
213
+ }
214
+ }
215
+ if (delivered === 0 && sawText) {
216
+ throw new AssistantTransportError(response.headers.get("content-type") ?? "");
217
+ }
218
+ }
219
+ function emitFrames(input, onEvent, flush) {
220
+ const normalized = input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
221
+ const frames = normalized.split("\n\n");
222
+ const pending = flush ? "" : frames.pop() ?? "";
223
+ for (const frame of frames) {
224
+ const event = parseFrame(frame);
225
+ if (event) onEvent(event);
226
+ }
227
+ if (flush && frames.length === 0 && normalized.trim()) {
228
+ const event = parseFrame(normalized);
229
+ if (event) onEvent(event);
230
+ }
231
+ return pending;
232
+ }
233
+ function parseFrame(frame) {
234
+ let event = "message";
235
+ const data = [];
236
+ for (const line of frame.split("\n")) {
237
+ if (line.startsWith(":")) continue;
238
+ const separator = line.indexOf(":");
239
+ const field = separator < 0 ? line : line.slice(0, separator);
240
+ const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
241
+ if (field === "event") event = value;
242
+ if (field === "data") data.push(value);
243
+ }
244
+ if (data.length === 0) return void 0;
245
+ try {
246
+ return { event, data: JSON.parse(data.join("\n")) };
247
+ } catch {
248
+ return void 0;
249
+ }
250
+ }
251
+
252
+ // src/element.ts
253
+ var ASSISTANT_TAG_NAME = "noodle-assistant";
254
+ var HTMLElementBase = globalThis.HTMLElement ?? class {
255
+ };
256
+ var NoodleAssistantElement = class extends HTMLElementBase {
257
+ static observedAttributes = ["open", "theme"];
258
+ #appearance = resolveAppearance();
259
+ #session;
260
+ #messages;
261
+ #media;
262
+ #context;
263
+ sessionEndpoint = "";
264
+ connectedCallback() {
265
+ if (!this.shadowRoot) this.attachShadow({ mode: "open" });
266
+ if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
267
+ this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
268
+ this.#media?.addEventListener("change", this.#handleSystemTheme);
269
+ document.addEventListener("keydown", this.#handleDocumentKeydown);
270
+ document.addEventListener("pointerdown", this.#handleDocumentPointerDown);
271
+ if (this.#appearance.behavior.startOpen) this.setAttribute("open", "");
272
+ this.#render();
273
+ this.dispatchEvent(new CustomEvent("assistant-ready"));
274
+ }
275
+ disconnectedCallback() {
276
+ this.#media?.removeEventListener("change", this.#handleSystemTheme);
277
+ document.removeEventListener("keydown", this.#handleDocumentKeydown);
278
+ document.removeEventListener("pointerdown", this.#handleDocumentPointerDown);
279
+ }
280
+ attributeChangedCallback(name) {
281
+ if (name === "theme") this.#applyTheme();
282
+ }
283
+ open() {
284
+ this.setAttribute("open", "");
285
+ this.dispatchEvent(new CustomEvent("assistant-open"));
286
+ }
287
+ close() {
288
+ this.removeAttribute("open");
289
+ this.dispatchEvent(new CustomEvent("assistant-close"));
290
+ }
291
+ toggle() {
292
+ this.hasAttribute("open") ? this.close() : this.open();
293
+ }
294
+ focusComposer() {
295
+ this.shadowRoot?.querySelector("textarea")?.focus();
296
+ }
297
+ resetSession() {
298
+ this.#session = void 0;
299
+ this.#messages?.replaceChildren();
300
+ this.dispatchEvent(new CustomEvent("assistant-session-reset"));
301
+ }
302
+ updateContext(context) {
303
+ this.#context = { ...context };
304
+ this.dispatchEvent(new CustomEvent("assistant-context-changed", { detail: { context } }));
305
+ }
306
+ async sendMessage(text) {
307
+ const message = text.trim();
308
+ if (!message) return;
309
+ if (!this.#session) this.#session = await this.#createSession();
310
+ this.#appendMessage("user", message);
311
+ this.dispatchEvent(new CustomEvent("assistant-message-started"));
312
+ await this.#sendTurn(message, true);
313
+ }
314
+ async #sendTurn(message, mayRetry) {
315
+ if (!this.#session) this.#session = await this.#createSession();
316
+ const response = await fetch(this.#session.endpoints.turns, {
317
+ method: "POST",
318
+ headers: {
319
+ Authorization: `Bearer ${this.#session.token}`,
320
+ "Content-Type": "application/json",
321
+ Accept: "text/event-stream"
322
+ },
323
+ body: JSON.stringify({ message })
324
+ });
325
+ if (response.status === 401 && mayRetry) {
326
+ this.#session = void 0;
327
+ this.dispatchEvent(new CustomEvent("assistant-session-expired"));
328
+ await this.#sendTurn(message, false);
329
+ return;
330
+ }
331
+ if (!response.ok) {
332
+ this.#dispatchError({ code: "turn_failed", status: response.status, retryable: false });
333
+ throw new Error(`Assistant turn failed (${response.status})`);
334
+ }
335
+ let assistantBody;
336
+ try {
337
+ await consumeAssistantEvents(response, (event) => {
338
+ if (event.event === "content") {
339
+ const body = assistantBody ?? this.#appendMessage("assistant", "");
340
+ assistantBody = body;
341
+ if (body) body.textContent += String(event.data.delta ?? "");
342
+ }
343
+ if (event.event === "tool_proposed") {
344
+ this.#appendToolProposal(
345
+ String(event.data.id ?? ""),
346
+ String(event.data.tool ?? "Action")
347
+ );
348
+ }
349
+ if (event.event === "error") {
350
+ this.#dispatchError({
351
+ code: String(event.data.code ?? "stream_failed"),
352
+ retryable: event.data.retryable === true
353
+ });
354
+ }
355
+ });
356
+ } catch (error) {
357
+ if (error instanceof AssistantTransportError) {
358
+ this.#dispatchError({ code: error.code, retryable: false });
359
+ }
360
+ throw error;
361
+ }
362
+ this.dispatchEvent(new CustomEvent("assistant-message-completed"));
363
+ }
364
+ /**
365
+ * Every listener the element attaches to its own DOM must go through this guard: internal UI
366
+ * failures surface as `assistant-error` events (already dispatched by the failing path) and must
367
+ * never escape as unhandled rejections into the embedding page. Programmatic callers use
368
+ * `sendMessage`/`confirmTool` directly and keep the rejection.
369
+ */
370
+ #sendGuarded(text) {
371
+ void this.sendMessage(text).catch(() => {
372
+ });
373
+ }
374
+ async confirmTool(id) {
375
+ if (!this.#session) {
376
+ this.#dispatchError({ code: "confirmation_expired", retryable: false });
377
+ throw new Error("assistant session is not active");
378
+ }
379
+ const response = await fetch(this.#session.endpoints.toolConfirmations, {
380
+ method: "POST",
381
+ headers: {
382
+ Authorization: `Bearer ${this.#session.token}`,
383
+ "Content-Type": "application/json",
384
+ Accept: "text/event-stream"
385
+ },
386
+ body: JSON.stringify({ id })
387
+ });
388
+ if (!response.ok) {
389
+ const expired = response.status === 401 || response.status === 409;
390
+ if (response.status === 401) {
391
+ this.#session = void 0;
392
+ this.dispatchEvent(new CustomEvent("assistant-session-expired"));
393
+ }
394
+ this.#dispatchError({
395
+ code: expired ? "confirmation_expired" : "confirmation_failed",
396
+ status: response.status,
397
+ retryable: false
398
+ });
399
+ throw new Error(`Assistant confirmation failed (${response.status})`);
400
+ }
401
+ let completed;
402
+ await consumeAssistantEvents(response, (event) => {
403
+ if (event.event === "tool_completed") completed = event.data;
404
+ });
405
+ if (completed) this.#appendMessage("assistant", JSON.stringify(completed.result ?? {}));
406
+ this.dispatchEvent(new CustomEvent("assistant-tool-completed", { detail: { id } }));
407
+ }
408
+ #handleSystemTheme = () => this.#applyTheme();
409
+ #handleDocumentKeydown = (event) => {
410
+ if (event.key === "Escape" && this.hasAttribute("open") && this.#appearance.behavior.closeOnEscape) {
411
+ this.close();
412
+ }
413
+ };
414
+ #handleDocumentPointerDown = (event) => {
415
+ if (this.hasAttribute("open") && this.#appearance.behavior.closeOnOutsideClick && !event.composedPath().includes(this)) {
416
+ this.close();
417
+ }
418
+ };
419
+ async #createSession() {
420
+ if (!this.sessionEndpoint) throw new Error("sessionEndpoint is required");
421
+ let response;
422
+ try {
423
+ response = await fetch(this.sessionEndpoint, {
424
+ method: "POST",
425
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
426
+ body: JSON.stringify(this.#context ? { context: this.#context } : {}),
427
+ credentials: "same-origin"
428
+ });
429
+ } catch (error) {
430
+ this.#dispatchError({ code: "session_failed", retryable: true });
431
+ throw error;
432
+ }
433
+ if (!response.ok) {
434
+ this.#dispatchError({ code: "session_failed", status: response.status, retryable: true });
435
+ throw new Error(`Assistant session failed (${response.status})`);
436
+ }
437
+ let session;
438
+ try {
439
+ session = await response.json();
440
+ } catch (error) {
441
+ this.#dispatchError({ code: "session_failed", retryable: true });
442
+ throw error;
443
+ }
444
+ if (session.configuration) {
445
+ this.#appearance = resolveAppearance(session.configuration);
446
+ this.#render();
447
+ }
448
+ this.dispatchEvent(
449
+ new CustomEvent("assistant-session-started", { detail: { expiresAt: session.expiresAt } })
450
+ );
451
+ return session;
452
+ }
453
+ #appendMessage(role, text) {
454
+ if (!this.#messages) return void 0;
455
+ const message = document.createElement("div");
456
+ message.className = `message ${role}`;
457
+ if (role === "assistant" && this.#appearance.behavior.showAvatars && this.#appearance.brand.avatar[this.#resolvedMode()]) {
458
+ const avatar = document.createElement("img");
459
+ avatar.className = "avatar";
460
+ avatar.src = this.#appearance.brand.avatar[this.#resolvedMode()] ?? "";
461
+ avatar.alt = "";
462
+ message.append(avatar);
463
+ }
464
+ const body = document.createElement("span");
465
+ body.textContent = text;
466
+ message.append(body);
467
+ if (this.#appearance.behavior.showTimestamps) {
468
+ const timestamp = document.createElement("time");
469
+ timestamp.dateTime = (/* @__PURE__ */ new Date()).toISOString();
470
+ timestamp.textContent = new Intl.DateTimeFormat(this.#appearance.locale, {
471
+ hour: "numeric",
472
+ minute: "2-digit"
473
+ }).format(/* @__PURE__ */ new Date());
474
+ message.append(timestamp);
475
+ }
476
+ this.#messages.append(message);
477
+ return body;
478
+ }
479
+ #dispatchError(detail) {
480
+ this.dispatchEvent(new CustomEvent("assistant-error", { detail }));
481
+ }
482
+ #appendToolProposal(id, tool) {
483
+ if (!this.#messages || !id) return;
484
+ const card = document.createElement("section");
485
+ card.className = "tool-proposal";
486
+ const label = document.createElement("strong");
487
+ label.textContent = tool;
488
+ const confirm = document.createElement("button");
489
+ confirm.type = "button";
490
+ confirm.textContent = this.#appearance.labels.confirm;
491
+ confirm.addEventListener("click", () => {
492
+ confirm.disabled = true;
493
+ void this.confirmTool(id).catch(() => {
494
+ confirm.disabled = false;
495
+ this.dispatchEvent(
496
+ new CustomEvent("assistant-error", { detail: { code: "confirmation_failed" } })
497
+ );
498
+ });
499
+ });
500
+ const cancel = document.createElement("button");
501
+ cancel.type = "button";
502
+ cancel.textContent = this.#appearance.labels.cancel;
503
+ cancel.addEventListener("click", () => card.remove());
504
+ card.append(label, confirm, cancel);
505
+ this.#messages.append(card);
506
+ this.dispatchEvent(new CustomEvent("assistant-tool-proposed", { detail: { id, tool } }));
507
+ }
508
+ #resolvedMode() {
509
+ const configured = this.getAttribute("theme") ?? this.#appearance.theme.defaultMode;
510
+ return configured === "auto" ? this.#media?.matches ? "dark" : "light" : configured;
511
+ }
512
+ #applyTheme() {
513
+ const mode = this.#resolvedMode();
514
+ this.setAttribute("data-theme", mode);
515
+ const tokens = this.#appearance.theme[mode];
516
+ for (const [name, value] of Object.entries(tokens)) {
517
+ this.style.setProperty(
518
+ `--ns-assistant-default-${camelToKebab(name)}`,
519
+ tokenValue(name, value)
520
+ );
521
+ }
522
+ this.style.setProperty(
523
+ "--ns-assistant-default-panel-width",
524
+ `${this.#appearance.layout.panelWidth}px`
525
+ );
526
+ this.style.setProperty(
527
+ "--ns-assistant-default-min-height",
528
+ `${this.#appearance.layout.panelMinHeight}px`
529
+ );
530
+ this.style.setProperty(
531
+ "--ns-assistant-default-max-height",
532
+ `${this.#appearance.layout.panelMaxHeight}px`
533
+ );
534
+ this.style.setProperty(
535
+ "--ns-assistant-default-z-index",
536
+ String(this.#appearance.layout.zIndex)
537
+ );
538
+ const logo = this.shadowRoot?.querySelector(".brand-logo");
539
+ const logoUrl = this.#appearance.brand.logo[mode];
540
+ if (logo) {
541
+ logo.hidden = !logoUrl;
542
+ if (logoUrl) logo.src = logoUrl;
543
+ }
544
+ }
545
+ #render() {
546
+ if (!this.shadowRoot) return;
547
+ const appearance = this.#appearance;
548
+ this.setAttribute("role", "complementary");
549
+ this.setAttribute("aria-label", appearance.brand.name);
550
+ this.setAttribute("lang", appearance.locale);
551
+ this.setAttribute("dir", appearance.direction);
552
+ this.dataset.mode = appearance.layout.mode;
553
+ this.dataset.position = appearance.layout.position;
554
+ this.dataset.density = appearance.layout.density;
555
+ this.toggleAttribute("mobile-fullscreen", appearance.layout.mobileFullscreen);
556
+ this.shadowRoot.innerHTML = `${styles}
557
+ <button class="launcher" type="button" aria-label="${escapeAttribute(appearance.labels.open)}"><slot name="launcher-icon">\u2726</slot></button>
558
+ <section class="panel" aria-label="${escapeAttribute(appearance.brand.name)}">
559
+ <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>
560
+ <div class="welcome"><slot name="empty-state"><h2></h2><p></p></slot></div>
561
+ <div class="suggested-prompts"></div>
562
+ <div class="messages" aria-live="polite"></div>
563
+ <form><slot name="composer-leading"></slot><textarea rows="1"></textarea><button class="send" type="submit"></button><slot name="composer-trailing"></slot></form>
564
+ <nav class="legal" aria-label="Legal"></nav>
565
+ <slot name="conversation-footer"></slot>
566
+ </section>`;
567
+ queryRequired(this.shadowRoot, "strong").textContent = appearance.brand.name;
568
+ const launcherIcon = appearance.brand.mark[this.#resolvedMode()];
569
+ if (launcherIcon) {
570
+ const launcherSlot = queryRequired(
571
+ this.shadowRoot,
572
+ 'slot[name="launcher-icon"]'
573
+ );
574
+ const icon = document.createElement("img");
575
+ icon.src = launcherIcon;
576
+ icon.alt = "";
577
+ launcherSlot.replaceChildren(icon);
578
+ }
579
+ const logo = queryRequired(this.shadowRoot, ".brand-logo");
580
+ const logoUrl = appearance.brand.logo[this.#resolvedMode()];
581
+ if (logoUrl) {
582
+ logo.src = logoUrl;
583
+ logo.alt = appearance.brand.name;
584
+ } else {
585
+ logo.hidden = true;
586
+ }
587
+ queryRequired(this.shadowRoot, "h2").textContent = appearance.labels.welcomeHeading;
588
+ queryRequired(this.shadowRoot, "p").textContent = appearance.labels.welcomeMessage;
589
+ const textarea = queryRequired(this.shadowRoot, "textarea");
590
+ textarea.placeholder = appearance.labels.composerPlaceholder;
591
+ textarea.setAttribute("aria-label", appearance.labels.composerPlaceholder);
592
+ queryRequired(this.shadowRoot, ".send").textContent = appearance.labels.send;
593
+ const prompts = queryRequired(this.shadowRoot, ".suggested-prompts");
594
+ for (const prompt of appearance.suggestedPrompts) {
595
+ const button = document.createElement("button");
596
+ button.type = "button";
597
+ button.textContent = prompt;
598
+ button.addEventListener("click", () => this.#sendGuarded(prompt));
599
+ prompts.append(button);
600
+ }
601
+ if (!appearance.behavior.showLauncher)
602
+ queryRequired(this.shadowRoot, ".launcher").hidden = true;
603
+ if (!appearance.behavior.showHeader)
604
+ queryRequired(this.shadowRoot, "header").hidden = true;
605
+ const legal = queryRequired(this.shadowRoot, ".legal");
606
+ for (const [label, href] of [
607
+ ["Privacy", appearance.privacyUrl],
608
+ ["Terms", appearance.termsUrl]
609
+ ]) {
610
+ if (!href) continue;
611
+ const link = document.createElement("a");
612
+ link.href = href;
613
+ link.target = "_blank";
614
+ link.rel = "noopener noreferrer";
615
+ link.textContent = label;
616
+ legal.append(link);
617
+ }
618
+ this.#messages = this.shadowRoot.querySelector(".messages") ?? void 0;
619
+ this.shadowRoot.querySelector(".launcher")?.addEventListener("click", () => this.open());
620
+ this.shadowRoot.querySelector(".close")?.addEventListener("click", () => this.close());
621
+ this.shadowRoot.querySelector("form")?.addEventListener("submit", (event) => {
622
+ event.preventDefault();
623
+ const value = textarea.value;
624
+ textarea.value = "";
625
+ this.#sendGuarded(value);
626
+ });
627
+ this.#applyTheme();
628
+ }
629
+ };
630
+ function registerNoodleAssistant() {
631
+ if (globalThis.customElements && !customElements.get(ASSISTANT_TAG_NAME)) {
632
+ customElements.define(ASSISTANT_TAG_NAME, NoodleAssistantElement);
633
+ }
634
+ }
635
+ function camelToKebab(value) {
636
+ return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
637
+ }
638
+ function tokenValue(name, value) {
639
+ if (typeof value !== "number") return value;
640
+ if (name === "lineHeight" || name === "motionScale") return String(value);
641
+ return `${value}px`;
642
+ }
643
+ function escapeAttribute(value) {
644
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
645
+ }
646
+ function queryRequired(root, selector) {
647
+ const element = root.querySelector(selector);
648
+ if (!element) throw new Error(`assistant renderer is missing ${selector}`);
649
+ return element;
650
+ }
651
+ var styles = `<style>
652
+ :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)); }
653
+ :host([data-mode="floating"]) { position: fixed; bottom: 24px; right: 24px; }
654
+ :host([data-mode="floating"][data-position="bottom-left"]) { right: auto; left: 24px; }
655
+ .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; }
656
+ .launcher img { width: 24px; height: 24px; object-fit: contain; }
657
+ .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)); }
658
+ :host([open]) .panel { display: grid; grid-template-rows: auto auto 1fr auto; }
659
+ :host([open]) .launcher { display: none; }
660
+ 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)); }
661
+ header strong { flex: 1; }
662
+ .brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
663
+ button { font: inherit; }
664
+ .close, .send { border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); cursor: pointer; }
665
+ .close { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; font-size: 24px; }
666
+ .welcome { padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.5); }
667
+ .welcome h2 { margin: 0; font-size: 1.25em; }
668
+ .welcome p { margin: 6px 0 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
669
+ .suggested-prompts { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
670
+ .suggested-prompts:empty { display: none; }
671
+ .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; }
672
+ .messages { min-height: 120px; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); overflow: auto; }
673
+ .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; }
674
+ .message .avatar { float: left; width: 24px; height: 24px; margin-right: 8px; border-radius: 50%; object-fit: cover; }
675
+ .message time { display: block; margin-top: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .75em; }
676
+ .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)); }
677
+ .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)); }
678
+ .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; }
679
+ .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)); }
680
+ 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)); }
681
+ 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; }
682
+ textarea:focus-visible, button:focus-visible { outline: 3px solid var(--ns-assistant-focus, var(--ns-assistant-default-focus)); outline-offset: 2px; }
683
+ .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)); }
684
+ .legal { display: flex; gap: 12px; justify-content: center; padding: 0 12px 10px; font-size: .8em; }
685
+ .legal:empty { display: none; }
686
+ .legal a { color: var(--ns-assistant-link, var(--ns-assistant-default-link)); }
687
+ :host([data-density="compact"]) .welcome { padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
688
+ @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; } }
689
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
690
+ @media (max-width: 560px) { :host([data-mode="floating"]) { inset: auto 12px 12px; } :host([open]) .panel { width: calc(100vw - 24px); max-height: calc(100vh - 24px); } }
691
+ </style>`;
692
+
693
+ export {
694
+ ASSISTANT_TAG_NAME,
695
+ NoodleAssistantElement,
696
+ registerNoodleAssistant
697
+ };
698
+ //# sourceMappingURL=chunk-OXFV6KZ2.js.map