@runtypelabs/persona 1.36.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.
Files changed (61) hide show
  1. package/README.md +1080 -0
  2. package/dist/index.cjs +140 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2626 -0
  5. package/dist/index.d.ts +2626 -0
  6. package/dist/index.global.js +1843 -0
  7. package/dist/index.global.js.map +1 -0
  8. package/dist/index.js +140 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/install.global.js +2 -0
  11. package/dist/install.global.js.map +1 -0
  12. package/dist/widget.css +1627 -0
  13. package/package.json +79 -0
  14. package/src/@types/idiomorph.d.ts +37 -0
  15. package/src/client.test.ts +387 -0
  16. package/src/client.ts +1589 -0
  17. package/src/components/composer-builder.ts +530 -0
  18. package/src/components/feedback.ts +379 -0
  19. package/src/components/forms.ts +170 -0
  20. package/src/components/header-builder.ts +455 -0
  21. package/src/components/header-layouts.ts +303 -0
  22. package/src/components/launcher.ts +193 -0
  23. package/src/components/message-bubble.ts +528 -0
  24. package/src/components/messages.ts +54 -0
  25. package/src/components/panel.ts +204 -0
  26. package/src/components/reasoning-bubble.ts +144 -0
  27. package/src/components/registry.ts +87 -0
  28. package/src/components/suggestions.ts +97 -0
  29. package/src/components/tool-bubble.ts +288 -0
  30. package/src/defaults.ts +321 -0
  31. package/src/index.ts +175 -0
  32. package/src/install.ts +284 -0
  33. package/src/plugins/registry.ts +77 -0
  34. package/src/plugins/types.ts +95 -0
  35. package/src/postprocessors.ts +194 -0
  36. package/src/runtime/init.ts +162 -0
  37. package/src/session.ts +376 -0
  38. package/src/styles/tailwind.css +20 -0
  39. package/src/styles/widget.css +1627 -0
  40. package/src/types.ts +1635 -0
  41. package/src/ui.ts +3341 -0
  42. package/src/utils/actions.ts +227 -0
  43. package/src/utils/attachment-manager.ts +384 -0
  44. package/src/utils/code-generators.test.ts +500 -0
  45. package/src/utils/code-generators.ts +1806 -0
  46. package/src/utils/component-middleware.ts +137 -0
  47. package/src/utils/component-parser.ts +119 -0
  48. package/src/utils/constants.ts +16 -0
  49. package/src/utils/content.ts +306 -0
  50. package/src/utils/dom.ts +25 -0
  51. package/src/utils/events.ts +41 -0
  52. package/src/utils/formatting.test.ts +166 -0
  53. package/src/utils/formatting.ts +470 -0
  54. package/src/utils/icons.ts +92 -0
  55. package/src/utils/message-id.ts +37 -0
  56. package/src/utils/morph.ts +36 -0
  57. package/src/utils/positioning.ts +17 -0
  58. package/src/utils/storage.ts +72 -0
  59. package/src/utils/theme.ts +105 -0
  60. package/src/widget.css +1 -0
  61. package/widget.css +1 -0
@@ -0,0 +1,204 @@
1
+ import { createElement } from "../utils/dom";
2
+ import { AgentWidgetConfig } from "../types";
3
+ import { positionMap } from "../utils/positioning";
4
+ import { buildHeader, attachHeaderToContainer, HeaderElements } from "./header-builder";
5
+ import { buildHeaderWithLayout } from "./header-layouts";
6
+ import { buildComposer, ComposerElements } from "./composer-builder";
7
+
8
+ export interface PanelWrapper {
9
+ wrapper: HTMLElement;
10
+ panel: HTMLElement;
11
+ }
12
+
13
+ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
14
+ const launcherEnabled = config?.launcher?.enabled ?? true;
15
+
16
+ if (!launcherEnabled) {
17
+ // For inline embed mode, use flex layout to ensure the widget fills its container
18
+ // and only the chat messages area scrolls
19
+ const wrapper = createElement(
20
+ "div",
21
+ "tvw-relative tvw-h-full tvw-flex tvw-flex-col tvw-flex-1 tvw-min-h-0"
22
+ );
23
+ const panel = createElement(
24
+ "div",
25
+ "tvw-relative tvw-flex-1 tvw-flex tvw-flex-col tvw-min-h-0"
26
+ );
27
+
28
+ // Apply width from config, defaulting to 100% for inline embed mode
29
+ const inlineWidth = config?.launcher?.width ?? "100%";
30
+ wrapper.style.width = inlineWidth;
31
+ panel.style.width = "100%";
32
+
33
+ wrapper.appendChild(panel);
34
+ return { wrapper, panel };
35
+ }
36
+
37
+ const launcher = config?.launcher ?? {};
38
+ const position =
39
+ launcher.position && positionMap[launcher.position]
40
+ ? positionMap[launcher.position]
41
+ : positionMap["bottom-right"];
42
+
43
+ const wrapper = createElement(
44
+ "div",
45
+ `tvw-widget-wrapper tvw-fixed ${position} tvw-z-50 tvw-transition`
46
+ );
47
+
48
+ const panel = createElement(
49
+ "div",
50
+ "tvw-widget-panel tvw-relative tvw-min-h-[320px]"
51
+ );
52
+ const launcherWidth = config?.launcher?.width ?? config?.launcherWidth;
53
+ const width = launcherWidth ?? "min(400px, calc(100vw - 24px))";
54
+ panel.style.width = width;
55
+ panel.style.maxWidth = width;
56
+
57
+ wrapper.appendChild(panel);
58
+ return { wrapper, panel };
59
+ };
60
+
61
+ export interface PanelElements {
62
+ container: HTMLElement;
63
+ body: HTMLElement;
64
+ messagesWrapper: HTMLElement;
65
+ suggestions: HTMLElement;
66
+ textarea: HTMLTextAreaElement;
67
+ sendButton: HTMLButtonElement;
68
+ sendButtonWrapper: HTMLElement;
69
+ micButton: HTMLButtonElement | null;
70
+ micButtonWrapper: HTMLElement | null;
71
+ composerForm: HTMLFormElement;
72
+ statusText: HTMLElement;
73
+ introTitle: HTMLElement;
74
+ introSubtitle: HTMLElement;
75
+ closeButton: HTMLButtonElement;
76
+ closeButtonWrapper: HTMLElement;
77
+ clearChatButton: HTMLButtonElement | null;
78
+ clearChatButtonWrapper: HTMLElement | null;
79
+ iconHolder: HTMLElement;
80
+ headerTitle: HTMLElement;
81
+ headerSubtitle: HTMLElement;
82
+ // Exposed for potential header replacement
83
+ header: HTMLElement;
84
+ footer: HTMLElement;
85
+ // Attachment elements
86
+ attachmentButton: HTMLButtonElement | null;
87
+ attachmentButtonWrapper: HTMLElement | null;
88
+ attachmentInput: HTMLInputElement | null;
89
+ attachmentPreviewsContainer: HTMLElement | null;
90
+ // Actions row layout elements
91
+ actionsRow: HTMLElement;
92
+ leftActions: HTMLElement;
93
+ rightActions: HTMLElement;
94
+ }
95
+
96
+ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelElements => {
97
+ // Use flex-1 and min-h-0 to ensure the container fills its parent and allows
98
+ // the body (chat messages area) to scroll while header/footer stay fixed
99
+ const container = createElement(
100
+ "div",
101
+ "tvw-widget-container tvw-flex tvw-h-full tvw-w-full tvw-flex-1 tvw-min-h-0 tvw-flex-col tvw-bg-cw-surface tvw-text-cw-primary tvw-rounded-2xl tvw-overflow-hidden tvw-border tvw-border-cw-border"
102
+ );
103
+
104
+ // Build header using layout config if available, otherwise use standard builder
105
+ const headerLayoutConfig = config?.layout?.header;
106
+ const showHeader = config?.layout?.showHeader !== false; // default to true
107
+ const headerElements: HeaderElements = headerLayoutConfig
108
+ ? buildHeaderWithLayout(config!, headerLayoutConfig, { showClose })
109
+ : buildHeader({ config, showClose });
110
+
111
+ // Build body with intro card and messages wrapper
112
+ const body = createElement(
113
+ "div",
114
+ "tvw-widget-body tvw-flex tvw-flex-1 tvw-min-h-0 tvw-flex-col tvw-gap-6 tvw-overflow-y-auto tvw-bg-cw-container tvw-px-6 tvw-py-6"
115
+ );
116
+ body.id = "persona-scroll-container";
117
+
118
+ const introCard = createElement(
119
+ "div",
120
+ "tvw-rounded-2xl tvw-bg-cw-surface tvw-p-6 tvw-shadow-sm"
121
+ );
122
+ const introTitle = createElement(
123
+ "h2",
124
+ "tvw-text-lg tvw-font-semibold tvw-text-cw-primary"
125
+ );
126
+ introTitle.textContent = config?.copy?.welcomeTitle ?? "Hello 👋";
127
+ const introSubtitle = createElement(
128
+ "p",
129
+ "tvw-mt-2 tvw-text-sm tvw-text-cw-muted"
130
+ );
131
+ introSubtitle.textContent =
132
+ config?.copy?.welcomeSubtitle ??
133
+ "Ask anything about your account or products.";
134
+ introCard.append(introTitle, introSubtitle);
135
+
136
+ const messagesWrapper = createElement(
137
+ "div",
138
+ "tvw-flex tvw-flex-col tvw-gap-3"
139
+ );
140
+
141
+ body.append(introCard, messagesWrapper);
142
+
143
+ // Build composer/footer using extracted builder
144
+ const composerElements: ComposerElements = buildComposer({ config });
145
+ const showFooter = config?.layout?.showFooter !== false; // default to true
146
+
147
+ // Assemble container with header, body, and footer
148
+ if (showHeader) {
149
+ attachHeaderToContainer(container, headerElements, config);
150
+ } else {
151
+ // Hide header completely
152
+ headerElements.header.style.display = 'none';
153
+ attachHeaderToContainer(container, headerElements, config);
154
+ }
155
+
156
+ container.append(body);
157
+
158
+ if (showFooter) {
159
+ container.append(composerElements.footer);
160
+ } else {
161
+ // Hide footer completely
162
+ composerElements.footer.style.display = 'none';
163
+ container.append(composerElements.footer);
164
+ }
165
+
166
+ return {
167
+ container,
168
+ body,
169
+ messagesWrapper,
170
+ suggestions: composerElements.suggestions,
171
+ textarea: composerElements.textarea,
172
+ sendButton: composerElements.sendButton,
173
+ sendButtonWrapper: composerElements.sendButtonWrapper,
174
+ micButton: composerElements.micButton,
175
+ micButtonWrapper: composerElements.micButtonWrapper,
176
+ composerForm: composerElements.composerForm,
177
+ statusText: composerElements.statusText,
178
+ introTitle,
179
+ introSubtitle,
180
+ closeButton: headerElements.closeButton,
181
+ closeButtonWrapper: headerElements.closeButtonWrapper,
182
+ clearChatButton: headerElements.clearChatButton,
183
+ clearChatButtonWrapper: headerElements.clearChatButtonWrapper,
184
+ iconHolder: headerElements.iconHolder,
185
+ headerTitle: headerElements.headerTitle,
186
+ headerSubtitle: headerElements.headerSubtitle,
187
+ header: headerElements.header,
188
+ footer: composerElements.footer,
189
+ // Attachment elements
190
+ attachmentButton: composerElements.attachmentButton,
191
+ attachmentButtonWrapper: composerElements.attachmentButtonWrapper,
192
+ attachmentInput: composerElements.attachmentInput,
193
+ attachmentPreviewsContainer: composerElements.attachmentPreviewsContainer,
194
+ // Actions row layout elements
195
+ actionsRow: composerElements.actionsRow,
196
+ leftActions: composerElements.leftActions,
197
+ rightActions: composerElements.rightActions
198
+ };
199
+ };
200
+
201
+ // Re-export builder types and functions for plugin use
202
+ export { buildHeader, buildComposer, attachHeaderToContainer };
203
+ export type { HeaderElements, HeaderBuildContext } from "./header-builder";
204
+ export type { ComposerElements, ComposerBuildContext } from "./composer-builder";
@@ -0,0 +1,144 @@
1
+ import { createElement } from "../utils/dom";
2
+ import { AgentWidgetMessage } from "../types";
3
+ import { describeReasonStatus } from "../utils/formatting";
4
+ import { renderLucideIcon } from "../utils/icons";
5
+
6
+ // Expansion state per widget instance
7
+ export const reasoningExpansionState = new Set<string>();
8
+
9
+ // Helper function to update reasoning bubble UI after expansion state changes
10
+ export const updateReasoningBubbleUI = (messageId: string, bubble: HTMLElement): void => {
11
+ const expanded = reasoningExpansionState.has(messageId);
12
+ const header = bubble.querySelector('button[data-expand-header="true"]') as HTMLElement;
13
+ const content = bubble.querySelector('.tvw-border-t') as HTMLElement;
14
+
15
+ if (!header || !content) return;
16
+
17
+ header.setAttribute("aria-expanded", expanded ? "true" : "false");
18
+
19
+ // Find toggle icon container - it's the direct child div of headerMeta (which has tvw-ml-auto)
20
+ const headerMeta = header.querySelector('.tvw-ml-auto') as HTMLElement;
21
+ const toggleIcon = headerMeta?.querySelector(':scope > .tvw-flex.tvw-items-center') as HTMLElement;
22
+ if (toggleIcon) {
23
+ toggleIcon.innerHTML = "";
24
+ const iconColor = "currentColor";
25
+ const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
26
+ if (chevronIcon) {
27
+ toggleIcon.appendChild(chevronIcon);
28
+ } else {
29
+ toggleIcon.textContent = expanded ? "Hide" : "Show";
30
+ }
31
+ }
32
+
33
+ content.style.display = expanded ? "" : "none";
34
+ };
35
+
36
+ export const createReasoningBubble = (message: AgentWidgetMessage): HTMLElement => {
37
+ const reasoning = message.reasoning;
38
+ const bubble = createElement(
39
+ "div",
40
+ [
41
+ "vanilla-message-bubble",
42
+ "vanilla-reasoning-bubble",
43
+ "tvw-w-full",
44
+ "tvw-max-w-[85%]",
45
+ "tvw-rounded-2xl",
46
+ "tvw-bg-cw-surface",
47
+ "tvw-border",
48
+ "tvw-border-cw-message-border",
49
+ "tvw-text-cw-primary",
50
+ "tvw-shadow-sm",
51
+ "tvw-overflow-hidden",
52
+ "tvw-px-0",
53
+ "tvw-py-0"
54
+ ].join(" ")
55
+ );
56
+ // Set id for idiomorph matching
57
+ bubble.id = `bubble-${message.id}`;
58
+ bubble.setAttribute("data-message-id", message.id);
59
+
60
+ if (!reasoning) {
61
+ return bubble;
62
+ }
63
+
64
+ let expanded = reasoningExpansionState.has(message.id);
65
+ const header = createElement(
66
+ "button",
67
+ "tvw-flex tvw-w-full tvw-items-center tvw-justify-between tvw-gap-3 tvw-bg-transparent tvw-px-4 tvw-py-3 tvw-text-left tvw-cursor-pointer tvw-border-none"
68
+ ) as HTMLButtonElement;
69
+ header.type = "button";
70
+ header.setAttribute("aria-expanded", expanded ? "true" : "false");
71
+ header.setAttribute("data-expand-header", "true");
72
+ header.setAttribute("data-bubble-type", "reasoning");
73
+
74
+ const headerContent = createElement("div", "tvw-flex tvw-flex-col tvw-text-left");
75
+ const title = createElement("span", "tvw-text-xs tvw-text-cw-primary");
76
+ title.textContent = "Thinking...";
77
+ headerContent.appendChild(title);
78
+
79
+ const status = createElement("span", "tvw-text-xs tvw-text-cw-primary");
80
+ status.textContent = describeReasonStatus(reasoning);
81
+ headerContent.appendChild(status);
82
+
83
+ if (reasoning.status === "complete") {
84
+ title.style.display = "none";
85
+ } else {
86
+ title.style.display = "";
87
+ }
88
+
89
+ const toggleIcon = createElement("div", "tvw-flex tvw-items-center");
90
+ const iconColor = "currentColor";
91
+ const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
92
+ if (chevronIcon) {
93
+ toggleIcon.appendChild(chevronIcon);
94
+ } else {
95
+ // Fallback to text if icon fails
96
+ toggleIcon.textContent = expanded ? "Hide" : "Show";
97
+ }
98
+
99
+ const headerMeta = createElement("div", "tvw-flex tvw-items-center tvw-ml-auto");
100
+ headerMeta.append(toggleIcon);
101
+
102
+ header.append(headerContent, headerMeta);
103
+
104
+ const content = createElement(
105
+ "div",
106
+ "tvw-border-t tvw-border-gray-200 tvw-bg-gray-50 tvw-px-4 tvw-py-3"
107
+ );
108
+ content.style.display = expanded ? "" : "none";
109
+
110
+ const text = reasoning.chunks.join("");
111
+ const body = createElement(
112
+ "div",
113
+ "tvw-whitespace-pre-wrap tvw-text-xs tvw-leading-snug tvw-text-cw-muted"
114
+ );
115
+ body.textContent =
116
+ text ||
117
+ (reasoning.status === "complete"
118
+ ? "No additional context was shared."
119
+ : "Waiting for details…");
120
+ content.appendChild(body);
121
+
122
+ const applyExpansionState = () => {
123
+ header.setAttribute("aria-expanded", expanded ? "true" : "false");
124
+ // Update chevron icon
125
+ toggleIcon.innerHTML = "";
126
+ const iconColor = "currentColor";
127
+ const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
128
+ if (chevronIcon) {
129
+ toggleIcon.appendChild(chevronIcon);
130
+ } else {
131
+ // Fallback to text if icon fails
132
+ toggleIcon.textContent = expanded ? "Hide" : "Show";
133
+ }
134
+ content.style.display = expanded ? "" : "none";
135
+ };
136
+
137
+ applyExpansionState();
138
+
139
+ bubble.append(header, content);
140
+ return bubble;
141
+ };
142
+
143
+
144
+
@@ -0,0 +1,87 @@
1
+ import { AgentWidgetConfig, AgentWidgetMessage } from "../types";
2
+
3
+ /**
4
+ * Context provided to component renderers
5
+ */
6
+ export interface ComponentContext {
7
+ message: AgentWidgetMessage;
8
+ config: AgentWidgetConfig;
9
+ /**
10
+ * Update component props during streaming
11
+ */
12
+ updateProps: (newProps: Record<string, unknown>) => void;
13
+ }
14
+
15
+ /**
16
+ * Component renderer function signature
17
+ */
18
+ export type ComponentRenderer = (
19
+ props: Record<string, unknown>,
20
+ context: ComponentContext
21
+ ) => HTMLElement;
22
+
23
+ /**
24
+ * Component registry for managing custom components
25
+ */
26
+ class ComponentRegistry {
27
+ private components: Map<string, ComponentRenderer> = new Map();
28
+
29
+ /**
30
+ * Register a custom component
31
+ */
32
+ register(name: string, renderer: ComponentRenderer): void {
33
+ if (this.components.has(name)) {
34
+ console.warn(`[ComponentRegistry] Component "${name}" is already registered. Overwriting.`);
35
+ }
36
+ this.components.set(name, renderer);
37
+ }
38
+
39
+ /**
40
+ * Unregister a component
41
+ */
42
+ unregister(name: string): void {
43
+ this.components.delete(name);
44
+ }
45
+
46
+ /**
47
+ * Get a component renderer by name
48
+ */
49
+ get(name: string): ComponentRenderer | undefined {
50
+ return this.components.get(name);
51
+ }
52
+
53
+ /**
54
+ * Check if a component is registered
55
+ */
56
+ has(name: string): boolean {
57
+ return this.components.has(name);
58
+ }
59
+
60
+ /**
61
+ * Get all registered component names
62
+ */
63
+ getAllNames(): string[] {
64
+ return Array.from(this.components.keys());
65
+ }
66
+
67
+ /**
68
+ * Clear all registered components
69
+ */
70
+ clear(): void {
71
+ this.components.clear();
72
+ }
73
+
74
+ /**
75
+ * Register multiple components at once
76
+ */
77
+ registerAll(components: Record<string, ComponentRenderer>): void {
78
+ Object.entries(components).forEach(([name, renderer]) => {
79
+ this.register(name, renderer);
80
+ });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Global component registry instance
86
+ */
87
+ export const componentRegistry = new ComponentRegistry();
@@ -0,0 +1,97 @@
1
+ import { createElement } from "../utils/dom";
2
+ import { AgentWidgetSession } from "../session";
3
+ import { AgentWidgetMessage, AgentWidgetSuggestionChipsConfig } from "../types";
4
+
5
+ export interface SuggestionButtons {
6
+ buttons: HTMLButtonElement[];
7
+ render: (
8
+ chips: string[] | undefined,
9
+ session: AgentWidgetSession,
10
+ textarea: HTMLTextAreaElement,
11
+ messages?: AgentWidgetMessage[],
12
+ config?: AgentWidgetSuggestionChipsConfig
13
+ ) => void;
14
+ }
15
+
16
+ export const createSuggestions = (container: HTMLElement): SuggestionButtons => {
17
+ const suggestionButtons: HTMLButtonElement[] = [];
18
+
19
+ const render = (
20
+ chips: string[] | undefined,
21
+ session: AgentWidgetSession,
22
+ textarea: HTMLTextAreaElement,
23
+ messages?: AgentWidgetMessage[],
24
+ chipsConfig?: AgentWidgetSuggestionChipsConfig
25
+ ) => {
26
+ container.innerHTML = "";
27
+ suggestionButtons.length = 0;
28
+ if (!chips || !chips.length) return;
29
+
30
+ // Hide suggestions after the first user message is sent
31
+ // Use provided messages or get from session
32
+ const messagesToCheck = messages ?? (session ? session.getMessages() : []);
33
+ const hasUserMessage = messagesToCheck.some((msg) => msg.role === "user");
34
+ if (hasUserMessage) return;
35
+
36
+ const fragment = document.createDocumentFragment();
37
+ const streaming = session ? session.isStreaming() : false;
38
+
39
+ // Get font family mapping function
40
+ const getFontFamilyValue = (family: "sans-serif" | "serif" | "mono"): string => {
41
+ switch (family) {
42
+ case "serif":
43
+ return 'Georgia, "Times New Roman", Times, serif';
44
+ case "mono":
45
+ return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
46
+ case "sans-serif":
47
+ default:
48
+ return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
49
+ }
50
+ };
51
+
52
+ chips.forEach((chip) => {
53
+ const btn = createElement(
54
+ "button",
55
+ "tvw-rounded-button tvw-bg-cw-surface tvw-px-3 tvw-py-1.5 tvw-text-xs tvw-font-medium tvw-text-cw-muted hover:tvw-opacity-90 tvw-cursor-pointer tvw-border tvw-border-gray-200"
56
+ ) as HTMLButtonElement;
57
+ btn.type = "button";
58
+ btn.textContent = chip;
59
+ btn.disabled = streaming;
60
+
61
+ // Apply typography settings
62
+ if (chipsConfig?.fontFamily) {
63
+ btn.style.fontFamily = getFontFamilyValue(chipsConfig.fontFamily);
64
+ }
65
+ if (chipsConfig?.fontWeight) {
66
+ btn.style.fontWeight = chipsConfig.fontWeight;
67
+ }
68
+
69
+ // Apply padding settings
70
+ if (chipsConfig?.paddingX) {
71
+ btn.style.paddingLeft = chipsConfig.paddingX;
72
+ btn.style.paddingRight = chipsConfig.paddingX;
73
+ }
74
+ if (chipsConfig?.paddingY) {
75
+ btn.style.paddingTop = chipsConfig.paddingY;
76
+ btn.style.paddingBottom = chipsConfig.paddingY;
77
+ }
78
+
79
+ btn.addEventListener("click", () => {
80
+ if (!session || session.isStreaming()) return;
81
+ textarea.value = "";
82
+ session.sendMessage(chip);
83
+ });
84
+ fragment.appendChild(btn);
85
+ suggestionButtons.push(btn);
86
+ });
87
+ container.appendChild(fragment);
88
+ };
89
+
90
+ return {
91
+ buttons: suggestionButtons,
92
+ render
93
+ };
94
+ };
95
+
96
+
97
+