vanilla-agent 1.9.0 → 1.11.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 +303 -8
- package/dist/index.cjs +46 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +581 -1
- package/dist/index.d.ts +581 -1
- package/dist/index.global.js +69 -30
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +141 -12
- package/src/components/composer-builder.ts +366 -0
- package/src/components/forms.ts +1 -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/components/registry.ts +87 -0
- package/src/defaults.ts +49 -1
- package/src/index.ts +64 -2
- package/src/plugins/registry.ts +1 -0
- package/src/plugins/types.ts +1 -0
- package/src/runtime/init.ts +26 -0
- package/src/types.ts +381 -0
- package/src/ui.ts +521 -40
- package/src/utils/component-middleware.ts +137 -0
- package/src/utils/component-parser.ts +119 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/dom.ts +1 -0
- package/src/utils/formatting.ts +33 -8
- package/src/utils/positioning.ts +1 -0
- package/src/utils/theme.ts +1 -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";
|
|
@@ -31,6 +34,13 @@ import {
|
|
|
31
34
|
defaultActionHandlers,
|
|
32
35
|
defaultJsonActionParser
|
|
33
36
|
} from "./utils/actions";
|
|
37
|
+
import { createLocalStorageAdapter } from "./utils/storage";
|
|
38
|
+
import { componentRegistry } from "./components/registry";
|
|
39
|
+
import {
|
|
40
|
+
renderComponentDirective,
|
|
41
|
+
extractComponentDirectiveFromMessage,
|
|
42
|
+
hasComponentDirective
|
|
43
|
+
} from "./utils/component-middleware";
|
|
34
44
|
|
|
35
45
|
// Default localStorage key for chat history (automatically cleared on clear chat)
|
|
36
46
|
const DEFAULT_CHAT_HISTORY_STORAGE_KEY = "vanilla-agent-chat-history";
|
|
@@ -132,10 +142,15 @@ export const createAgentExperience = (
|
|
|
132
142
|
|
|
133
143
|
// Get plugins for this instance
|
|
134
144
|
const plugins = pluginRegistry.getForInstance(config.plugins);
|
|
145
|
+
|
|
146
|
+
// Register components from config
|
|
147
|
+
if (config.components) {
|
|
148
|
+
componentRegistry.registerAll(config.components);
|
|
149
|
+
}
|
|
135
150
|
const eventBus = createEventBus<AgentWidgetControllerEventMap>();
|
|
136
151
|
|
|
137
|
-
const storageAdapter: AgentWidgetStorageAdapter
|
|
138
|
-
config.storageAdapter;
|
|
152
|
+
const storageAdapter: AgentWidgetStorageAdapter =
|
|
153
|
+
config.storageAdapter ?? createLocalStorageAdapter();
|
|
139
154
|
let persistentMetadata: Record<string, unknown> = {};
|
|
140
155
|
let pendingStoredState: Promise<AgentWidgetStoredState | null> | null = null;
|
|
141
156
|
|
|
@@ -211,7 +226,7 @@ export const createAgentExperience = (
|
|
|
211
226
|
|
|
212
227
|
const { wrapper, panel } = createWrapper(config);
|
|
213
228
|
const panelElements = buildPanel(config, launcherEnabled);
|
|
214
|
-
|
|
229
|
+
let {
|
|
215
230
|
container,
|
|
216
231
|
body,
|
|
217
232
|
messagesWrapper,
|
|
@@ -226,16 +241,297 @@ export const createAgentExperience = (
|
|
|
226
241
|
closeButton,
|
|
227
242
|
iconHolder,
|
|
228
243
|
headerTitle,
|
|
229
|
-
headerSubtitle
|
|
244
|
+
headerSubtitle,
|
|
245
|
+
header,
|
|
246
|
+
footer
|
|
230
247
|
} = panelElements;
|
|
231
248
|
|
|
232
249
|
// Use mutable references for mic button so we can update them dynamically
|
|
233
250
|
let micButton: HTMLButtonElement | null = panelElements.micButton;
|
|
234
251
|
let micButtonWrapper: HTMLElement | null = panelElements.micButtonWrapper;
|
|
235
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
|
+
|
|
236
395
|
panel.appendChild(container);
|
|
237
396
|
mount.appendChild(wrapper);
|
|
238
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
|
+
|
|
239
535
|
const destroyCallbacks: Array<() => void> = [];
|
|
240
536
|
const suggestionsManager = createSuggestions(suggestions);
|
|
241
537
|
let closeHandler: (() => void) | null = null;
|
|
@@ -302,11 +598,17 @@ export const createAgentExperience = (
|
|
|
302
598
|
: [];
|
|
303
599
|
|
|
304
600
|
function persistState(messagesOverride?: AgentWidgetMessage[]) {
|
|
305
|
-
if (!storageAdapter?.save
|
|
601
|
+
if (!storageAdapter?.save) return;
|
|
602
|
+
|
|
603
|
+
// Allow saving even if session doesn't exist yet (for metadata during init)
|
|
604
|
+
const messages = messagesOverride
|
|
605
|
+
? stripStreamingFromMessages(messagesOverride)
|
|
606
|
+
: session
|
|
607
|
+
? getMessagesForPersistence()
|
|
608
|
+
: [];
|
|
609
|
+
|
|
306
610
|
const payload = {
|
|
307
|
-
messages
|
|
308
|
-
? stripStreamingFromMessages(messagesOverride)
|
|
309
|
-
: getMessagesForPersistence(),
|
|
611
|
+
messages,
|
|
310
612
|
metadata: persistentMetadata
|
|
311
613
|
};
|
|
312
614
|
try {
|
|
@@ -488,6 +790,9 @@ export const createAgentExperience = (
|
|
|
488
790
|
return false;
|
|
489
791
|
});
|
|
490
792
|
|
|
793
|
+
// Get message layout config
|
|
794
|
+
const messageLayoutConfig = config.layout?.messages;
|
|
795
|
+
|
|
491
796
|
if (matchingPlugin) {
|
|
492
797
|
if (message.variant === "reasoning" && message.reasoning && matchingPlugin.renderReasoning) {
|
|
493
798
|
if (!showReasoning) return;
|
|
@@ -507,7 +812,7 @@ export const createAgentExperience = (
|
|
|
507
812
|
bubble = matchingPlugin.renderMessage({
|
|
508
813
|
message,
|
|
509
814
|
defaultRenderer: () => {
|
|
510
|
-
const b = createStandardBubble(message, transform);
|
|
815
|
+
const b = createStandardBubble(message, transform, messageLayoutConfig);
|
|
511
816
|
if (message.role !== "user") {
|
|
512
817
|
enhanceWithForms(b, message, config, session);
|
|
513
818
|
}
|
|
@@ -518,6 +823,51 @@ export const createAgentExperience = (
|
|
|
518
823
|
}
|
|
519
824
|
}
|
|
520
825
|
|
|
826
|
+
// Check for component directive if no plugin handled it
|
|
827
|
+
if (!bubble && message.role === "assistant" && !message.variant) {
|
|
828
|
+
const enableComponentStreaming = config.enableComponentStreaming !== false; // Default to true
|
|
829
|
+
if (enableComponentStreaming && hasComponentDirective(message)) {
|
|
830
|
+
const directive = extractComponentDirectiveFromMessage(message);
|
|
831
|
+
if (directive) {
|
|
832
|
+
const componentBubble = renderComponentDirective(directive, {
|
|
833
|
+
config,
|
|
834
|
+
message,
|
|
835
|
+
transform
|
|
836
|
+
});
|
|
837
|
+
if (componentBubble) {
|
|
838
|
+
// Wrap component in standard bubble styling
|
|
839
|
+
const wrapper = document.createElement("div");
|
|
840
|
+
wrapper.className = [
|
|
841
|
+
"vanilla-message-bubble",
|
|
842
|
+
"tvw-max-w-[85%]",
|
|
843
|
+
"tvw-rounded-2xl",
|
|
844
|
+
"tvw-bg-cw-surface",
|
|
845
|
+
"tvw-border",
|
|
846
|
+
"tvw-border-cw-message-border",
|
|
847
|
+
"tvw-p-4"
|
|
848
|
+
].join(" ");
|
|
849
|
+
wrapper.setAttribute("data-message-id", message.id);
|
|
850
|
+
|
|
851
|
+
// Add text content above component if present (combined text+component response)
|
|
852
|
+
if (message.content && message.content.trim()) {
|
|
853
|
+
const textDiv = document.createElement("div");
|
|
854
|
+
textDiv.className = "tvw-mb-3 tvw-text-sm tvw-leading-relaxed";
|
|
855
|
+
textDiv.innerHTML = transform({
|
|
856
|
+
text: message.content,
|
|
857
|
+
message,
|
|
858
|
+
streaming: Boolean(message.streaming),
|
|
859
|
+
raw: message.rawContent
|
|
860
|
+
});
|
|
861
|
+
wrapper.appendChild(textDiv);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
wrapper.appendChild(componentBubble);
|
|
865
|
+
bubble = wrapper;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
521
871
|
// Fallback to default rendering if plugin returned null or no plugin matched
|
|
522
872
|
if (!bubble) {
|
|
523
873
|
if (message.variant === "reasoning" && message.reasoning) {
|
|
@@ -527,8 +877,24 @@ export const createAgentExperience = (
|
|
|
527
877
|
if (!showToolCalls) return;
|
|
528
878
|
bubble = createToolBubble(message, config);
|
|
529
879
|
} else {
|
|
530
|
-
|
|
531
|
-
|
|
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) {
|
|
532
898
|
enhanceWithForms(bubble, message, config, session);
|
|
533
899
|
}
|
|
534
900
|
}
|
|
@@ -599,6 +965,8 @@ export const createAgentExperience = (
|
|
|
599
965
|
// Hide launcher button when widget is open
|
|
600
966
|
if (launcherButtonInstance) {
|
|
601
967
|
launcherButtonInstance.element.style.display = "none";
|
|
968
|
+
} else if (customLauncherElement) {
|
|
969
|
+
customLauncherElement.style.display = "none";
|
|
602
970
|
}
|
|
603
971
|
} else {
|
|
604
972
|
wrapper.classList.add("tvw-pointer-events-none", "tvw-opacity-0");
|
|
@@ -607,6 +975,8 @@ export const createAgentExperience = (
|
|
|
607
975
|
// Show launcher button when widget is closed
|
|
608
976
|
if (launcherButtonInstance) {
|
|
609
977
|
launcherButtonInstance.element.style.display = "";
|
|
978
|
+
} else if (customLauncherElement) {
|
|
979
|
+
customLauncherElement.style.display = "";
|
|
610
980
|
}
|
|
611
981
|
}
|
|
612
982
|
};
|
|
@@ -1100,12 +1470,36 @@ export const createAgentExperience = (
|
|
|
1100
1470
|
setOpenState(!open, "user");
|
|
1101
1471
|
};
|
|
1102
1472
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
+
}
|
|
1106
1498
|
|
|
1107
1499
|
if (launcherButtonInstance) {
|
|
1108
1500
|
mount.appendChild(launcherButtonInstance.element);
|
|
1501
|
+
} else if (customLauncherElement) {
|
|
1502
|
+
mount.appendChild(customLauncherElement);
|
|
1109
1503
|
}
|
|
1110
1504
|
updateOpenState();
|
|
1111
1505
|
suggestionsManager.render(config.suggestionChips, session, textarea, undefined, config.suggestionChipsConfig);
|
|
@@ -1115,20 +1509,31 @@ export const createAgentExperience = (
|
|
|
1115
1509
|
maybeRestoreVoiceFromMetadata();
|
|
1116
1510
|
|
|
1117
1511
|
const recalcPanelHeight = () => {
|
|
1512
|
+
const sidebarMode = config.launcher?.sidebarMode ?? false;
|
|
1513
|
+
const fullHeight = sidebarMode || (config.launcher?.fullHeight ?? false);
|
|
1514
|
+
|
|
1118
1515
|
if (!launcherEnabled) {
|
|
1119
1516
|
panel.style.height = "";
|
|
1120
1517
|
panel.style.width = "";
|
|
1121
1518
|
return;
|
|
1122
1519
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
+
}
|
|
1132
1537
|
};
|
|
1133
1538
|
|
|
1134
1539
|
recalcPanelHeight();
|
|
@@ -1265,6 +1670,10 @@ export const createAgentExperience = (
|
|
|
1265
1670
|
destroyCallbacks.push(() => {
|
|
1266
1671
|
launcherButtonInstance?.destroy();
|
|
1267
1672
|
});
|
|
1673
|
+
} else if (customLauncherElement) {
|
|
1674
|
+
destroyCallbacks.push(() => {
|
|
1675
|
+
customLauncherElement?.remove();
|
|
1676
|
+
});
|
|
1268
1677
|
}
|
|
1269
1678
|
|
|
1270
1679
|
const controller: Controller = {
|
|
@@ -1272,6 +1681,7 @@ export const createAgentExperience = (
|
|
|
1272
1681
|
const previousToolCallConfig = config.toolCall;
|
|
1273
1682
|
config = { ...config, ...nextConfig };
|
|
1274
1683
|
applyThemeVariables(mount, config);
|
|
1684
|
+
applyFullHeightStyles();
|
|
1275
1685
|
|
|
1276
1686
|
// Update plugins
|
|
1277
1687
|
const newPlugins = pluginRegistry.getForInstance(config.plugins);
|
|
@@ -1287,15 +1697,38 @@ export const createAgentExperience = (
|
|
|
1287
1697
|
launcherButtonInstance.destroy();
|
|
1288
1698
|
launcherButtonInstance = null;
|
|
1289
1699
|
}
|
|
1700
|
+
if (config.launcher?.enabled === false && customLauncherElement) {
|
|
1701
|
+
customLauncherElement.remove();
|
|
1702
|
+
customLauncherElement = null;
|
|
1703
|
+
}
|
|
1290
1704
|
|
|
1291
|
-
if (config.launcher?.enabled !== false && !launcherButtonInstance) {
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
+
}
|
|
1294
1726
|
}
|
|
1295
1727
|
|
|
1296
1728
|
if (launcherButtonInstance) {
|
|
1297
1729
|
launcherButtonInstance.update(config);
|
|
1298
1730
|
}
|
|
1731
|
+
// Note: Custom launcher updates are handled by the plugin's own logic
|
|
1299
1732
|
|
|
1300
1733
|
// Update panel header title and subtitle
|
|
1301
1734
|
if (headerTitle && config.launcher?.title !== undefined) {
|
|
@@ -1424,25 +1857,29 @@ export const createAgentExperience = (
|
|
|
1424
1857
|
closeButton.style.height = closeButtonSize;
|
|
1425
1858
|
closeButton.style.width = closeButtonSize;
|
|
1426
1859
|
|
|
1427
|
-
// Update placement if changed
|
|
1860
|
+
// Update placement if changed - move the wrapper (not just the button) to preserve tooltip
|
|
1861
|
+
const { closeButtonWrapper } = panelElements;
|
|
1428
1862
|
const isTopRight = closeButtonPlacement === "top-right";
|
|
1429
|
-
const
|
|
1863
|
+
const currentlyTopRight = closeButtonWrapper?.classList.contains("tvw-absolute");
|
|
1430
1864
|
|
|
1431
|
-
if (isTopRight !==
|
|
1432
|
-
// Placement changed - need to move
|
|
1433
|
-
|
|
1865
|
+
if (closeButtonWrapper && isTopRight !== currentlyTopRight) {
|
|
1866
|
+
// Placement changed - need to move wrapper and update classes
|
|
1867
|
+
closeButtonWrapper.remove();
|
|
1434
1868
|
|
|
1435
|
-
// Update classes
|
|
1869
|
+
// Update wrapper classes
|
|
1436
1870
|
if (isTopRight) {
|
|
1437
|
-
|
|
1871
|
+
closeButtonWrapper.className = "tvw-absolute tvw-top-4 tvw-right-4 tvw-z-50";
|
|
1438
1872
|
container.style.position = "relative";
|
|
1439
|
-
container.appendChild(
|
|
1873
|
+
container.appendChild(closeButtonWrapper);
|
|
1440
1874
|
} else {
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
|
1443
1880
|
const header = container.querySelector(".tvw-border-b-cw-divider");
|
|
1444
1881
|
if (header) {
|
|
1445
|
-
header.appendChild(
|
|
1882
|
+
header.appendChild(closeButtonWrapper);
|
|
1446
1883
|
}
|
|
1447
1884
|
}
|
|
1448
1885
|
}
|
|
@@ -1513,7 +1950,6 @@ export const createAgentExperience = (
|
|
|
1513
1950
|
}
|
|
1514
1951
|
|
|
1515
1952
|
// Update tooltip
|
|
1516
|
-
const { closeButtonWrapper } = panelElements;
|
|
1517
1953
|
const closeButtonTooltipText = launcher.closeButtonTooltipText ?? "Close chat";
|
|
1518
1954
|
const closeButtonShowTooltip = launcher.closeButtonShowTooltip ?? true;
|
|
1519
1955
|
|
|
@@ -1589,10 +2025,54 @@ export const createAgentExperience = (
|
|
|
1589
2025
|
if (clearChatButton) {
|
|
1590
2026
|
const clearChatConfig = launcher.clearChat ?? {};
|
|
1591
2027
|
const clearChatEnabled = clearChatConfig.enabled ?? true;
|
|
2028
|
+
const clearChatPlacement = clearChatConfig.placement ?? "inline";
|
|
1592
2029
|
|
|
1593
2030
|
// Show/hide button based on enabled state
|
|
1594
2031
|
if (clearChatButtonWrapper) {
|
|
1595
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
|
+
}
|
|
1596
2076
|
}
|
|
1597
2077
|
|
|
1598
2078
|
if (clearChatEnabled) {
|
|
@@ -2221,6 +2701,7 @@ export const createAgentExperience = (
|
|
|
2221
2701
|
destroyCallbacks.forEach((cb) => cb());
|
|
2222
2702
|
wrapper.remove();
|
|
2223
2703
|
launcherButtonInstance?.destroy();
|
|
2704
|
+
customLauncherElement?.remove();
|
|
2224
2705
|
if (closeHandler) {
|
|
2225
2706
|
closeButton.removeEventListener("click", closeHandler);
|
|
2226
2707
|
}
|