vanilla-agent 1.10.0 → 1.12.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 +57 -8
- package/dist/index.cjs +52 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +308 -1
- package/dist/index.d.ts +308 -1
- package/dist/index.global.js +97 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +52 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/composer-builder.ts +366 -0
- package/src/components/header-builder.ts +454 -0
- package/src/components/header-layouts.ts +303 -0
- package/src/components/message-bubble.ts +251 -34
- package/src/components/panel.ts +46 -684
- package/src/defaults.ts +49 -1
- package/src/index.ts +45 -1
- package/src/runtime/init.ts +27 -1
- package/src/types.ts +231 -0
- package/src/ui.ts +452 -34
- package/src/utils/code-generators.ts +1242 -0
package/src/ui.ts
CHANGED
|
@@ -9,14 +9,17 @@ import {
|
|
|
9
9
|
AgentWidgetControllerEventMap,
|
|
10
10
|
AgentWidgetVoiceStateEvent,
|
|
11
11
|
AgentWidgetStateEvent,
|
|
12
|
-
AgentWidgetStateSnapshot
|
|
12
|
+
AgentWidgetStateSnapshot,
|
|
13
|
+
WidgetLayoutSlot,
|
|
14
|
+
SlotRenderer
|
|
13
15
|
} from "./types";
|
|
14
16
|
import { applyThemeVariables } from "./utils/theme";
|
|
15
17
|
import { renderLucideIcon } from "./utils/icons";
|
|
16
18
|
import { createElement } from "./utils/dom";
|
|
17
19
|
import { statusCopy } from "./utils/constants";
|
|
18
20
|
import { createLauncherButton } from "./components/launcher";
|
|
19
|
-
import { createWrapper, buildPanel } from "./components/panel";
|
|
21
|
+
import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
|
|
22
|
+
import type { HeaderElements, ComposerElements } from "./components/panel";
|
|
20
23
|
import { MessageTransform } from "./components/message-bubble";
|
|
21
24
|
import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
|
|
22
25
|
import { createReasoningBubble } from "./components/reasoning-bubble";
|
|
@@ -223,7 +226,7 @@ export const createAgentExperience = (
|
|
|
223
226
|
|
|
224
227
|
const { wrapper, panel } = createWrapper(config);
|
|
225
228
|
const panelElements = buildPanel(config, launcherEnabled);
|
|
226
|
-
|
|
229
|
+
let {
|
|
227
230
|
container,
|
|
228
231
|
body,
|
|
229
232
|
messagesWrapper,
|
|
@@ -238,16 +241,297 @@ export const createAgentExperience = (
|
|
|
238
241
|
closeButton,
|
|
239
242
|
iconHolder,
|
|
240
243
|
headerTitle,
|
|
241
|
-
headerSubtitle
|
|
244
|
+
headerSubtitle,
|
|
245
|
+
header,
|
|
246
|
+
footer
|
|
242
247
|
} = panelElements;
|
|
243
248
|
|
|
244
249
|
// Use mutable references for mic button so we can update them dynamically
|
|
245
250
|
let micButton: HTMLButtonElement | null = panelElements.micButton;
|
|
246
251
|
let micButtonWrapper: HTMLElement | null = panelElements.micButtonWrapper;
|
|
247
252
|
|
|
253
|
+
// Plugin hook: renderHeader - allow plugins to provide custom header
|
|
254
|
+
const headerPlugin = plugins.find(p => p.renderHeader);
|
|
255
|
+
if (headerPlugin?.renderHeader) {
|
|
256
|
+
const customHeader = headerPlugin.renderHeader({
|
|
257
|
+
config,
|
|
258
|
+
defaultRenderer: () => {
|
|
259
|
+
const headerElements = buildHeader({ config, showClose: launcherEnabled });
|
|
260
|
+
attachHeaderToContainer(container, headerElements, config);
|
|
261
|
+
return headerElements.header;
|
|
262
|
+
},
|
|
263
|
+
onClose: () => setOpenState(false, "user")
|
|
264
|
+
});
|
|
265
|
+
if (customHeader) {
|
|
266
|
+
// Replace the default header with custom header
|
|
267
|
+
const existingHeader = container.querySelector('.tvw-border-b-cw-divider');
|
|
268
|
+
if (existingHeader) {
|
|
269
|
+
existingHeader.replaceWith(customHeader);
|
|
270
|
+
header = customHeader;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Plugin hook: renderComposer - allow plugins to provide custom composer
|
|
276
|
+
const composerPlugin = plugins.find(p => p.renderComposer);
|
|
277
|
+
if (composerPlugin?.renderComposer) {
|
|
278
|
+
const customComposer = composerPlugin.renderComposer({
|
|
279
|
+
config,
|
|
280
|
+
defaultRenderer: () => {
|
|
281
|
+
const composerElements = buildComposer({ config });
|
|
282
|
+
return composerElements.footer;
|
|
283
|
+
},
|
|
284
|
+
onSubmit: (text: string) => {
|
|
285
|
+
if (session && !session.isStreaming()) {
|
|
286
|
+
session.sendMessage(text);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
disabled: false
|
|
290
|
+
});
|
|
291
|
+
if (customComposer) {
|
|
292
|
+
// Replace the default footer with custom composer
|
|
293
|
+
footer.replaceWith(customComposer);
|
|
294
|
+
footer = customComposer;
|
|
295
|
+
// Note: When using custom composer, textarea/sendButton/etc may not exist
|
|
296
|
+
// The plugin is responsible for providing its own submit handling
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Slot system: allow custom content injection into specific regions
|
|
301
|
+
const renderSlots = () => {
|
|
302
|
+
const slots = config.layout?.slots ?? {};
|
|
303
|
+
|
|
304
|
+
// Helper to get default slot content
|
|
305
|
+
const getDefaultSlotContent = (slot: WidgetLayoutSlot): HTMLElement | null => {
|
|
306
|
+
switch (slot) {
|
|
307
|
+
case "body-top":
|
|
308
|
+
// Default: the intro card
|
|
309
|
+
return container.querySelector(".tvw-rounded-2xl.tvw-bg-cw-surface.tvw-p-6") as HTMLElement || null;
|
|
310
|
+
case "messages":
|
|
311
|
+
return messagesWrapper;
|
|
312
|
+
case "footer-top":
|
|
313
|
+
return suggestions;
|
|
314
|
+
case "composer":
|
|
315
|
+
return composerForm;
|
|
316
|
+
case "footer-bottom":
|
|
317
|
+
return statusText;
|
|
318
|
+
default:
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Helper to insert content into slot region
|
|
324
|
+
const insertSlotContent = (slot: WidgetLayoutSlot, element: HTMLElement) => {
|
|
325
|
+
switch (slot) {
|
|
326
|
+
case "header-left":
|
|
327
|
+
case "header-center":
|
|
328
|
+
case "header-right":
|
|
329
|
+
// Header slots - prepend/append to header
|
|
330
|
+
if (slot === "header-left") {
|
|
331
|
+
header.insertBefore(element, header.firstChild);
|
|
332
|
+
} else if (slot === "header-right") {
|
|
333
|
+
header.appendChild(element);
|
|
334
|
+
} else {
|
|
335
|
+
// header-center: insert after icon/title
|
|
336
|
+
const titleSection = header.querySelector(".tvw-flex-col");
|
|
337
|
+
if (titleSection) {
|
|
338
|
+
titleSection.parentNode?.insertBefore(element, titleSection.nextSibling);
|
|
339
|
+
} else {
|
|
340
|
+
header.appendChild(element);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
case "body-top":
|
|
345
|
+
// Replace or prepend to body
|
|
346
|
+
const introCard = body.querySelector(".tvw-rounded-2xl.tvw-bg-cw-surface.tvw-p-6");
|
|
347
|
+
if (introCard) {
|
|
348
|
+
introCard.replaceWith(element);
|
|
349
|
+
} else {
|
|
350
|
+
body.insertBefore(element, body.firstChild);
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
case "body-bottom":
|
|
354
|
+
// Append after messages wrapper
|
|
355
|
+
body.appendChild(element);
|
|
356
|
+
break;
|
|
357
|
+
case "footer-top":
|
|
358
|
+
// Replace suggestions area
|
|
359
|
+
suggestions.replaceWith(element);
|
|
360
|
+
break;
|
|
361
|
+
case "footer-bottom":
|
|
362
|
+
// Replace or append after status text
|
|
363
|
+
statusText.replaceWith(element);
|
|
364
|
+
break;
|
|
365
|
+
default:
|
|
366
|
+
// For other slots, just append to appropriate container
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Process each configured slot
|
|
372
|
+
for (const [slotName, renderer] of Object.entries(slots) as [WidgetLayoutSlot, SlotRenderer][]) {
|
|
373
|
+
if (renderer) {
|
|
374
|
+
try {
|
|
375
|
+
const slotElement = renderer({
|
|
376
|
+
config,
|
|
377
|
+
defaultContent: () => getDefaultSlotContent(slotName)
|
|
378
|
+
});
|
|
379
|
+
if (slotElement) {
|
|
380
|
+
insertSlotContent(slotName, slotElement);
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (typeof console !== "undefined") {
|
|
384
|
+
// eslint-disable-next-line no-console
|
|
385
|
+
console.error(`[AgentWidget] Error rendering slot "${slotName}":`, error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Render custom slots
|
|
393
|
+
renderSlots();
|
|
394
|
+
|
|
248
395
|
panel.appendChild(container);
|
|
249
396
|
mount.appendChild(wrapper);
|
|
250
397
|
|
|
398
|
+
// Apply full-height and sidebar styles if enabled
|
|
399
|
+
// This ensures the widget fills its container height with proper flex layout
|
|
400
|
+
const applyFullHeightStyles = () => {
|
|
401
|
+
const sidebarMode = config.launcher?.sidebarMode ?? false;
|
|
402
|
+
const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
403
|
+
const theme = config.theme ?? {};
|
|
404
|
+
|
|
405
|
+
// Determine panel styling based on mode, with theme overrides
|
|
406
|
+
const position = config.launcher?.position ?? 'bottom-left';
|
|
407
|
+
const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
|
|
408
|
+
|
|
409
|
+
// Default values based on mode
|
|
410
|
+
const defaultPanelBorder = sidebarMode ? 'none' : '1px solid var(--tvw-cw-border)';
|
|
411
|
+
const defaultPanelShadow = sidebarMode
|
|
412
|
+
? (isLeftSidebar ? '2px 0 12px rgba(0, 0, 0, 0.08)' : '-2px 0 12px rgba(0, 0, 0, 0.08)')
|
|
413
|
+
: '0 25px 50px -12px rgba(0, 0, 0, 0.25)';
|
|
414
|
+
const defaultPanelBorderRadius = sidebarMode ? '0' : '16px';
|
|
415
|
+
|
|
416
|
+
// Apply theme overrides or defaults
|
|
417
|
+
const panelBorder = theme.panelBorder ?? defaultPanelBorder;
|
|
418
|
+
const panelShadow = theme.panelShadow ?? defaultPanelShadow;
|
|
419
|
+
const panelBorderRadius = theme.panelBorderRadius ?? defaultPanelBorderRadius;
|
|
420
|
+
|
|
421
|
+
// Apply panel styling to container (works in all modes)
|
|
422
|
+
container.style.border = panelBorder;
|
|
423
|
+
container.style.boxShadow = panelShadow;
|
|
424
|
+
container.style.borderRadius = panelBorderRadius;
|
|
425
|
+
|
|
426
|
+
if (fullHeight) {
|
|
427
|
+
// Mount container
|
|
428
|
+
mount.style.display = 'flex';
|
|
429
|
+
mount.style.flexDirection = 'column';
|
|
430
|
+
mount.style.height = '100%';
|
|
431
|
+
mount.style.minHeight = '0';
|
|
432
|
+
|
|
433
|
+
// Wrapper
|
|
434
|
+
wrapper.style.display = 'flex';
|
|
435
|
+
wrapper.style.flexDirection = 'column';
|
|
436
|
+
wrapper.style.flex = '1 1 0%';
|
|
437
|
+
wrapper.style.minHeight = '0';
|
|
438
|
+
wrapper.style.maxHeight = '100%';
|
|
439
|
+
wrapper.style.height = '100%';
|
|
440
|
+
wrapper.style.overflow = 'hidden';
|
|
441
|
+
|
|
442
|
+
// Panel
|
|
443
|
+
panel.style.display = 'flex';
|
|
444
|
+
panel.style.flexDirection = 'column';
|
|
445
|
+
panel.style.flex = '1 1 0%';
|
|
446
|
+
panel.style.minHeight = '0';
|
|
447
|
+
panel.style.maxHeight = '100%';
|
|
448
|
+
panel.style.height = '100%';
|
|
449
|
+
panel.style.overflow = 'hidden';
|
|
450
|
+
|
|
451
|
+
// Main container
|
|
452
|
+
container.style.display = 'flex';
|
|
453
|
+
container.style.flexDirection = 'column';
|
|
454
|
+
container.style.flex = '1 1 0%';
|
|
455
|
+
container.style.minHeight = '0';
|
|
456
|
+
container.style.maxHeight = '100%';
|
|
457
|
+
container.style.overflow = 'hidden';
|
|
458
|
+
|
|
459
|
+
// Body (scrollable messages area)
|
|
460
|
+
body.style.flex = '1 1 0%';
|
|
461
|
+
body.style.minHeight = '0';
|
|
462
|
+
body.style.overflowY = 'auto';
|
|
463
|
+
|
|
464
|
+
// Footer (composer) - should not shrink
|
|
465
|
+
footer.style.flexShrink = '0';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Apply sidebar-specific styles
|
|
469
|
+
if (sidebarMode) {
|
|
470
|
+
const sidebarWidth = config.launcher?.sidebarWidth ?? '420px';
|
|
471
|
+
|
|
472
|
+
// Remove Tailwind positioning classes that add spacing (tvw-bottom-6, tvw-right-6, etc.)
|
|
473
|
+
wrapper.classList.remove(
|
|
474
|
+
'tvw-bottom-6', 'tvw-right-6', 'tvw-left-6', 'tvw-top-6',
|
|
475
|
+
'tvw-bottom-4', 'tvw-right-4', 'tvw-left-4', 'tvw-top-4'
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Wrapper - fixed position, flush with edges
|
|
479
|
+
wrapper.style.cssText = `
|
|
480
|
+
position: fixed !important;
|
|
481
|
+
top: 0 !important;
|
|
482
|
+
bottom: 0 !important;
|
|
483
|
+
width: ${sidebarWidth} !important;
|
|
484
|
+
height: 100vh !important;
|
|
485
|
+
max-height: 100vh !important;
|
|
486
|
+
margin: 0 !important;
|
|
487
|
+
padding: 0 !important;
|
|
488
|
+
display: flex !important;
|
|
489
|
+
flex-direction: column !important;
|
|
490
|
+
${isLeftSidebar ? 'left: 0 !important; right: auto !important;' : 'left: auto !important; right: 0 !important;'}
|
|
491
|
+
`;
|
|
492
|
+
|
|
493
|
+
// Panel - fill wrapper (override inline width/max-width from panel.ts)
|
|
494
|
+
panel.style.cssText = `
|
|
495
|
+
position: relative !important;
|
|
496
|
+
display: flex !important;
|
|
497
|
+
flex-direction: column !important;
|
|
498
|
+
flex: 1 1 0% !important;
|
|
499
|
+
width: 100% !important;
|
|
500
|
+
max-width: 100% !important;
|
|
501
|
+
height: 100% !important;
|
|
502
|
+
min-height: 0 !important;
|
|
503
|
+
margin: 0 !important;
|
|
504
|
+
padding: 0 !important;
|
|
505
|
+
`;
|
|
506
|
+
// Force override any inline width/maxWidth that may be set elsewhere
|
|
507
|
+
panel.style.setProperty('width', '100%', 'important');
|
|
508
|
+
panel.style.setProperty('max-width', '100%', 'important');
|
|
509
|
+
|
|
510
|
+
// Container - apply configurable styles with sidebar layout
|
|
511
|
+
container.style.cssText = `
|
|
512
|
+
display: flex !important;
|
|
513
|
+
flex-direction: column !important;
|
|
514
|
+
flex: 1 1 0% !important;
|
|
515
|
+
width: 100% !important;
|
|
516
|
+
height: 100% !important;
|
|
517
|
+
min-height: 0 !important;
|
|
518
|
+
max-height: 100% !important;
|
|
519
|
+
overflow: hidden !important;
|
|
520
|
+
border-radius: ${panelBorderRadius} !important;
|
|
521
|
+
border: ${panelBorder} !important;
|
|
522
|
+
box-shadow: ${panelShadow} !important;
|
|
523
|
+
`;
|
|
524
|
+
|
|
525
|
+
// Remove footer border in sidebar mode
|
|
526
|
+
footer.style.cssText = `
|
|
527
|
+
flex-shrink: 0 !important;
|
|
528
|
+
border-top: none !important;
|
|
529
|
+
padding: 8px 16px 12px 16px !important;
|
|
530
|
+
`;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
applyFullHeightStyles();
|
|
534
|
+
|
|
251
535
|
const destroyCallbacks: Array<() => void> = [];
|
|
252
536
|
const suggestionsManager = createSuggestions(suggestions);
|
|
253
537
|
let closeHandler: (() => void) | null = null;
|
|
@@ -506,6 +790,9 @@ export const createAgentExperience = (
|
|
|
506
790
|
return false;
|
|
507
791
|
});
|
|
508
792
|
|
|
793
|
+
// Get message layout config
|
|
794
|
+
const messageLayoutConfig = config.layout?.messages;
|
|
795
|
+
|
|
509
796
|
if (matchingPlugin) {
|
|
510
797
|
if (message.variant === "reasoning" && message.reasoning && matchingPlugin.renderReasoning) {
|
|
511
798
|
if (!showReasoning) return;
|
|
@@ -525,7 +812,7 @@ export const createAgentExperience = (
|
|
|
525
812
|
bubble = matchingPlugin.renderMessage({
|
|
526
813
|
message,
|
|
527
814
|
defaultRenderer: () => {
|
|
528
|
-
const b = createStandardBubble(message, transform);
|
|
815
|
+
const b = createStandardBubble(message, transform, messageLayoutConfig);
|
|
529
816
|
if (message.role !== "user") {
|
|
530
817
|
enhanceWithForms(b, message, config, session);
|
|
531
818
|
}
|
|
@@ -590,8 +877,24 @@ export const createAgentExperience = (
|
|
|
590
877
|
if (!showToolCalls) return;
|
|
591
878
|
bubble = createToolBubble(message, config);
|
|
592
879
|
} else {
|
|
593
|
-
|
|
594
|
-
|
|
880
|
+
// Check for custom message renderers in layout config
|
|
881
|
+
const messageLayoutConfig = config.layout?.messages;
|
|
882
|
+
if (messageLayoutConfig?.renderUserMessage && message.role === "user") {
|
|
883
|
+
bubble = messageLayoutConfig.renderUserMessage({
|
|
884
|
+
message,
|
|
885
|
+
config,
|
|
886
|
+
streaming: Boolean(message.streaming)
|
|
887
|
+
});
|
|
888
|
+
} else if (messageLayoutConfig?.renderAssistantMessage && message.role === "assistant") {
|
|
889
|
+
bubble = messageLayoutConfig.renderAssistantMessage({
|
|
890
|
+
message,
|
|
891
|
+
config,
|
|
892
|
+
streaming: Boolean(message.streaming)
|
|
893
|
+
});
|
|
894
|
+
} else {
|
|
895
|
+
bubble = createStandardBubble(message, transform, messageLayoutConfig);
|
|
896
|
+
}
|
|
897
|
+
if (message.role !== "user" && bubble) {
|
|
595
898
|
enhanceWithForms(bubble, message, config, session);
|
|
596
899
|
}
|
|
597
900
|
}
|
|
@@ -662,6 +965,8 @@ export const createAgentExperience = (
|
|
|
662
965
|
// Hide launcher button when widget is open
|
|
663
966
|
if (launcherButtonInstance) {
|
|
664
967
|
launcherButtonInstance.element.style.display = "none";
|
|
968
|
+
} else if (customLauncherElement) {
|
|
969
|
+
customLauncherElement.style.display = "none";
|
|
665
970
|
}
|
|
666
971
|
} else {
|
|
667
972
|
wrapper.classList.add("tvw-pointer-events-none", "tvw-opacity-0");
|
|
@@ -670,6 +975,8 @@ export const createAgentExperience = (
|
|
|
670
975
|
// Show launcher button when widget is closed
|
|
671
976
|
if (launcherButtonInstance) {
|
|
672
977
|
launcherButtonInstance.element.style.display = "";
|
|
978
|
+
} else if (customLauncherElement) {
|
|
979
|
+
customLauncherElement.style.display = "";
|
|
673
980
|
}
|
|
674
981
|
}
|
|
675
982
|
};
|
|
@@ -1163,12 +1470,36 @@ export const createAgentExperience = (
|
|
|
1163
1470
|
setOpenState(!open, "user");
|
|
1164
1471
|
};
|
|
1165
1472
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1473
|
+
// Plugin hook: renderLauncher - allow plugins to provide custom launcher
|
|
1474
|
+
let launcherButtonInstance: ReturnType<typeof createLauncherButton> | null = null;
|
|
1475
|
+
let customLauncherElement: HTMLElement | null = null;
|
|
1476
|
+
|
|
1477
|
+
if (launcherEnabled) {
|
|
1478
|
+
const launcherPlugin = plugins.find(p => p.renderLauncher);
|
|
1479
|
+
if (launcherPlugin?.renderLauncher) {
|
|
1480
|
+
const customLauncher = launcherPlugin.renderLauncher({
|
|
1481
|
+
config,
|
|
1482
|
+
defaultRenderer: () => {
|
|
1483
|
+
const btn = createLauncherButton(config, toggleOpen);
|
|
1484
|
+
return btn.element;
|
|
1485
|
+
},
|
|
1486
|
+
onToggle: toggleOpen
|
|
1487
|
+
});
|
|
1488
|
+
if (customLauncher) {
|
|
1489
|
+
customLauncherElement = customLauncher;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Use custom launcher if provided, otherwise use default
|
|
1494
|
+
if (!customLauncherElement) {
|
|
1495
|
+
launcherButtonInstance = createLauncherButton(config, toggleOpen);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1169
1498
|
|
|
1170
1499
|
if (launcherButtonInstance) {
|
|
1171
1500
|
mount.appendChild(launcherButtonInstance.element);
|
|
1501
|
+
} else if (customLauncherElement) {
|
|
1502
|
+
mount.appendChild(customLauncherElement);
|
|
1172
1503
|
}
|
|
1173
1504
|
updateOpenState();
|
|
1174
1505
|
suggestionsManager.render(config.suggestionChips, session, textarea, undefined, config.suggestionChipsConfig);
|
|
@@ -1178,20 +1509,31 @@ export const createAgentExperience = (
|
|
|
1178
1509
|
maybeRestoreVoiceFromMetadata();
|
|
1179
1510
|
|
|
1180
1511
|
const recalcPanelHeight = () => {
|
|
1512
|
+
const sidebarMode = config.launcher?.sidebarMode ?? false;
|
|
1513
|
+
const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
1514
|
+
|
|
1181
1515
|
if (!launcherEnabled) {
|
|
1182
1516
|
panel.style.height = "";
|
|
1183
1517
|
panel.style.width = "";
|
|
1184
1518
|
return;
|
|
1185
1519
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1520
|
+
|
|
1521
|
+
// In sidebar/fullHeight mode, don't override the width - it's handled by applyFullHeightStyles
|
|
1522
|
+
if (!sidebarMode) {
|
|
1523
|
+
const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
|
|
1524
|
+
const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
|
|
1525
|
+
panel.style.width = width;
|
|
1526
|
+
panel.style.maxWidth = width;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// In fullHeight mode, don't set a fixed height
|
|
1530
|
+
if (!fullHeight) {
|
|
1531
|
+
const viewportHeight = window.innerHeight;
|
|
1532
|
+
const verticalMargin = 64; // leave space for launcher's offset
|
|
1533
|
+
const available = Math.max(200, viewportHeight - verticalMargin);
|
|
1534
|
+
const clamped = Math.min(640, available);
|
|
1535
|
+
panel.style.height = `${clamped}px`;
|
|
1536
|
+
}
|
|
1195
1537
|
};
|
|
1196
1538
|
|
|
1197
1539
|
recalcPanelHeight();
|
|
@@ -1328,6 +1670,10 @@ export const createAgentExperience = (
|
|
|
1328
1670
|
destroyCallbacks.push(() => {
|
|
1329
1671
|
launcherButtonInstance?.destroy();
|
|
1330
1672
|
});
|
|
1673
|
+
} else if (customLauncherElement) {
|
|
1674
|
+
destroyCallbacks.push(() => {
|
|
1675
|
+
customLauncherElement?.remove();
|
|
1676
|
+
});
|
|
1331
1677
|
}
|
|
1332
1678
|
|
|
1333
1679
|
const controller: Controller = {
|
|
@@ -1335,6 +1681,7 @@ export const createAgentExperience = (
|
|
|
1335
1681
|
const previousToolCallConfig = config.toolCall;
|
|
1336
1682
|
config = { ...config, ...nextConfig };
|
|
1337
1683
|
applyThemeVariables(mount, config);
|
|
1684
|
+
applyFullHeightStyles();
|
|
1338
1685
|
|
|
1339
1686
|
// Update plugins
|
|
1340
1687
|
const newPlugins = pluginRegistry.getForInstance(config.plugins);
|
|
@@ -1350,15 +1697,38 @@ export const createAgentExperience = (
|
|
|
1350
1697
|
launcherButtonInstance.destroy();
|
|
1351
1698
|
launcherButtonInstance = null;
|
|
1352
1699
|
}
|
|
1700
|
+
if (config.launcher?.enabled === false && customLauncherElement) {
|
|
1701
|
+
customLauncherElement.remove();
|
|
1702
|
+
customLauncherElement = null;
|
|
1703
|
+
}
|
|
1353
1704
|
|
|
1354
|
-
if (config.launcher?.enabled !== false && !launcherButtonInstance) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1705
|
+
if (config.launcher?.enabled !== false && !launcherButtonInstance && !customLauncherElement) {
|
|
1706
|
+
// Check for launcher plugin when re-enabling
|
|
1707
|
+
const launcherPlugin = plugins.find(p => p.renderLauncher);
|
|
1708
|
+
if (launcherPlugin?.renderLauncher) {
|
|
1709
|
+
const customLauncher = launcherPlugin.renderLauncher({
|
|
1710
|
+
config,
|
|
1711
|
+
defaultRenderer: () => {
|
|
1712
|
+
const btn = createLauncherButton(config, toggleOpen);
|
|
1713
|
+
return btn.element;
|
|
1714
|
+
},
|
|
1715
|
+
onToggle: toggleOpen
|
|
1716
|
+
});
|
|
1717
|
+
if (customLauncher) {
|
|
1718
|
+
customLauncherElement = customLauncher;
|
|
1719
|
+
mount.appendChild(customLauncherElement);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (!customLauncherElement) {
|
|
1723
|
+
launcherButtonInstance = createLauncherButton(config, toggleOpen);
|
|
1724
|
+
mount.appendChild(launcherButtonInstance.element);
|
|
1725
|
+
}
|
|
1357
1726
|
}
|
|
1358
1727
|
|
|
1359
1728
|
if (launcherButtonInstance) {
|
|
1360
1729
|
launcherButtonInstance.update(config);
|
|
1361
1730
|
}
|
|
1731
|
+
// Note: Custom launcher updates are handled by the plugin's own logic
|
|
1362
1732
|
|
|
1363
1733
|
// Update panel header title and subtitle
|
|
1364
1734
|
if (headerTitle && config.launcher?.title !== undefined) {
|
|
@@ -1487,25 +1857,29 @@ export const createAgentExperience = (
|
|
|
1487
1857
|
closeButton.style.height = closeButtonSize;
|
|
1488
1858
|
closeButton.style.width = closeButtonSize;
|
|
1489
1859
|
|
|
1490
|
-
// Update placement if changed
|
|
1860
|
+
// Update placement if changed - move the wrapper (not just the button) to preserve tooltip
|
|
1861
|
+
const { closeButtonWrapper } = panelElements;
|
|
1491
1862
|
const isTopRight = closeButtonPlacement === "top-right";
|
|
1492
|
-
const
|
|
1863
|
+
const currentlyTopRight = closeButtonWrapper?.classList.contains("tvw-absolute");
|
|
1493
1864
|
|
|
1494
|
-
if (isTopRight !==
|
|
1495
|
-
// Placement changed - need to move
|
|
1496
|
-
|
|
1865
|
+
if (closeButtonWrapper && isTopRight !== currentlyTopRight) {
|
|
1866
|
+
// Placement changed - need to move wrapper and update classes
|
|
1867
|
+
closeButtonWrapper.remove();
|
|
1497
1868
|
|
|
1498
|
-
// Update classes
|
|
1869
|
+
// Update wrapper classes
|
|
1499
1870
|
if (isTopRight) {
|
|
1500
|
-
|
|
1871
|
+
closeButtonWrapper.className = "tvw-absolute tvw-top-4 tvw-right-4 tvw-z-50";
|
|
1501
1872
|
container.style.position = "relative";
|
|
1502
|
-
container.appendChild(
|
|
1873
|
+
container.appendChild(closeButtonWrapper);
|
|
1503
1874
|
} else {
|
|
1504
|
-
|
|
1505
|
-
|
|
1875
|
+
// Check if clear chat is inline to determine if we need ml-auto
|
|
1876
|
+
const clearChatPlacement = launcher.clearChat?.placement ?? "inline";
|
|
1877
|
+
const clearChatEnabled = launcher.clearChat?.enabled ?? true;
|
|
1878
|
+
closeButtonWrapper.className = (clearChatEnabled && clearChatPlacement === "inline") ? "" : "tvw-ml-auto";
|
|
1879
|
+
// Find header element
|
|
1506
1880
|
const header = container.querySelector(".tvw-border-b-cw-divider");
|
|
1507
1881
|
if (header) {
|
|
1508
|
-
header.appendChild(
|
|
1882
|
+
header.appendChild(closeButtonWrapper);
|
|
1509
1883
|
}
|
|
1510
1884
|
}
|
|
1511
1885
|
}
|
|
@@ -1576,7 +1950,6 @@ export const createAgentExperience = (
|
|
|
1576
1950
|
}
|
|
1577
1951
|
|
|
1578
1952
|
// Update tooltip
|
|
1579
|
-
const { closeButtonWrapper } = panelElements;
|
|
1580
1953
|
const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
|
|
1581
1954
|
const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
|
|
1582
1955
|
|
|
@@ -1652,10 +2025,54 @@ export const createAgentExperience = (
|
|
|
1652
2025
|
if (clearChatButton) {
|
|
1653
2026
|
const clearChatConfig = launcher.clearChat ?? {};
|
|
1654
2027
|
const clearChatEnabled = clearChatConfig.enabled ?? true;
|
|
2028
|
+
const clearChatPlacement = clearChatConfig.placement ?? "inline";
|
|
1655
2029
|
|
|
1656
2030
|
// Show/hide button based on enabled state
|
|
1657
2031
|
if (clearChatButtonWrapper) {
|
|
1658
2032
|
clearChatButtonWrapper.style.display = clearChatEnabled ? "" : "none";
|
|
2033
|
+
|
|
2034
|
+
// Update placement if changed
|
|
2035
|
+
const isTopRight = clearChatPlacement === "top-right";
|
|
2036
|
+
const currentlyTopRight = clearChatButtonWrapper.classList.contains("tvw-absolute");
|
|
2037
|
+
|
|
2038
|
+
if (isTopRight !== currentlyTopRight && clearChatEnabled) {
|
|
2039
|
+
clearChatButtonWrapper.remove();
|
|
2040
|
+
|
|
2041
|
+
if (isTopRight) {
|
|
2042
|
+
// Don't use tvw-clear-chat-button-wrapper class for top-right mode as its
|
|
2043
|
+
// display: inline-flex causes alignment issues with the close button
|
|
2044
|
+
clearChatButtonWrapper.className = "tvw-absolute tvw-top-4 tvw-z-50";
|
|
2045
|
+
// Position to the left of the close button (which is at right: 1rem/16px)
|
|
2046
|
+
// Close button is ~32px wide, plus small gap = 48px from right
|
|
2047
|
+
clearChatButtonWrapper.style.right = "48px";
|
|
2048
|
+
container.style.position = "relative";
|
|
2049
|
+
container.appendChild(clearChatButtonWrapper);
|
|
2050
|
+
} else {
|
|
2051
|
+
clearChatButtonWrapper.className = "tvw-relative tvw-ml-auto tvw-clear-chat-button-wrapper";
|
|
2052
|
+
// Clear the inline right style when switching back to inline mode
|
|
2053
|
+
clearChatButtonWrapper.style.right = "";
|
|
2054
|
+
// Find header and insert before close button
|
|
2055
|
+
const header = container.querySelector(".tvw-border-b-cw-divider");
|
|
2056
|
+
const closeButtonWrapperEl = panelElements.closeButtonWrapper;
|
|
2057
|
+
if (header && closeButtonWrapperEl && closeButtonWrapperEl.parentElement === header) {
|
|
2058
|
+
header.insertBefore(clearChatButtonWrapper, closeButtonWrapperEl);
|
|
2059
|
+
} else if (header) {
|
|
2060
|
+
header.appendChild(clearChatButtonWrapper);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// Also update close button's ml-auto class based on clear chat position
|
|
2065
|
+
const closeButtonWrapperEl = panelElements.closeButtonWrapper;
|
|
2066
|
+
if (closeButtonWrapperEl && !closeButtonWrapperEl.classList.contains("tvw-absolute")) {
|
|
2067
|
+
if (isTopRight) {
|
|
2068
|
+
// Clear chat moved to top-right, close needs ml-auto
|
|
2069
|
+
closeButtonWrapperEl.classList.add("tvw-ml-auto");
|
|
2070
|
+
} else {
|
|
2071
|
+
// Clear chat is inline, close doesn't need ml-auto
|
|
2072
|
+
closeButtonWrapperEl.classList.remove("tvw-ml-auto");
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
1659
2076
|
}
|
|
1660
2077
|
|
|
1661
2078
|
if (clearChatEnabled) {
|
|
@@ -2284,6 +2701,7 @@ export const createAgentExperience = (
|
|
|
2284
2701
|
destroyCallbacks.forEach((cb) => cb());
|
|
2285
2702
|
wrapper.remove();
|
|
2286
2703
|
launcherButtonInstance?.destroy();
|
|
2704
|
+
customLauncherElement?.remove();
|
|
2287
2705
|
if (closeHandler) {
|
|
2288
2706
|
closeButton.removeEventListener("click", closeHandler);
|
|
2289
2707
|
}
|