@runtypelabs/persona 4.14.0 → 4.16.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 +4 -4
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
- package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-QJ4LV2KW.js +1 -0
- package/dist/codegen.cjs +12 -11
- package/dist/codegen.js +14 -13
- package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
- package/dist/context-mentions.cjs +11 -8
- package/dist/context-mentions.d.cts +520 -21
- package/dist/context-mentions.d.ts +520 -21
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +69 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +595 -46
- package/dist/index.d.ts +595 -46
- package/dist/index.global.js +60 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -68
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +520 -21
- package/dist/smart-dom-reader.d.ts +520 -21
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +7 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +67 -67
- package/dist/theme-editor-preview.d.cts +534 -22
- package/dist/theme-editor-preview.d.ts +534 -22
- package/dist/theme-editor-preview.js +57 -57
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +521 -23
- package/dist/theme-editor.d.ts +521 -23
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +104 -3
- package/dist/theme-reference.d.ts +104 -3
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/ask-user-question-tool.ts +10 -7
- package/src/components/artifact-pane.test.ts +27 -0
- package/src/components/artifact-pane.ts +7 -3
- package/src/components/artifact-preview.ts +1 -1
- package/src/components/composer-parts.test.ts +103 -4
- package/src/components/composer-parts.ts +53 -15
- package/src/components/header-builder.ts +27 -2
- package/src/components/header-layouts.test.ts +56 -0
- package/src/components/header-layouts.ts +29 -9
- package/src/components/launcher-surface.test.ts +403 -0
- package/src/components/launcher.ts +297 -2
- package/src/components/messages.ts +5 -2
- package/src/components/panel.ts +92 -71
- package/src/components/pill-composer-builder.test.ts +18 -0
- package/src/components/suggestions.test.ts +273 -0
- package/src/components/suggestions.ts +528 -86
- package/src/components/welcome.test.ts +121 -0
- package/src/components/welcome.ts +305 -0
- package/src/components/widget-view.test.ts +5 -3
- package/src/components/widget-view.ts +18 -7
- package/src/defaults.ts +7 -5
- package/src/generated/runtype-openapi-contract.ts +17 -0
- package/src/index-core.ts +29 -2
- package/src/launcher-global.ts +11 -8
- package/src/plugins/types.ts +136 -3
- package/src/presets.ts +3 -3
- package/src/session.ts +10 -8
- package/src/styles/widget-styles.test.ts +382 -0
- package/src/styles/widget.css +778 -4
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- package/src/testing/mock-stream.test.ts +19 -0
- package/src/testing/mock-stream.ts +20 -0
- package/src/theme-editor/preview-utils.test.ts +34 -0
- package/src/theme-editor/preview-utils.ts +77 -2
- package/src/theme-editor/sections.test.ts +33 -0
- package/src/theme-editor/sections.ts +83 -6
- package/src/theme-reference.ts +34 -5
- package/src/types/theme.ts +72 -3
- package/src/types.ts +312 -18
- package/src/ui.body-top-slot.test.ts +95 -0
- package/src/ui.content-width.test.ts +91 -0
- package/src/ui.detached-panel.test.ts +1 -1
- package/src/ui.docked.test.ts +7 -4
- package/src/ui.launcher-teaser.test.ts +162 -0
- package/src/ui.rebuild-composer.test.ts +491 -0
- package/src/ui.render-welcome.test.ts +505 -0
- package/src/ui.scroll-additive.test.ts +91 -21
- package/src/ui.scroll.test.ts +302 -14
- package/src/ui.suggest-replies.test.ts +1112 -1
- package/src/ui.ts +1008 -149
- package/src/ui.welcome.test.ts +504 -0
- package/src/utils/artifact-file.test.ts +1 -1
- package/src/utils/artifact-gate.test.ts +27 -0
- package/src/utils/artifact-gate.ts +9 -0
- package/src/utils/attachment-manager.test.ts +37 -0
- package/src/utils/attachment-manager.ts +15 -0
- package/src/utils/auto-follow.test.ts +23 -0
- package/src/utils/auto-follow.ts +11 -1
- package/src/utils/code-generators.test.ts +40 -0
- package/src/utils/code-generators.ts +22 -0
- package/src/utils/config-merge.test.ts +24 -0
- package/src/utils/config-merge.ts +2 -0
- package/src/utils/content-width.test.ts +37 -0
- package/src/utils/content-width.ts +17 -0
- package/src/utils/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/plugin-storage.test.ts +94 -0
- package/src/utils/plugin-storage.ts +98 -0
- package/src/utils/theme.test.ts +216 -0
- package/src/utils/tokens.ts +113 -3
- package/src/welcome.test.ts +213 -0
- package/src/welcome.ts +130 -0
- package/dist/chunk-MMUPR2JW.js +0 -1
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
import { createElement } from "../utils/dom";
|
|
1
|
+
import { createElement, createNode } from "../utils/dom";
|
|
2
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
2
3
|
import { AgentWidgetSession } from "../session";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
AgentWidgetConfig,
|
|
6
|
+
AgentWidgetMessage,
|
|
7
|
+
AgentWidgetResolvedSuggestion,
|
|
8
|
+
AgentWidgetSuggestion,
|
|
9
|
+
AgentWidgetSuggestionChipsConfig,
|
|
10
|
+
AgentWidgetSuggestionBehavior,
|
|
11
|
+
AgentWidgetSuggestionSource,
|
|
12
|
+
AgentWidgetSuggestionSurface,
|
|
13
|
+
AgentWidgetSuggestionVariant,
|
|
14
|
+
} from "../types";
|
|
15
|
+
import type { AgentWidgetPlugin } from "../plugins/types";
|
|
16
|
+
|
|
17
|
+
export type NormalizedSuggestion = Omit<
|
|
18
|
+
AgentWidgetResolvedSuggestion,
|
|
19
|
+
"behavior"
|
|
20
|
+
> & {
|
|
21
|
+
behavior?: AgentWidgetSuggestionBehavior;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** DOM attribute values stay kebab-case; TS surface values stay camelCase. */
|
|
25
|
+
export const suggestionSurfaceAttr = (
|
|
26
|
+
surface: AgentWidgetSuggestionSurface
|
|
27
|
+
): string => (surface === "followUp" ? "follow-up" : surface);
|
|
4
28
|
|
|
5
29
|
export interface SuggestionButtons {
|
|
6
30
|
buttons: HTMLButtonElement[];
|
|
31
|
+
elements: HTMLElement[];
|
|
32
|
+
destroy: () => void;
|
|
7
33
|
render: (
|
|
8
|
-
|
|
34
|
+
items: AgentWidgetSuggestion[] | undefined,
|
|
9
35
|
session: AgentWidgetSession,
|
|
10
36
|
textarea: HTMLTextAreaElement,
|
|
11
37
|
messages?: AgentWidgetMessage[],
|
|
@@ -15,113 +41,523 @@ export interface SuggestionButtons {
|
|
|
15
41
|
}
|
|
16
42
|
|
|
17
43
|
export interface SuggestionRenderOptions {
|
|
44
|
+
/** Whether these are welcome starters or agent-produced follow-ups. */
|
|
45
|
+
surface?: AgentWidgetSuggestionSurface;
|
|
46
|
+
/** Presentation density. */
|
|
47
|
+
variant?: AgentWidgetSuggestionVariant;
|
|
48
|
+
/** Default click behavior, overridable per item. */
|
|
49
|
+
behavior?: AgentWidgetSuggestionBehavior;
|
|
50
|
+
/** Chip overflow behavior. */
|
|
51
|
+
overflow?: "scroll" | "wrap";
|
|
52
|
+
/** Renderer cap. */
|
|
53
|
+
maxItems?: number;
|
|
18
54
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* dispatches `persona:suggestReplies:*` DOM events.
|
|
55
|
+
* Who produced these items. Defaults to `config`; `agent` and `host` mark
|
|
56
|
+
* the follow-up surface, which keeps the legacy `persona:suggestReplies:*`
|
|
57
|
+
* events and skips the starters' before-first-user-message gate.
|
|
23
58
|
*/
|
|
24
|
-
|
|
59
|
+
source?: AgentWidgetSuggestionSource;
|
|
60
|
+
/** Live widget config exposed to plugin hooks. */
|
|
61
|
+
config?: AgentWidgetConfig;
|
|
62
|
+
/** Priority-sorted plugins active for this widget instance. */
|
|
63
|
+
plugins?: readonly AgentWidgetPlugin[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const normalizeSuggestion = (
|
|
67
|
+
item: AgentWidgetSuggestion,
|
|
68
|
+
index = 0
|
|
69
|
+
): NormalizedSuggestion | null => {
|
|
70
|
+
if (typeof item === "string") {
|
|
71
|
+
const value = item.trim();
|
|
72
|
+
if (!value) return null;
|
|
73
|
+
return {
|
|
74
|
+
id: value,
|
|
75
|
+
label: value,
|
|
76
|
+
prompt: value,
|
|
77
|
+
emphasis: "default",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const label = item.label.trim();
|
|
82
|
+
if (!label) return null;
|
|
83
|
+
const prompt = item.prompt?.trim() || label;
|
|
84
|
+
return {
|
|
85
|
+
id: item.id?.trim() || prompt || `suggestion-${index}`,
|
|
86
|
+
label,
|
|
87
|
+
prompt,
|
|
88
|
+
description: item.description?.trim() || undefined,
|
|
89
|
+
icon: item.icon,
|
|
90
|
+
iconColor: item.iconColor?.trim() || undefined,
|
|
91
|
+
behavior: item.behavior,
|
|
92
|
+
emphasis: item.emphasis ?? "default",
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const fontFamilyValue = (
|
|
97
|
+
family: "sans-serif" | "serif" | "mono"
|
|
98
|
+
): string => {
|
|
99
|
+
switch (family) {
|
|
100
|
+
case "serif":
|
|
101
|
+
return 'Georgia, "Times New Roman", Times, serif';
|
|
102
|
+
case "mono":
|
|
103
|
+
return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
|
|
104
|
+
default:
|
|
105
|
+
return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Module-level, keyed on the widget config: one widget owns several suggestion
|
|
110
|
+
// managers, so a per-instance flag hints once per surface instead of once.
|
|
111
|
+
const hintedOverflowVariant = new WeakSet<AgentWidgetConfig>();
|
|
112
|
+
|
|
113
|
+
const prefersReducedMotion = (): boolean =>
|
|
114
|
+
typeof window !== "undefined" &&
|
|
115
|
+
typeof window.matchMedia === "function" &&
|
|
116
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Staggered fade-up for a newly shown set. WAAPI, not CSS transitions: morph
|
|
120
|
+
* re-renders strip post-render CSS state, and a running animation survives.
|
|
121
|
+
*/
|
|
122
|
+
const animateEntrance = (elements: readonly HTMLElement[]): void => {
|
|
123
|
+
if (prefersReducedMotion()) return;
|
|
124
|
+
elements.forEach((element, index) => {
|
|
125
|
+
if (typeof element.animate !== "function") return;
|
|
126
|
+
element.animate(
|
|
127
|
+
[
|
|
128
|
+
{ opacity: "0", transform: "translateY(4px)" },
|
|
129
|
+
{ opacity: "1", transform: "none" },
|
|
130
|
+
],
|
|
131
|
+
{
|
|
132
|
+
duration: 250,
|
|
133
|
+
delay: index * 60,
|
|
134
|
+
easing: "ease-out",
|
|
135
|
+
fill: "backwards",
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export interface SuggestionElementContext {
|
|
142
|
+
/** Fully normalized item, including the effective send/fill behavior. */
|
|
143
|
+
item: AgentWidgetResolvedSuggestion;
|
|
144
|
+
index: number;
|
|
145
|
+
surface: AgentWidgetSuggestionSurface;
|
|
146
|
+
source: AgentWidgetSuggestionSource;
|
|
147
|
+
variant: AgentWidgetSuggestionVariant;
|
|
148
|
+
streaming: boolean;
|
|
149
|
+
config: AgentWidgetConfig;
|
|
150
|
+
plugins: readonly AgentWidgetPlugin[];
|
|
151
|
+
chipsConfig?: AgentWidgetSuggestionChipsConfig;
|
|
152
|
+
session: AgentWidgetSession;
|
|
153
|
+
/** Read live: inline mentions and composer rebuilds swap the input element. */
|
|
154
|
+
getTextarea: () => HTMLTextAreaElement;
|
|
155
|
+
/** Node the cancelable `persona:suggestion:*` events dispatch from. */
|
|
156
|
+
eventTarget: HTMLElement;
|
|
25
157
|
}
|
|
26
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Build one suggestion element with the full select pipeline attached:
|
|
161
|
+
* cancelable DOM events, `onSuggestionSelect` hooks, then send or fill. The
|
|
162
|
+
* suggestion rows and the `renderWelcome` ctx's `renderStarter` share this, so
|
|
163
|
+
* a plugin-rendered starter behaves identically to a built-in one.
|
|
164
|
+
*/
|
|
165
|
+
export const createSuggestionElement = (
|
|
166
|
+
context: SuggestionElementContext
|
|
167
|
+
): HTMLElement => {
|
|
168
|
+
const {
|
|
169
|
+
item,
|
|
170
|
+
index,
|
|
171
|
+
surface,
|
|
172
|
+
source,
|
|
173
|
+
variant,
|
|
174
|
+
streaming,
|
|
175
|
+
config,
|
|
176
|
+
plugins,
|
|
177
|
+
chipsConfig,
|
|
178
|
+
session,
|
|
179
|
+
getTextarea,
|
|
180
|
+
eventTarget,
|
|
181
|
+
} = context;
|
|
182
|
+
const isFollowUp = surface === "followUp";
|
|
183
|
+
const itemBehavior = item.behavior;
|
|
184
|
+
|
|
185
|
+
const select = () => {
|
|
186
|
+
if (session.isStreaming()) return;
|
|
187
|
+
const detail = {
|
|
188
|
+
suggestion: { ...item },
|
|
189
|
+
surface,
|
|
190
|
+
source,
|
|
191
|
+
behavior: itemBehavior,
|
|
192
|
+
};
|
|
193
|
+
const shouldContinue = eventTarget.dispatchEvent(
|
|
194
|
+
new CustomEvent("persona:suggestion:selected", {
|
|
195
|
+
detail,
|
|
196
|
+
bubbles: true,
|
|
197
|
+
composed: true,
|
|
198
|
+
cancelable: true,
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
if (isFollowUp) {
|
|
202
|
+
eventTarget.dispatchEvent(
|
|
203
|
+
new CustomEvent("persona:suggestReplies:selected", {
|
|
204
|
+
detail: { suggestion: item.prompt },
|
|
205
|
+
bubbles: true,
|
|
206
|
+
composed: true,
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (!shouldContinue) return;
|
|
211
|
+
|
|
212
|
+
for (const plugin of plugins) {
|
|
213
|
+
if (
|
|
214
|
+
plugin.onSuggestionSelect?.({
|
|
215
|
+
suggestion: { ...item },
|
|
216
|
+
surface,
|
|
217
|
+
source,
|
|
218
|
+
variant,
|
|
219
|
+
config,
|
|
220
|
+
}) === false
|
|
221
|
+
) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const textarea = getTextarea();
|
|
227
|
+
if (itemBehavior === "fill") {
|
|
228
|
+
textarea.value = item.prompt;
|
|
229
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
230
|
+
textarea.focus();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
textarea.value = "";
|
|
235
|
+
session.sendMessage(item.prompt);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const defaultRenderer = (): HTMLElement => {
|
|
239
|
+
const button = createElement(
|
|
240
|
+
"button",
|
|
241
|
+
`persona-suggestion persona-suggestion--${variant}`
|
|
242
|
+
) as HTMLButtonElement;
|
|
243
|
+
button.type = "button";
|
|
244
|
+
button.disabled = streaming;
|
|
245
|
+
button.dataset.suggestionId = item.id;
|
|
246
|
+
button.dataset.emphasis = item.emphasis;
|
|
247
|
+
button.dataset.behavior = itemBehavior;
|
|
248
|
+
|
|
249
|
+
// Legacy `suggestionChipsConfig` compat is chip-only: its inline
|
|
250
|
+
// styles would otherwise override the card/list padding tokens.
|
|
251
|
+
if (variant === "chip") {
|
|
252
|
+
if (chipsConfig?.fontFamily) {
|
|
253
|
+
button.style.fontFamily = fontFamilyValue(chipsConfig.fontFamily);
|
|
254
|
+
}
|
|
255
|
+
if (chipsConfig?.fontWeight) {
|
|
256
|
+
button.style.fontWeight = chipsConfig.fontWeight;
|
|
257
|
+
}
|
|
258
|
+
if (chipsConfig?.paddingX) {
|
|
259
|
+
button.style.paddingLeft = chipsConfig.paddingX;
|
|
260
|
+
button.style.paddingRight = chipsConfig.paddingX;
|
|
261
|
+
}
|
|
262
|
+
if (chipsConfig?.paddingY) {
|
|
263
|
+
button.style.paddingTop = chipsConfig.paddingY;
|
|
264
|
+
button.style.paddingBottom = chipsConfig.paddingY;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (item.icon) {
|
|
269
|
+
const icon = renderLucideIcon(
|
|
270
|
+
item.icon,
|
|
271
|
+
"var(--persona-suggestion-icon-size)",
|
|
272
|
+
"currentColor",
|
|
273
|
+
1.8
|
|
274
|
+
);
|
|
275
|
+
if (icon) {
|
|
276
|
+
icon.classList.add("persona-suggestion__icon");
|
|
277
|
+
// The glyph strokes in currentColor, so this tints it alone; inline
|
|
278
|
+
// also beats the emphasis-primary icon rule.
|
|
279
|
+
if (item.iconColor) icon.style.color = item.iconColor;
|
|
280
|
+
button.appendChild(icon);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const copy = createElement("span", "persona-suggestion__copy");
|
|
285
|
+
copy.appendChild(
|
|
286
|
+
createNode("span", {
|
|
287
|
+
className: "persona-suggestion__label",
|
|
288
|
+
text: item.label,
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
// Chips are single-line labels; descriptions belong to card and list.
|
|
292
|
+
if (item.description && variant !== "chip") {
|
|
293
|
+
copy.appendChild(
|
|
294
|
+
createNode("span", {
|
|
295
|
+
className: "persona-suggestion__description",
|
|
296
|
+
text: item.description,
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
button.appendChild(copy);
|
|
301
|
+
|
|
302
|
+
if (variant !== "chip") {
|
|
303
|
+
const arrow = renderLucideIcon("arrow-right", 16, "currentColor", 1.8);
|
|
304
|
+
if (arrow) {
|
|
305
|
+
arrow.classList.add("persona-suggestion__arrow");
|
|
306
|
+
button.appendChild(arrow);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
button.addEventListener("click", select);
|
|
311
|
+
return button;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
let element: HTMLElement | null = null;
|
|
315
|
+
for (const plugin of plugins) {
|
|
316
|
+
if (!plugin.renderSuggestion) continue;
|
|
317
|
+
element = plugin.renderSuggestion({
|
|
318
|
+
suggestion: { ...item },
|
|
319
|
+
index,
|
|
320
|
+
surface,
|
|
321
|
+
source,
|
|
322
|
+
variant,
|
|
323
|
+
streaming,
|
|
324
|
+
config,
|
|
325
|
+
defaultRenderer,
|
|
326
|
+
select,
|
|
327
|
+
});
|
|
328
|
+
if (element) break;
|
|
329
|
+
}
|
|
330
|
+
element ??= defaultRenderer();
|
|
331
|
+
element.dataset.suggestionId ||= item.id;
|
|
332
|
+
element.dataset.behavior ||= itemBehavior;
|
|
333
|
+
element.dataset.emphasis ||= item.emphasis;
|
|
334
|
+
if (!(element instanceof HTMLButtonElement)) {
|
|
335
|
+
element.setAttribute("aria-disabled", streaming ? "true" : "false");
|
|
336
|
+
}
|
|
337
|
+
return element;
|
|
338
|
+
};
|
|
339
|
+
|
|
27
340
|
export const createSuggestions = (container: HTMLElement): SuggestionButtons => {
|
|
28
341
|
const suggestionButtons: HTMLButtonElement[] = [];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let
|
|
342
|
+
const suggestionElements: HTMLElement[] = [];
|
|
343
|
+
let lastShownKey: string | null = null;
|
|
344
|
+
let overflowFrame: number | null = null;
|
|
345
|
+
|
|
346
|
+
const clearOverflowAffordance = () => {
|
|
347
|
+
container.removeAttribute("data-scroll-left");
|
|
348
|
+
container.removeAttribute("data-scroll-right");
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const syncOverflowAffordance = () => {
|
|
352
|
+
overflowFrame = null;
|
|
353
|
+
if (
|
|
354
|
+
container.hidden ||
|
|
355
|
+
container.dataset.variant !== "chip" ||
|
|
356
|
+
container.dataset.overflow !== "scroll"
|
|
357
|
+
) {
|
|
358
|
+
clearOverflowAffordance();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const maxScroll = Math.max(0, container.scrollWidth - container.clientWidth);
|
|
363
|
+
if (maxScroll <= 1) {
|
|
364
|
+
clearOverflowAffordance();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const isRtl = getComputedStyle(container).direction === "rtl";
|
|
369
|
+
const position = Math.min(
|
|
370
|
+
maxScroll,
|
|
371
|
+
Math.max(0, Math.abs(container.scrollLeft))
|
|
372
|
+
);
|
|
373
|
+
// Scroll snap and the scroller's 2px visual padding can settle a browser
|
|
374
|
+
// just off either boundary. Treat that subpixel-sized drift as the edge so
|
|
375
|
+
// a misleading opposite fade does not appear before the user scrolls.
|
|
376
|
+
const edgeTolerance = 4;
|
|
377
|
+
const atStart = position <= edgeTolerance;
|
|
378
|
+
const atEnd = position >= maxScroll - edgeTolerance;
|
|
379
|
+
const hasLeftOverflow = isRtl ? !atEnd : !atStart;
|
|
380
|
+
const hasRightOverflow = isRtl ? !atStart : !atEnd;
|
|
381
|
+
|
|
382
|
+
container.toggleAttribute("data-scroll-left", hasLeftOverflow);
|
|
383
|
+
container.toggleAttribute("data-scroll-right", hasRightOverflow);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const scheduleOverflowAffordance = () => {
|
|
387
|
+
if (overflowFrame !== null || typeof requestAnimationFrame !== "function") {
|
|
388
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
389
|
+
syncOverflowAffordance();
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
overflowFrame = requestAnimationFrame(syncOverflowAffordance);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
container.addEventListener("scroll", scheduleOverflowAffordance, {
|
|
397
|
+
passive: true,
|
|
398
|
+
});
|
|
399
|
+
const resizeObserver =
|
|
400
|
+
typeof ResizeObserver === "function"
|
|
401
|
+
? new ResizeObserver(scheduleOverflowAffordance)
|
|
402
|
+
: null;
|
|
403
|
+
resizeObserver?.observe(container);
|
|
32
404
|
|
|
33
405
|
const render = (
|
|
34
|
-
|
|
406
|
+
items: AgentWidgetSuggestion[] | undefined,
|
|
35
407
|
session: AgentWidgetSession,
|
|
36
408
|
textarea: HTMLTextAreaElement,
|
|
37
409
|
messages?: AgentWidgetMessage[],
|
|
38
410
|
chipsConfig?: AgentWidgetSuggestionChipsConfig,
|
|
39
411
|
opts?: SuggestionRenderOptions
|
|
40
412
|
) => {
|
|
41
|
-
container.
|
|
413
|
+
container.replaceChildren();
|
|
42
414
|
suggestionButtons.length = 0;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
415
|
+
suggestionElements.length = 0;
|
|
416
|
+
|
|
417
|
+
const source: AgentWidgetSuggestionSource = opts?.source ?? "config";
|
|
418
|
+
const surface =
|
|
419
|
+
opts?.surface ?? (source === "config" ? "starter" : "followUp");
|
|
420
|
+
// Follow-ups render mid-conversation and keep the legacy event names.
|
|
421
|
+
const isFollowUp = surface === "followUp";
|
|
422
|
+
const variant = opts?.variant ?? "chip";
|
|
423
|
+
const behavior = opts?.behavior ?? "send";
|
|
424
|
+
const overflow = opts?.overflow ?? "wrap";
|
|
425
|
+
const widgetConfig = opts?.config ?? ({} as AgentWidgetConfig);
|
|
426
|
+
const plugins = opts?.plugins ?? [];
|
|
427
|
+
const maxItems =
|
|
428
|
+
typeof opts?.maxItems === "number"
|
|
429
|
+
? Math.max(0, Math.floor(opts.maxItems))
|
|
430
|
+
: undefined;
|
|
431
|
+
|
|
432
|
+
// `data-overflow` is styled under the chip variant only; hint once so the
|
|
433
|
+
// no-op is discoverable without reading the CSS.
|
|
434
|
+
const surfaceKey = isFollowUp ? "followUps" : "starters";
|
|
435
|
+
if (
|
|
436
|
+
!hintedOverflowVariant.has(widgetConfig) &&
|
|
437
|
+
widgetConfig.debug === true &&
|
|
438
|
+
variant !== "chip" &&
|
|
439
|
+
widgetConfig.suggestions?.[surfaceKey]?.overflow !== undefined
|
|
440
|
+
) {
|
|
441
|
+
hintedOverflowVariant.add(widgetConfig);
|
|
442
|
+
console.info(
|
|
443
|
+
`[persona] suggestions.${surfaceKey}.overflow applies to the "chip" variant only; the "${variant}" variant manages its own stacking.`
|
|
444
|
+
);
|
|
55
445
|
}
|
|
56
446
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return '"Courier New", Courier, "Lucida Console", Monaco, monospace';
|
|
67
|
-
case "sans-serif":
|
|
68
|
-
default:
|
|
69
|
-
return '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif';
|
|
70
|
-
}
|
|
71
|
-
};
|
|
447
|
+
// Normalize before transform: hooks see resolved items and may return the
|
|
448
|
+
// loose shape, so re-resolution only fills a missing per-item behavior.
|
|
449
|
+
const resolveAll = (
|
|
450
|
+
list: AgentWidgetSuggestion[]
|
|
451
|
+
): AgentWidgetResolvedSuggestion[] =>
|
|
452
|
+
list
|
|
453
|
+
.map(normalizeSuggestion)
|
|
454
|
+
.filter((item): item is NormalizedSuggestion => item !== null)
|
|
455
|
+
.map((item) => ({ ...item, behavior: item.behavior ?? behavior }));
|
|
72
456
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
457
|
+
let resolved = resolveAll(items ?? []);
|
|
458
|
+
plugins.forEach((plugin) => {
|
|
459
|
+
if (!plugin.transformSuggestions) return;
|
|
460
|
+
resolved = resolveAll(
|
|
461
|
+
plugin.transformSuggestions({
|
|
462
|
+
suggestions: resolved.map((item) => ({ ...item })),
|
|
463
|
+
surface,
|
|
464
|
+
source,
|
|
465
|
+
config: widgetConfig,
|
|
466
|
+
})
|
|
467
|
+
);
|
|
468
|
+
});
|
|
81
469
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
if (chipsConfig?.fontWeight) {
|
|
87
|
-
btn.style.fontWeight = chipsConfig.fontWeight;
|
|
88
|
-
}
|
|
470
|
+
// The cap applies once, after the full transform chain.
|
|
471
|
+
const normalized =
|
|
472
|
+
maxItems === undefined ? resolved : resolved.slice(0, maxItems);
|
|
89
473
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
474
|
+
if (!normalized.length) {
|
|
475
|
+
container.hidden = true;
|
|
476
|
+
lastShownKey = null;
|
|
477
|
+
clearOverflowAffordance();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!isFollowUp) {
|
|
482
|
+
const messagesToCheck = messages ?? session.getMessages();
|
|
483
|
+
if (messagesToCheck.some((message) => message.role === "user")) {
|
|
484
|
+
container.hidden = true;
|
|
485
|
+
lastShownKey = null;
|
|
486
|
+
return;
|
|
98
487
|
}
|
|
488
|
+
}
|
|
99
489
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
490
|
+
container.hidden = false;
|
|
491
|
+
container.classList.add("persona-suggestions");
|
|
492
|
+
container.dataset.personaSuggestionSurface = suggestionSurfaceAttr(surface);
|
|
493
|
+
container.dataset.variant = variant;
|
|
494
|
+
container.dataset.overflow = overflow;
|
|
495
|
+
|
|
496
|
+
const fragment = document.createDocumentFragment();
|
|
497
|
+
const streaming = session.isStreaming();
|
|
498
|
+
|
|
499
|
+
normalized.forEach((item, index) => {
|
|
500
|
+
const element = createSuggestionElement({
|
|
501
|
+
item,
|
|
502
|
+
index,
|
|
503
|
+
surface,
|
|
504
|
+
source,
|
|
505
|
+
variant,
|
|
506
|
+
streaming,
|
|
507
|
+
config: widgetConfig,
|
|
508
|
+
plugins,
|
|
509
|
+
chipsConfig,
|
|
510
|
+
session,
|
|
511
|
+
getTextarea: () => textarea,
|
|
512
|
+
eventTarget: container,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
fragment.appendChild(element);
|
|
516
|
+
suggestionElements.push(element);
|
|
517
|
+
const buttons = [
|
|
518
|
+
...(element instanceof HTMLButtonElement ? [element] : []),
|
|
519
|
+
...Array.from(element.querySelectorAll<HTMLButtonElement>("button")),
|
|
520
|
+
];
|
|
521
|
+
buttons.forEach((button) => {
|
|
522
|
+
if (!suggestionButtons.includes(button)) {
|
|
523
|
+
button.disabled = streaming;
|
|
524
|
+
suggestionButtons.push(button);
|
|
111
525
|
}
|
|
112
|
-
session.sendMessage(chip);
|
|
113
526
|
});
|
|
114
|
-
fragment.appendChild(btn);
|
|
115
|
-
suggestionButtons.push(btn);
|
|
116
527
|
});
|
|
117
528
|
container.appendChild(fragment);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
529
|
+
syncOverflowAffordance();
|
|
530
|
+
scheduleOverflowAffordance();
|
|
531
|
+
|
|
532
|
+
const shownKey = JSON.stringify({
|
|
533
|
+
normalized,
|
|
534
|
+
surface,
|
|
535
|
+
variant,
|
|
536
|
+
source,
|
|
537
|
+
});
|
|
538
|
+
if (shownKey !== lastShownKey) {
|
|
539
|
+
lastShownKey = shownKey;
|
|
540
|
+
// Guarded by the same key as the shown event: render() re-runs on every
|
|
541
|
+
// UI update with identical items and must not re-animate.
|
|
542
|
+
animateEntrance(suggestionElements);
|
|
543
|
+
container.dispatchEvent(
|
|
544
|
+
new CustomEvent("persona:suggestion:shown", {
|
|
545
|
+
detail: {
|
|
546
|
+
suggestions: normalized.map((item) => ({ ...item })),
|
|
547
|
+
surface,
|
|
548
|
+
source,
|
|
549
|
+
variant,
|
|
550
|
+
},
|
|
551
|
+
bubbles: true,
|
|
552
|
+
composed: true,
|
|
553
|
+
})
|
|
554
|
+
);
|
|
555
|
+
if (isFollowUp) {
|
|
122
556
|
container.dispatchEvent(
|
|
123
557
|
new CustomEvent("persona:suggestReplies:shown", {
|
|
124
|
-
detail: {
|
|
558
|
+
detail: {
|
|
559
|
+
suggestions: normalized.map((item) => item.prompt),
|
|
560
|
+
},
|
|
125
561
|
bubbles: true,
|
|
126
562
|
composed: true,
|
|
127
563
|
})
|
|
@@ -132,9 +568,15 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
|
|
|
132
568
|
|
|
133
569
|
return {
|
|
134
570
|
buttons: suggestionButtons,
|
|
135
|
-
|
|
571
|
+
elements: suggestionElements,
|
|
572
|
+
destroy: () => {
|
|
573
|
+
container.removeEventListener("scroll", scheduleOverflowAffordance);
|
|
574
|
+
resizeObserver?.disconnect();
|
|
575
|
+
if (overflowFrame !== null && typeof cancelAnimationFrame === "function") {
|
|
576
|
+
cancelAnimationFrame(overflowFrame);
|
|
577
|
+
}
|
|
578
|
+
overflowFrame = null;
|
|
579
|
+
},
|
|
580
|
+
render,
|
|
136
581
|
};
|
|
137
582
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|