@runtypelabs/persona 4.13.0 → 4.15.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 +339 -22
- package/dist/context-mentions.d.ts +339 -22
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +68 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -48
- package/dist/index.d.ts +483 -48
- package/dist/index.global.js +61 -58
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- 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 +339 -22
- package/dist/smart-dom-reader.d.ts +339 -22
- package/dist/theme-editor-preview.cjs +67 -67
- package/dist/theme-editor-preview.d.cts +355 -23
- package/dist/theme-editor-preview.d.ts +355 -23
- package/dist/theme-editor-preview.js +75 -75
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +376 -24
- package/dist/theme-editor.d.ts +376 -24
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +38 -0
- package/dist/theme-reference.d.ts +38 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +106 -0
- package/src/ask-user-question-tool.ts +10 -7
- package/src/client.test.ts +2 -2
- package/src/client.ts +4 -1
- package/src/components/artifact-card.ts +13 -2
- package/src/components/artifact-inline.ts +11 -2
- package/src/components/artifact-preview.ts +1 -1
- package/src/components/composer-builder.test.ts +2 -0
- package/src/components/composer-builder.ts +1 -1
- package/src/components/composer-parts.test.ts +105 -4
- package/src/components/composer-parts.ts +54 -16
- package/src/components/message-bubble.test.ts +7 -1
- package/src/components/message-bubble.ts +11 -8
- package/src/components/messages.ts +5 -2
- package/src/components/panel.ts +31 -5
- package/src/components/pill-composer-builder.test.ts +18 -0
- package/src/components/suggestions.test.ts +233 -0
- package/src/components/suggestions.ts +463 -81
- package/src/components/widget-view.ts +4 -0
- package/src/defaults.ts +5 -3
- package/src/generated/runtype-openapi-contract.ts +17 -0
- package/src/index-core.ts +52 -2
- package/src/launcher-global.ts +1 -1
- package/src/plugins/types.ts +71 -3
- package/src/presets.ts +3 -3
- package/src/session.ts +55 -12
- package/src/styles/widget-styles.test.ts +162 -0
- package/src/styles/widget.css +440 -1
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/presets.test.ts +43 -0
- package/src/theme-editor/presets.ts +101 -87
- 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 +72 -7
- package/src/theme-editor/sections.ts +113 -13
- package/src/theme-editor/state-preferences.test.ts +15 -0
- package/src/theme-editor/state.ts +46 -0
- package/src/theme-editor/types.ts +15 -1
- package/src/theme-editor/webmcp/tools.test.ts +13 -0
- package/src/theme-editor/webmcp/tools.ts +5 -0
- package/src/theme-reference.ts +9 -2
- package/src/types/theme.ts +32 -0
- package/src/types.ts +291 -25
- package/src/ui.artifact-pane-gating.test.ts +5 -5
- package/src/ui.config-preferences.test.ts +166 -0
- package/src/ui.detached-panel.test.ts +1 -1
- package/src/ui.suggest-replies.test.ts +1066 -1
- package/src/ui.ts +311 -45
- package/src/ui.typing-indicator.test.ts +135 -0
- package/src/utils/artifact-display.test.ts +180 -16
- package/src/utils/artifact-display.ts +155 -14
- package/src/utils/artifact-file.test.ts +1 -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 +84 -0
- package/src/utils/config-merge.ts +6 -1
- package/src/utils/feature-preferences.test.ts +326 -0
- package/src/utils/feature-preferences.ts +498 -0
- package/src/utils/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/theme.test.ts +171 -0
- package/src/utils/tokens.ts +71 -7
- 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,463 @@ 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[];
|
|
25
64
|
}
|
|
26
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
|
+
|
|
27
141
|
export const createSuggestions = (container: HTMLElement): SuggestionButtons => {
|
|
28
142
|
const suggestionButtons: HTMLButtonElement[] = [];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let
|
|
143
|
+
const suggestionElements: HTMLElement[] = [];
|
|
144
|
+
let lastShownKey: string | null = null;
|
|
145
|
+
let overflowFrame: number | null = null;
|
|
146
|
+
|
|
147
|
+
const clearOverflowAffordance = () => {
|
|
148
|
+
container.removeAttribute("data-scroll-left");
|
|
149
|
+
container.removeAttribute("data-scroll-right");
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const syncOverflowAffordance = () => {
|
|
153
|
+
overflowFrame = null;
|
|
154
|
+
if (
|
|
155
|
+
container.hidden ||
|
|
156
|
+
container.dataset.variant !== "chip" ||
|
|
157
|
+
container.dataset.overflow !== "scroll"
|
|
158
|
+
) {
|
|
159
|
+
clearOverflowAffordance();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const maxScroll = Math.max(0, container.scrollWidth - container.clientWidth);
|
|
164
|
+
if (maxScroll <= 1) {
|
|
165
|
+
clearOverflowAffordance();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const isRtl = getComputedStyle(container).direction === "rtl";
|
|
170
|
+
const position = Math.min(
|
|
171
|
+
maxScroll,
|
|
172
|
+
Math.max(0, Math.abs(container.scrollLeft))
|
|
173
|
+
);
|
|
174
|
+
// Scroll snap and the scroller's 2px visual padding can settle a browser
|
|
175
|
+
// just off either boundary. Treat that subpixel-sized drift as the edge so
|
|
176
|
+
// a misleading opposite fade does not appear before the user scrolls.
|
|
177
|
+
const edgeTolerance = 4;
|
|
178
|
+
const atStart = position <= edgeTolerance;
|
|
179
|
+
const atEnd = position >= maxScroll - edgeTolerance;
|
|
180
|
+
const hasLeftOverflow = isRtl ? !atEnd : !atStart;
|
|
181
|
+
const hasRightOverflow = isRtl ? !atStart : !atEnd;
|
|
182
|
+
|
|
183
|
+
container.toggleAttribute("data-scroll-left", hasLeftOverflow);
|
|
184
|
+
container.toggleAttribute("data-scroll-right", hasRightOverflow);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const scheduleOverflowAffordance = () => {
|
|
188
|
+
if (overflowFrame !== null || typeof requestAnimationFrame !== "function") {
|
|
189
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
190
|
+
syncOverflowAffordance();
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
overflowFrame = requestAnimationFrame(syncOverflowAffordance);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
container.addEventListener("scroll", scheduleOverflowAffordance, {
|
|
198
|
+
passive: true,
|
|
199
|
+
});
|
|
200
|
+
const resizeObserver =
|
|
201
|
+
typeof ResizeObserver === "function"
|
|
202
|
+
? new ResizeObserver(scheduleOverflowAffordance)
|
|
203
|
+
: null;
|
|
204
|
+
resizeObserver?.observe(container);
|
|
32
205
|
|
|
33
206
|
const render = (
|
|
34
|
-
|
|
207
|
+
items: AgentWidgetSuggestion[] | undefined,
|
|
35
208
|
session: AgentWidgetSession,
|
|
36
209
|
textarea: HTMLTextAreaElement,
|
|
37
210
|
messages?: AgentWidgetMessage[],
|
|
38
211
|
chipsConfig?: AgentWidgetSuggestionChipsConfig,
|
|
39
212
|
opts?: SuggestionRenderOptions
|
|
40
213
|
) => {
|
|
41
|
-
container.
|
|
214
|
+
container.replaceChildren();
|
|
42
215
|
suggestionButtons.length = 0;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
216
|
+
suggestionElements.length = 0;
|
|
217
|
+
|
|
218
|
+
const source: AgentWidgetSuggestionSource = opts?.source ?? "config";
|
|
219
|
+
const surface =
|
|
220
|
+
opts?.surface ?? (source === "config" ? "starter" : "followUp");
|
|
221
|
+
// Follow-ups render mid-conversation and keep the legacy event names.
|
|
222
|
+
const isFollowUp = surface === "followUp";
|
|
223
|
+
const variant = opts?.variant ?? "chip";
|
|
224
|
+
const behavior = opts?.behavior ?? "send";
|
|
225
|
+
const overflow = opts?.overflow ?? "wrap";
|
|
226
|
+
const widgetConfig = opts?.config ?? ({} as AgentWidgetConfig);
|
|
227
|
+
const plugins = opts?.plugins ?? [];
|
|
228
|
+
const maxItems =
|
|
229
|
+
typeof opts?.maxItems === "number"
|
|
230
|
+
? Math.max(0, Math.floor(opts.maxItems))
|
|
231
|
+
: undefined;
|
|
232
|
+
|
|
233
|
+
// `data-overflow` is styled under the chip variant only; hint once so the
|
|
234
|
+
// no-op is discoverable without reading the CSS.
|
|
235
|
+
const surfaceKey = isFollowUp ? "followUps" : "starters";
|
|
236
|
+
if (
|
|
237
|
+
!hintedOverflowVariant.has(widgetConfig) &&
|
|
238
|
+
widgetConfig.debug === true &&
|
|
239
|
+
variant !== "chip" &&
|
|
240
|
+
widgetConfig.suggestions?.[surfaceKey]?.overflow !== undefined
|
|
241
|
+
) {
|
|
242
|
+
hintedOverflowVariant.add(widgetConfig);
|
|
243
|
+
console.info(
|
|
244
|
+
`[persona] suggestions.${surfaceKey}.overflow applies to the "chip" variant only; the "${variant}" variant manages its own stacking.`
|
|
245
|
+
);
|
|
55
246
|
}
|
|
56
247
|
|
|
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
|
-
};
|
|
248
|
+
// Normalize before transform: hooks see resolved items and may return the
|
|
249
|
+
// loose shape, so re-resolution only fills a missing per-item behavior.
|
|
250
|
+
const resolveAll = (
|
|
251
|
+
list: AgentWidgetSuggestion[]
|
|
252
|
+
): AgentWidgetResolvedSuggestion[] =>
|
|
253
|
+
list
|
|
254
|
+
.map(normalizeSuggestion)
|
|
255
|
+
.filter((item): item is NormalizedSuggestion => item !== null)
|
|
256
|
+
.map((item) => ({ ...item, behavior: item.behavior ?? behavior }));
|
|
72
257
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
if (chipsConfig?.fontWeight) {
|
|
87
|
-
btn.style.fontWeight = chipsConfig.fontWeight;
|
|
88
|
-
}
|
|
258
|
+
let resolved = resolveAll(items ?? []);
|
|
259
|
+
plugins.forEach((plugin) => {
|
|
260
|
+
if (!plugin.transformSuggestions) return;
|
|
261
|
+
resolved = resolveAll(
|
|
262
|
+
plugin.transformSuggestions({
|
|
263
|
+
suggestions: resolved.map((item) => ({ ...item })),
|
|
264
|
+
surface,
|
|
265
|
+
source,
|
|
266
|
+
config: widgetConfig,
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
});
|
|
89
270
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
271
|
+
// The cap applies once, after the full transform chain.
|
|
272
|
+
const normalized =
|
|
273
|
+
maxItems === undefined ? resolved : resolved.slice(0, maxItems);
|
|
274
|
+
|
|
275
|
+
if (!normalized.length) {
|
|
276
|
+
container.hidden = true;
|
|
277
|
+
lastShownKey = null;
|
|
278
|
+
clearOverflowAffordance();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!isFollowUp) {
|
|
283
|
+
const messagesToCheck = messages ?? session.getMessages();
|
|
284
|
+
if (messagesToCheck.some((message) => message.role === "user")) {
|
|
285
|
+
container.hidden = true;
|
|
286
|
+
lastShownKey = null;
|
|
287
|
+
return;
|
|
98
288
|
}
|
|
289
|
+
}
|
|
99
290
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
291
|
+
container.hidden = false;
|
|
292
|
+
container.classList.add("persona-suggestions");
|
|
293
|
+
container.dataset.personaSuggestionSurface = suggestionSurfaceAttr(surface);
|
|
294
|
+
container.dataset.variant = variant;
|
|
295
|
+
container.dataset.overflow = overflow;
|
|
296
|
+
|
|
297
|
+
const fragment = document.createDocumentFragment();
|
|
298
|
+
const streaming = session.isStreaming();
|
|
299
|
+
|
|
300
|
+
normalized.forEach((item, index) => {
|
|
301
|
+
const itemBehavior = item.behavior;
|
|
302
|
+
const select = () => {
|
|
303
|
+
if (session.isStreaming()) return;
|
|
304
|
+
const detail = {
|
|
305
|
+
suggestion: { ...item },
|
|
306
|
+
surface,
|
|
307
|
+
source,
|
|
308
|
+
behavior: itemBehavior,
|
|
309
|
+
};
|
|
310
|
+
const shouldContinue = container.dispatchEvent(
|
|
311
|
+
new CustomEvent("persona:suggestion:selected", {
|
|
312
|
+
detail,
|
|
313
|
+
bubbles: true,
|
|
314
|
+
composed: true,
|
|
315
|
+
cancelable: true,
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
if (isFollowUp) {
|
|
104
319
|
container.dispatchEvent(
|
|
105
320
|
new CustomEvent("persona:suggestReplies:selected", {
|
|
106
|
-
detail: { suggestion:
|
|
321
|
+
detail: { suggestion: item.prompt },
|
|
107
322
|
bubbles: true,
|
|
108
323
|
composed: true,
|
|
109
324
|
})
|
|
110
325
|
);
|
|
111
326
|
}
|
|
112
|
-
|
|
327
|
+
if (!shouldContinue) return;
|
|
328
|
+
|
|
329
|
+
for (const plugin of plugins) {
|
|
330
|
+
if (
|
|
331
|
+
plugin.onSuggestionSelect?.({
|
|
332
|
+
suggestion: { ...item },
|
|
333
|
+
surface,
|
|
334
|
+
source,
|
|
335
|
+
variant,
|
|
336
|
+
config: widgetConfig,
|
|
337
|
+
}) === false
|
|
338
|
+
) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (itemBehavior === "fill") {
|
|
344
|
+
textarea.value = item.prompt;
|
|
345
|
+
textarea.dispatchEvent(
|
|
346
|
+
new Event("input", { bubbles: true, composed: true })
|
|
347
|
+
);
|
|
348
|
+
textarea.focus();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
textarea.value = "";
|
|
353
|
+
session.sendMessage(item.prompt);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const defaultRenderer = (): HTMLElement => {
|
|
357
|
+
const button = createElement(
|
|
358
|
+
"button",
|
|
359
|
+
`persona-suggestion persona-suggestion--${variant}`
|
|
360
|
+
) as HTMLButtonElement;
|
|
361
|
+
button.type = "button";
|
|
362
|
+
button.disabled = streaming;
|
|
363
|
+
button.dataset.suggestionId = item.id;
|
|
364
|
+
button.dataset.emphasis = item.emphasis;
|
|
365
|
+
button.dataset.behavior = itemBehavior;
|
|
366
|
+
|
|
367
|
+
// Legacy `suggestionChipsConfig` compat is chip-only: its inline
|
|
368
|
+
// styles would otherwise override the card/list padding tokens.
|
|
369
|
+
if (variant === "chip") {
|
|
370
|
+
if (chipsConfig?.fontFamily) {
|
|
371
|
+
button.style.fontFamily = fontFamilyValue(chipsConfig.fontFamily);
|
|
372
|
+
}
|
|
373
|
+
if (chipsConfig?.fontWeight) {
|
|
374
|
+
button.style.fontWeight = chipsConfig.fontWeight;
|
|
375
|
+
}
|
|
376
|
+
if (chipsConfig?.paddingX) {
|
|
377
|
+
button.style.paddingLeft = chipsConfig.paddingX;
|
|
378
|
+
button.style.paddingRight = chipsConfig.paddingX;
|
|
379
|
+
}
|
|
380
|
+
if (chipsConfig?.paddingY) {
|
|
381
|
+
button.style.paddingTop = chipsConfig.paddingY;
|
|
382
|
+
button.style.paddingBottom = chipsConfig.paddingY;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (item.icon) {
|
|
387
|
+
const icon = renderLucideIcon(
|
|
388
|
+
item.icon,
|
|
389
|
+
"var(--persona-suggestion-icon-size)",
|
|
390
|
+
"currentColor",
|
|
391
|
+
1.8
|
|
392
|
+
);
|
|
393
|
+
if (icon) {
|
|
394
|
+
icon.classList.add("persona-suggestion__icon");
|
|
395
|
+
// The glyph strokes in currentColor, so this tints it alone; inline
|
|
396
|
+
// also beats the emphasis-primary icon rule.
|
|
397
|
+
if (item.iconColor) icon.style.color = item.iconColor;
|
|
398
|
+
button.appendChild(icon);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const copy = createElement("span", "persona-suggestion__copy");
|
|
403
|
+
copy.appendChild(
|
|
404
|
+
createNode("span", {
|
|
405
|
+
className: "persona-suggestion__label",
|
|
406
|
+
text: item.label,
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
if (item.description) {
|
|
410
|
+
copy.appendChild(
|
|
411
|
+
createNode("span", {
|
|
412
|
+
className: "persona-suggestion__description",
|
|
413
|
+
text: item.description,
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
button.appendChild(copy);
|
|
418
|
+
|
|
419
|
+
if (variant !== "chip") {
|
|
420
|
+
const arrow = renderLucideIcon("arrow-right", 16, "currentColor", 1.8);
|
|
421
|
+
if (arrow) {
|
|
422
|
+
arrow.classList.add("persona-suggestion__arrow");
|
|
423
|
+
button.appendChild(arrow);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
button.addEventListener("click", select);
|
|
428
|
+
return button;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
let element: HTMLElement | null = null;
|
|
432
|
+
for (const plugin of plugins) {
|
|
433
|
+
if (!plugin.renderSuggestion) continue;
|
|
434
|
+
element = plugin.renderSuggestion({
|
|
435
|
+
suggestion: { ...item },
|
|
436
|
+
index,
|
|
437
|
+
surface,
|
|
438
|
+
source,
|
|
439
|
+
variant,
|
|
440
|
+
streaming,
|
|
441
|
+
config: widgetConfig,
|
|
442
|
+
defaultRenderer,
|
|
443
|
+
select,
|
|
444
|
+
});
|
|
445
|
+
if (element) break;
|
|
446
|
+
}
|
|
447
|
+
element ??= defaultRenderer();
|
|
448
|
+
element.dataset.suggestionId ||= item.id;
|
|
449
|
+
element.dataset.behavior ||= itemBehavior;
|
|
450
|
+
element.dataset.emphasis ||= item.emphasis;
|
|
451
|
+
if (!(element instanceof HTMLButtonElement)) {
|
|
452
|
+
element.setAttribute("aria-disabled", streaming ? "true" : "false");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
fragment.appendChild(element);
|
|
456
|
+
suggestionElements.push(element);
|
|
457
|
+
const buttons = [
|
|
458
|
+
...(element instanceof HTMLButtonElement ? [element] : []),
|
|
459
|
+
...Array.from(element.querySelectorAll<HTMLButtonElement>("button")),
|
|
460
|
+
];
|
|
461
|
+
buttons.forEach((button) => {
|
|
462
|
+
if (!suggestionButtons.includes(button)) {
|
|
463
|
+
button.disabled = streaming;
|
|
464
|
+
suggestionButtons.push(button);
|
|
465
|
+
}
|
|
113
466
|
});
|
|
114
|
-
fragment.appendChild(btn);
|
|
115
|
-
suggestionButtons.push(btn);
|
|
116
467
|
});
|
|
117
468
|
container.appendChild(fragment);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
469
|
+
syncOverflowAffordance();
|
|
470
|
+
scheduleOverflowAffordance();
|
|
471
|
+
|
|
472
|
+
const shownKey = JSON.stringify({
|
|
473
|
+
normalized,
|
|
474
|
+
surface,
|
|
475
|
+
variant,
|
|
476
|
+
source,
|
|
477
|
+
});
|
|
478
|
+
if (shownKey !== lastShownKey) {
|
|
479
|
+
lastShownKey = shownKey;
|
|
480
|
+
// Guarded by the same key as the shown event: render() re-runs on every
|
|
481
|
+
// UI update with identical items and must not re-animate.
|
|
482
|
+
animateEntrance(suggestionElements);
|
|
483
|
+
container.dispatchEvent(
|
|
484
|
+
new CustomEvent("persona:suggestion:shown", {
|
|
485
|
+
detail: {
|
|
486
|
+
suggestions: normalized.map((item) => ({ ...item })),
|
|
487
|
+
surface,
|
|
488
|
+
source,
|
|
489
|
+
variant,
|
|
490
|
+
},
|
|
491
|
+
bubbles: true,
|
|
492
|
+
composed: true,
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
if (isFollowUp) {
|
|
122
496
|
container.dispatchEvent(
|
|
123
497
|
new CustomEvent("persona:suggestReplies:shown", {
|
|
124
|
-
detail: {
|
|
498
|
+
detail: {
|
|
499
|
+
suggestions: normalized.map((item) => item.prompt),
|
|
500
|
+
},
|
|
125
501
|
bubbles: true,
|
|
126
502
|
composed: true,
|
|
127
503
|
})
|
|
@@ -132,9 +508,15 @@ export const createSuggestions = (container: HTMLElement): SuggestionButtons =>
|
|
|
132
508
|
|
|
133
509
|
return {
|
|
134
510
|
buttons: suggestionButtons,
|
|
135
|
-
|
|
511
|
+
elements: suggestionElements,
|
|
512
|
+
destroy: () => {
|
|
513
|
+
container.removeEventListener("scroll", scheduleOverflowAffordance);
|
|
514
|
+
resizeObserver?.disconnect();
|
|
515
|
+
if (overflowFrame !== null && typeof cancelAnimationFrame === "function") {
|
|
516
|
+
cancelAnimationFrame(overflowFrame);
|
|
517
|
+
}
|
|
518
|
+
overflowFrame = null;
|
|
519
|
+
},
|
|
520
|
+
render,
|
|
136
521
|
};
|
|
137
522
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
@@ -30,6 +30,8 @@ export interface WidgetTranscriptRefs {
|
|
|
30
30
|
container: HTMLElement;
|
|
31
31
|
body: HTMLElement;
|
|
32
32
|
messagesWrapper: HTMLElement;
|
|
33
|
+
starterSuggestions: HTMLElement;
|
|
34
|
+
transcriptSuggestions: HTMLElement;
|
|
33
35
|
/** Absolute slot above the composer where interactive sheets mount. */
|
|
34
36
|
composerOverlay: HTMLElement;
|
|
35
37
|
introTitle: HTMLElement;
|
|
@@ -119,6 +121,8 @@ export const createWidgetView = ({
|
|
|
119
121
|
container: panelElements.container,
|
|
120
122
|
body: panelElements.body,
|
|
121
123
|
messagesWrapper: panelElements.messagesWrapper,
|
|
124
|
+
starterSuggestions: panelElements.starterSuggestions,
|
|
125
|
+
transcriptSuggestions: panelElements.transcriptSuggestions,
|
|
122
126
|
composerOverlay: panelElements.composerOverlay,
|
|
123
127
|
introTitle: panelElements.introTitle,
|
|
124
128
|
introSubtitle: panelElements.introSubtitle,
|
package/src/defaults.ts
CHANGED
|
@@ -169,10 +169,12 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
|
|
|
169
169
|
submitLabel: "Send",
|
|
170
170
|
},
|
|
171
171
|
},
|
|
172
|
+
// Placeholder copy that models the right shape: verb-first, user-voice,
|
|
173
|
+
// one line. Integrators should swap in domain prompts.
|
|
172
174
|
suggestionChips: [
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
175
|
+
"Show me what you can help with",
|
|
176
|
+
"Walk me through getting started",
|
|
177
|
+
"Answer a question about this page",
|
|
176
178
|
],
|
|
177
179
|
suggestionChipsConfig: {
|
|
178
180
|
fontFamily: "sans-serif",
|
|
@@ -260,7 +260,24 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
260
260
|
type: "source";
|
|
261
261
|
url?: string;
|
|
262
262
|
[key: string]: unknown;
|
|
263
|
+
} | {
|
|
264
|
+
executionId: string;
|
|
265
|
+
iteration?: number;
|
|
266
|
+
seq: number;
|
|
267
|
+
state: Record<string, unknown>;
|
|
268
|
+
type: "state_snapshot";
|
|
263
269
|
} | ({
|
|
270
|
+
executionId: string;
|
|
271
|
+
iteration?: number;
|
|
272
|
+
patch: Array<{
|
|
273
|
+
from?: string;
|
|
274
|
+
op: "add" | "remove" | "replace" | "move" | "copy" | "test";
|
|
275
|
+
path: string;
|
|
276
|
+
value?: unknown;
|
|
277
|
+
}>;
|
|
278
|
+
seq: number;
|
|
279
|
+
type: "state_delta";
|
|
280
|
+
}) | ({
|
|
264
281
|
executionId: string;
|
|
265
282
|
hiddenParameterNames?: Array<string>;
|
|
266
283
|
iteration?: number;
|