@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.
- package/README.md +1080 -0
- package/dist/index.cjs +140 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2626 -0
- package/dist/index.d.ts +2626 -0
- package/dist/index.global.js +1843 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +140 -0
- package/dist/index.js.map +1 -0
- package/dist/install.global.js +2 -0
- package/dist/install.global.js.map +1 -0
- package/dist/widget.css +1627 -0
- package/package.json +79 -0
- package/src/@types/idiomorph.d.ts +37 -0
- package/src/client.test.ts +387 -0
- package/src/client.ts +1589 -0
- package/src/components/composer-builder.ts +530 -0
- package/src/components/feedback.ts +379 -0
- package/src/components/forms.ts +170 -0
- package/src/components/header-builder.ts +455 -0
- package/src/components/header-layouts.ts +303 -0
- package/src/components/launcher.ts +193 -0
- package/src/components/message-bubble.ts +528 -0
- package/src/components/messages.ts +54 -0
- package/src/components/panel.ts +204 -0
- package/src/components/reasoning-bubble.ts +144 -0
- package/src/components/registry.ts +87 -0
- package/src/components/suggestions.ts +97 -0
- package/src/components/tool-bubble.ts +288 -0
- package/src/defaults.ts +321 -0
- package/src/index.ts +175 -0
- package/src/install.ts +284 -0
- package/src/plugins/registry.ts +77 -0
- package/src/plugins/types.ts +95 -0
- package/src/postprocessors.ts +194 -0
- package/src/runtime/init.ts +162 -0
- package/src/session.ts +376 -0
- package/src/styles/tailwind.css +20 -0
- package/src/styles/widget.css +1627 -0
- package/src/types.ts +1635 -0
- package/src/ui.ts +3341 -0
- package/src/utils/actions.ts +227 -0
- package/src/utils/attachment-manager.ts +384 -0
- package/src/utils/code-generators.test.ts +500 -0
- package/src/utils/code-generators.ts +1806 -0
- package/src/utils/component-middleware.ts +137 -0
- package/src/utils/component-parser.ts +119 -0
- package/src/utils/constants.ts +16 -0
- package/src/utils/content.ts +306 -0
- package/src/utils/dom.ts +25 -0
- package/src/utils/events.ts +41 -0
- package/src/utils/formatting.test.ts +166 -0
- package/src/utils/formatting.ts +470 -0
- package/src/utils/icons.ts +92 -0
- package/src/utils/message-id.ts +37 -0
- package/src/utils/morph.ts +36 -0
- package/src/utils/positioning.ts +17 -0
- package/src/utils/storage.ts +72 -0
- package/src/utils/theme.ts +105 -0
- package/src/widget.css +1 -0
- 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
|
+
|