@runtypelabs/persona 1.48.0 → 2.0.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 +140 -8
- package/dist/index.cjs +90 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1055 -24
- package/dist/index.d.ts +1055 -24
- package/dist/index.global.js +111 -60
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +90 -39
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/widget.css +836 -513
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +80 -0
- package/src/client.test.ts +20 -21
- package/src/client.ts +153 -4
- package/src/components/approval-bubble.ts +45 -42
- package/src/components/artifact-card.ts +91 -0
- package/src/components/artifact-pane.ts +501 -0
- package/src/components/composer-builder.ts +32 -27
- package/src/components/event-stream-view.ts +40 -40
- package/src/components/feedback.ts +36 -36
- package/src/components/forms.ts +11 -11
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +55 -36
- package/src/components/header-layouts.ts +58 -125
- package/src/components/launcher.ts +36 -21
- package/src/components/message-bubble.ts +92 -65
- package/src/components/messages.ts +2 -2
- package/src/components/panel.ts +42 -11
- package/src/components/reasoning-bubble.ts +23 -23
- package/src/components/registry.ts +4 -0
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +32 -32
- package/src/defaults.ts +30 -4
- package/src/index.ts +80 -2
- package/src/install.ts +22 -0
- package/src/plugins/types.ts +23 -0
- package/src/postprocessors.ts +2 -2
- package/src/runtime/host-layout.ts +174 -0
- package/src/runtime/init.test.ts +236 -0
- package/src/runtime/init.ts +114 -55
- package/src/session.ts +135 -2
- package/src/styles/tailwind.css +1 -1
- package/src/styles/widget.css +836 -513
- package/src/types/theme.ts +354 -0
- package/src/types.ts +314 -15
- package/src/ui.docked.test.ts +104 -0
- package/src/ui.ts +940 -227
- package/src/utils/artifact-gate.test.ts +255 -0
- package/src/utils/artifact-gate.ts +142 -0
- package/src/utils/artifact-resize.test.ts +64 -0
- package/src/utils/artifact-resize.ts +67 -0
- package/src/utils/attachment-manager.ts +10 -10
- package/src/utils/code-generators.test.ts +52 -0
- package/src/utils/code-generators.ts +40 -36
- package/src/utils/dock.ts +17 -0
- package/src/utils/dom-context.test.ts +504 -0
- package/src/utils/dom-context.ts +896 -0
- package/src/utils/dom.ts +12 -1
- package/src/utils/message-fingerprint.test.ts +187 -0
- package/src/utils/message-fingerprint.ts +105 -0
- package/src/utils/migration.ts +179 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/plugins.ts +175 -0
- package/src/utils/positioning.ts +4 -4
- package/src/utils/theme.test.ts +125 -0
- package/src/utils/theme.ts +216 -60
- package/src/utils/tokens.ts +682 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ComponentRenderer } from "./registry";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in artifact reference card component.
|
|
5
|
+
* Renders a compact clickable card in the chat thread that links to an artifact.
|
|
6
|
+
* Uses `data-open-artifact` attribute for click delegation (handled in ui.ts).
|
|
7
|
+
*/
|
|
8
|
+
export const PersonaArtifactCard: ComponentRenderer = (props) => {
|
|
9
|
+
const title =
|
|
10
|
+
typeof props.title === "string" && props.title
|
|
11
|
+
? props.title
|
|
12
|
+
: "Untitled artifact";
|
|
13
|
+
const artifactId =
|
|
14
|
+
typeof props.artifactId === "string" ? props.artifactId : "";
|
|
15
|
+
const status = props.status === "streaming" ? "streaming" : "complete";
|
|
16
|
+
const artifactType =
|
|
17
|
+
typeof props.artifactType === "string" ? props.artifactType : "markdown";
|
|
18
|
+
const subtitle =
|
|
19
|
+
artifactType === "component" ? "Component" : "Document";
|
|
20
|
+
|
|
21
|
+
const root = document.createElement("div");
|
|
22
|
+
root.className =
|
|
23
|
+
"persona-flex persona-w-full persona-max-w-full persona-items-center persona-gap-3 persona-rounded-xl persona-px-4 persona-py-3";
|
|
24
|
+
root.style.border = "1px solid var(--persona-border, #e5e7eb)";
|
|
25
|
+
root.style.backgroundColor = "var(--persona-bg, #ffffff)";
|
|
26
|
+
root.style.cursor = "pointer";
|
|
27
|
+
root.tabIndex = 0;
|
|
28
|
+
root.setAttribute("role", "button");
|
|
29
|
+
root.setAttribute("aria-label", `Open ${title} in artifact panel`);
|
|
30
|
+
if (artifactId) {
|
|
31
|
+
root.setAttribute("data-open-artifact", artifactId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Document icon
|
|
35
|
+
const iconBox = document.createElement("div");
|
|
36
|
+
iconBox.className =
|
|
37
|
+
"persona-flex persona-h-10 persona-w-10 persona-flex-shrink-0 persona-items-center persona-justify-center persona-rounded-lg";
|
|
38
|
+
iconBox.style.border = "1px solid var(--persona-border, #e5e7eb)";
|
|
39
|
+
iconBox.style.color = "var(--persona-muted, #9ca3af)";
|
|
40
|
+
iconBox.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>`;
|
|
41
|
+
|
|
42
|
+
// Title and subtitle
|
|
43
|
+
const meta = document.createElement("div");
|
|
44
|
+
meta.className =
|
|
45
|
+
"persona-min-w-0 persona-flex-1 persona-flex persona-flex-col persona-gap-0.5";
|
|
46
|
+
|
|
47
|
+
const titleEl = document.createElement("div");
|
|
48
|
+
titleEl.className = "persona-truncate persona-text-sm persona-font-medium";
|
|
49
|
+
titleEl.style.color = "var(--persona-text, #1f2937)";
|
|
50
|
+
titleEl.textContent = title;
|
|
51
|
+
|
|
52
|
+
const subtitleEl = document.createElement("div");
|
|
53
|
+
subtitleEl.className = "persona-text-xs persona-flex persona-items-center persona-gap-1.5";
|
|
54
|
+
subtitleEl.style.color = "var(--persona-muted, #9ca3af)";
|
|
55
|
+
|
|
56
|
+
if (status === "streaming") {
|
|
57
|
+
// Pulsing dot for streaming status
|
|
58
|
+
const dot = document.createElement("span");
|
|
59
|
+
dot.className = "persona-inline-block persona-w-1.5 persona-h-1.5 persona-rounded-full";
|
|
60
|
+
dot.style.backgroundColor = "var(--persona-primary, #3b82f6)";
|
|
61
|
+
dot.style.animation = "persona-pulse 1.5s ease-in-out infinite";
|
|
62
|
+
subtitleEl.appendChild(dot);
|
|
63
|
+
|
|
64
|
+
const statusText = document.createElement("span");
|
|
65
|
+
statusText.textContent = `Generating ${subtitle.toLowerCase()}...`;
|
|
66
|
+
subtitleEl.appendChild(statusText);
|
|
67
|
+
} else {
|
|
68
|
+
subtitleEl.textContent = subtitle;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
meta.append(titleEl, subtitleEl);
|
|
72
|
+
root.append(iconBox, meta);
|
|
73
|
+
|
|
74
|
+
// Download button (visible when artifact is complete)
|
|
75
|
+
if (status === "complete") {
|
|
76
|
+
const dl = document.createElement("button");
|
|
77
|
+
dl.type = "button";
|
|
78
|
+
dl.textContent = "Download";
|
|
79
|
+
dl.title = `Download ${title}`;
|
|
80
|
+
dl.className =
|
|
81
|
+
"persona-flex-shrink-0 persona-rounded-md persona-px-3 persona-py-1.5 persona-text-xs persona-font-medium";
|
|
82
|
+
dl.style.border = "1px solid var(--persona-border, #e5e7eb)";
|
|
83
|
+
dl.style.color = "var(--persona-text, #1f2937)";
|
|
84
|
+
dl.style.backgroundColor = "transparent";
|
|
85
|
+
dl.style.cursor = "pointer";
|
|
86
|
+
dl.setAttribute("data-download-artifact", artifactId);
|
|
87
|
+
root.append(dl);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return root;
|
|
91
|
+
};
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { createElement } from "../utils/dom";
|
|
2
|
+
import type { AgentWidgetConfig, AgentWidgetMessage, PersonaArtifactRecord } from "../types";
|
|
3
|
+
import { escapeHtml, createMarkdownProcessorFromConfig } from "../postprocessors";
|
|
4
|
+
import { componentRegistry, type ComponentContext } from "./registry";
|
|
5
|
+
import { renderLucideIcon } from "../utils/icons";
|
|
6
|
+
|
|
7
|
+
export type ArtifactPaneApi = {
|
|
8
|
+
element: HTMLElement;
|
|
9
|
+
/** Backdrop for mobile drawer (optional separate layer) */
|
|
10
|
+
backdrop: HTMLElement | null;
|
|
11
|
+
update: (state: { artifacts: PersonaArtifactRecord[]; selectedId: string | null }) => void;
|
|
12
|
+
setMobileOpen: (open: boolean) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function fallbackComponentCard(sel: PersonaArtifactRecord): HTMLElement {
|
|
16
|
+
const card = createElement(
|
|
17
|
+
"div",
|
|
18
|
+
"persona-rounded-lg persona-border persona-border-persona-border persona-p-3 persona-text-persona-primary"
|
|
19
|
+
);
|
|
20
|
+
const title = createElement("div", "persona-font-semibold persona-text-sm persona-mb-2");
|
|
21
|
+
title.textContent = sel.component ? `Component: ${sel.component}` : "Component";
|
|
22
|
+
const pre = createElement("pre", "persona-font-mono persona-text-xs persona-whitespace-pre-wrap persona-overflow-x-auto");
|
|
23
|
+
pre.textContent = JSON.stringify(sel.props ?? {}, null, 2);
|
|
24
|
+
card.appendChild(title);
|
|
25
|
+
card.appendChild(pre);
|
|
26
|
+
return card;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function iconButton(iconName: string, label: string, extraClass = ""): HTMLButtonElement {
|
|
30
|
+
const btn = createElement(
|
|
31
|
+
"button",
|
|
32
|
+
`persona-inline-flex persona-items-center persona-justify-center persona-rounded-md persona-border persona-border-persona-border persona-bg-persona-surface persona-p-1 persona-text-persona-primary hover:persona-bg-persona-container ${extraClass}`
|
|
33
|
+
) as HTMLButtonElement;
|
|
34
|
+
btn.type = "button";
|
|
35
|
+
btn.setAttribute("aria-label", label);
|
|
36
|
+
btn.title = label;
|
|
37
|
+
const icon = renderLucideIcon(iconName, 16, "currentColor", 2);
|
|
38
|
+
if (icon) btn.appendChild(icon);
|
|
39
|
+
return btn;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function documentToolbarIconButton(
|
|
43
|
+
iconName: string,
|
|
44
|
+
label: string,
|
|
45
|
+
extraClass: string
|
|
46
|
+
): HTMLButtonElement {
|
|
47
|
+
const btn = createElement(
|
|
48
|
+
"button",
|
|
49
|
+
`persona-artifact-doc-icon-btn ${extraClass}`.trim()
|
|
50
|
+
) as HTMLButtonElement;
|
|
51
|
+
btn.type = "button";
|
|
52
|
+
btn.setAttribute("aria-label", label);
|
|
53
|
+
btn.title = label;
|
|
54
|
+
const icon = renderLucideIcon(iconName, 16, "currentColor", 2);
|
|
55
|
+
if (icon) btn.appendChild(icon);
|
|
56
|
+
return btn;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function documentToolbarCopyMainButton(showLabel: boolean): HTMLButtonElement {
|
|
60
|
+
const btn = createElement("button", "persona-artifact-doc-copy-btn") as HTMLButtonElement;
|
|
61
|
+
btn.type = "button";
|
|
62
|
+
btn.setAttribute("aria-label", "Copy");
|
|
63
|
+
btn.title = "Copy";
|
|
64
|
+
const icon = renderLucideIcon("copy", showLabel ? 14 : 16, "currentColor", 2);
|
|
65
|
+
if (icon) btn.appendChild(icon);
|
|
66
|
+
if (showLabel) {
|
|
67
|
+
const span = createElement("span", "persona-artifact-doc-copy-label");
|
|
68
|
+
span.textContent = "Copy";
|
|
69
|
+
btn.appendChild(span);
|
|
70
|
+
}
|
|
71
|
+
return btn;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function documentToolbarChevronMenuButton(): HTMLButtonElement {
|
|
75
|
+
const btn = createElement(
|
|
76
|
+
"button",
|
|
77
|
+
"persona-artifact-doc-copy-menu-chevron persona-artifact-doc-icon-btn"
|
|
78
|
+
) as HTMLButtonElement;
|
|
79
|
+
btn.type = "button";
|
|
80
|
+
btn.setAttribute("aria-label", "More copy options");
|
|
81
|
+
btn.setAttribute("aria-haspopup", "true");
|
|
82
|
+
btn.setAttribute("aria-expanded", "false");
|
|
83
|
+
const chev = renderLucideIcon("chevron-down", 14, "currentColor", 2);
|
|
84
|
+
if (chev) btn.appendChild(chev);
|
|
85
|
+
return btn;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Right-hand artifact sidebar / mobile drawer content.
|
|
90
|
+
*/
|
|
91
|
+
export function createArtifactPane(
|
|
92
|
+
config: AgentWidgetConfig,
|
|
93
|
+
options: {
|
|
94
|
+
onSelect: (id: string) => void;
|
|
95
|
+
/** User closed the pane (mobile drawer or split sidebar) — parent should persist “hidden until reopened”. */
|
|
96
|
+
onDismiss?: () => void;
|
|
97
|
+
}
|
|
98
|
+
): ArtifactPaneApi {
|
|
99
|
+
const layout = config.features?.artifacts?.layout;
|
|
100
|
+
const toolbarPreset = layout?.toolbarPreset ?? "default";
|
|
101
|
+
const documentChrome = toolbarPreset === "document";
|
|
102
|
+
const panePadding = layout?.panePadding?.trim();
|
|
103
|
+
|
|
104
|
+
const md = config.markdown ? createMarkdownProcessorFromConfig(config.markdown) : null;
|
|
105
|
+
const toHtml = (text: string) => (md ? md(text) : escapeHtml(text));
|
|
106
|
+
|
|
107
|
+
const backdrop =
|
|
108
|
+
typeof document !== "undefined"
|
|
109
|
+
? createElement(
|
|
110
|
+
"div",
|
|
111
|
+
"persona-artifact-backdrop persona-fixed persona-inset-0 persona-z-[55] persona-bg-black/30 persona-hidden md:persona-hidden"
|
|
112
|
+
)
|
|
113
|
+
: null;
|
|
114
|
+
const dismissLocalUi = () => {
|
|
115
|
+
backdrop?.classList.add("persona-hidden");
|
|
116
|
+
shell.classList.remove("persona-artifact-drawer-open");
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (backdrop) {
|
|
120
|
+
backdrop.addEventListener("click", () => {
|
|
121
|
+
dismissLocalUi();
|
|
122
|
+
options.onDismiss?.();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const shell = createElement(
|
|
127
|
+
"aside",
|
|
128
|
+
"persona-artifact-pane persona-flex persona-flex-col persona-min-h-0 persona-min-w-0 persona-bg-persona-surface persona-text-persona-primary persona-border-l persona-border-persona-border"
|
|
129
|
+
);
|
|
130
|
+
if (documentChrome) {
|
|
131
|
+
shell.classList.add("persona-artifact-pane-document");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const toolbar = createElement(
|
|
135
|
+
"div",
|
|
136
|
+
"persona-artifact-toolbar persona-flex persona-items-center persona-justify-between persona-gap-2 persona-px-2 persona-py-2 persona-border-b persona-border-persona-border persona-shrink-0"
|
|
137
|
+
);
|
|
138
|
+
if (documentChrome) {
|
|
139
|
+
toolbar.classList.add("persona-artifact-toolbar-document");
|
|
140
|
+
}
|
|
141
|
+
const titleEl = createElement("span", "persona-text-xs persona-font-medium persona-truncate");
|
|
142
|
+
titleEl.textContent = "Artifacts";
|
|
143
|
+
|
|
144
|
+
const closeBtn = createElement(
|
|
145
|
+
"button",
|
|
146
|
+
"persona-rounded-md persona-border persona-border-persona-border persona-px-2 persona-py-1 persona-text-xs persona-bg-persona-surface"
|
|
147
|
+
);
|
|
148
|
+
closeBtn.type = "button";
|
|
149
|
+
closeBtn.textContent = "Close";
|
|
150
|
+
closeBtn.setAttribute("aria-label", "Close artifacts panel");
|
|
151
|
+
closeBtn.addEventListener("click", () => {
|
|
152
|
+
dismissLocalUi();
|
|
153
|
+
options.onDismiss?.();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/** Document preset: view vs raw source */
|
|
157
|
+
let viewMode: "rendered" | "source" = "rendered";
|
|
158
|
+
const leftTools = createElement("div", "persona-flex persona-items-center persona-gap-1 persona-shrink-0");
|
|
159
|
+
const viewBtn = documentChrome
|
|
160
|
+
? documentToolbarIconButton("eye", "Rendered view", "persona-artifact-view-btn")
|
|
161
|
+
: iconButton("eye", "Rendered view", "");
|
|
162
|
+
const codeBtn = documentChrome
|
|
163
|
+
? documentToolbarIconButton("code-2", "Source", "persona-artifact-code-btn")
|
|
164
|
+
: iconButton("code-2", "Source", "");
|
|
165
|
+
const actionsRight = createElement("div", "persona-flex persona-items-center persona-gap-1 persona-shrink-0");
|
|
166
|
+
const showCopyLabel = layout?.documentToolbarShowCopyLabel === true;
|
|
167
|
+
const showCopyChevron = layout?.documentToolbarShowCopyChevron === true;
|
|
168
|
+
const copyMenuItems = layout?.documentToolbarCopyMenuItems;
|
|
169
|
+
const showCopyMenu = Boolean(showCopyChevron && copyMenuItems && copyMenuItems.length > 0);
|
|
170
|
+
|
|
171
|
+
let copyWrap: HTMLElement | null = null;
|
|
172
|
+
let copyBtn: HTMLButtonElement;
|
|
173
|
+
let copyMenuChevronBtn: HTMLButtonElement | null = null;
|
|
174
|
+
let copyMenuEl: HTMLElement | null = null;
|
|
175
|
+
|
|
176
|
+
if (documentChrome && (showCopyLabel || showCopyChevron) && !showCopyMenu) {
|
|
177
|
+
copyBtn = documentToolbarCopyMainButton(showCopyLabel);
|
|
178
|
+
if (showCopyChevron) {
|
|
179
|
+
const chev = renderLucideIcon("chevron-down", 14, "currentColor", 2);
|
|
180
|
+
if (chev) copyBtn.appendChild(chev);
|
|
181
|
+
}
|
|
182
|
+
} else if (documentChrome && showCopyMenu) {
|
|
183
|
+
copyWrap = createElement(
|
|
184
|
+
"div",
|
|
185
|
+
"persona-relative persona-inline-flex persona-items-center persona-gap-0 persona-rounded-md"
|
|
186
|
+
);
|
|
187
|
+
copyBtn = documentToolbarCopyMainButton(showCopyLabel);
|
|
188
|
+
copyMenuChevronBtn = documentToolbarChevronMenuButton();
|
|
189
|
+
copyWrap.append(copyBtn, copyMenuChevronBtn);
|
|
190
|
+
copyMenuEl = createElement(
|
|
191
|
+
"div",
|
|
192
|
+
"persona-artifact-doc-copy-menu persona-absolute persona-right-0 persona-top-full persona-z-20 persona-mt-1 persona-min-w-[10rem] persona-rounded-md persona-border persona-border-persona-border persona-bg-persona-surface persona-py-1 persona-shadow-md persona-hidden"
|
|
193
|
+
);
|
|
194
|
+
copyWrap.appendChild(copyMenuEl);
|
|
195
|
+
for (const item of copyMenuItems!) {
|
|
196
|
+
const opt = createElement(
|
|
197
|
+
"button",
|
|
198
|
+
"persona-block persona-w-full persona-text-left persona-px-3 persona-py-2 persona-text-xs persona-text-persona-primary hover:persona-bg-persona-container"
|
|
199
|
+
) as HTMLButtonElement;
|
|
200
|
+
opt.type = "button";
|
|
201
|
+
opt.textContent = item.label;
|
|
202
|
+
opt.dataset.copyMenuId = item.id;
|
|
203
|
+
copyMenuEl.appendChild(opt);
|
|
204
|
+
}
|
|
205
|
+
} else if (documentChrome) {
|
|
206
|
+
copyBtn = documentToolbarIconButton("copy", "Copy", "");
|
|
207
|
+
} else {
|
|
208
|
+
copyBtn = iconButton("copy", "Copy", "");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const refreshBtn = documentChrome
|
|
212
|
+
? documentToolbarIconButton("refresh-cw", "Refresh", "")
|
|
213
|
+
: iconButton("refresh-cw", "Refresh", "");
|
|
214
|
+
const closeIconBtn = documentChrome
|
|
215
|
+
? documentToolbarIconButton("x", "Close", "")
|
|
216
|
+
: iconButton("x", "Close", "");
|
|
217
|
+
|
|
218
|
+
const getSelectedArtifactText = (): { markdown: string; jsonPayload: string; id: string | null } => {
|
|
219
|
+
const sel = records.find((r) => r.id === selectedId) ?? records[records.length - 1];
|
|
220
|
+
const id = sel?.id ?? null;
|
|
221
|
+
const markdown = sel?.artifactType === "markdown" ? sel.markdown ?? "" : "";
|
|
222
|
+
const jsonPayload = sel
|
|
223
|
+
? JSON.stringify({ component: sel.component, props: sel.props }, null, 2)
|
|
224
|
+
: "";
|
|
225
|
+
return { markdown, jsonPayload, id };
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const defaultCopy = async () => {
|
|
229
|
+
const { markdown, jsonPayload } = getSelectedArtifactText();
|
|
230
|
+
const sel = records.find((r) => r.id === selectedId) ?? records[records.length - 1];
|
|
231
|
+
const text =
|
|
232
|
+
sel?.artifactType === "markdown"
|
|
233
|
+
? markdown
|
|
234
|
+
: sel
|
|
235
|
+
? jsonPayload
|
|
236
|
+
: "";
|
|
237
|
+
try {
|
|
238
|
+
await navigator.clipboard.writeText(text);
|
|
239
|
+
} catch {
|
|
240
|
+
/* ignore */
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
copyBtn.addEventListener("click", async () => {
|
|
245
|
+
const handler = layout?.onDocumentToolbarCopyMenuSelect;
|
|
246
|
+
if (handler && showCopyMenu) {
|
|
247
|
+
const { markdown, jsonPayload, id } = getSelectedArtifactText();
|
|
248
|
+
try {
|
|
249
|
+
await handler({ actionId: "primary", artifactId: id, markdown, jsonPayload });
|
|
250
|
+
} catch {
|
|
251
|
+
/* ignore */
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
await defaultCopy();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (copyMenuChevronBtn && copyMenuEl) {
|
|
259
|
+
const closeMenu = () => {
|
|
260
|
+
copyMenuEl!.classList.add("persona-hidden");
|
|
261
|
+
copyMenuChevronBtn!.setAttribute("aria-expanded", "false");
|
|
262
|
+
};
|
|
263
|
+
copyMenuChevronBtn.addEventListener("click", (e) => {
|
|
264
|
+
e.stopPropagation();
|
|
265
|
+
const open = copyMenuEl!.classList.contains("persona-hidden");
|
|
266
|
+
if (open) {
|
|
267
|
+
copyMenuEl!.classList.remove("persona-hidden");
|
|
268
|
+
copyMenuChevronBtn!.setAttribute("aria-expanded", "true");
|
|
269
|
+
} else {
|
|
270
|
+
closeMenu();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
if (typeof document !== "undefined") {
|
|
274
|
+
document.addEventListener("click", closeMenu);
|
|
275
|
+
}
|
|
276
|
+
copyMenuEl.addEventListener("click", async (e) => {
|
|
277
|
+
const t = (e.target as HTMLElement).closest("button[data-copy-menu-id]") as HTMLButtonElement | null;
|
|
278
|
+
if (!t?.dataset.copyMenuId) return;
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
const actionId = t.dataset.copyMenuId;
|
|
281
|
+
const { markdown, jsonPayload, id } = getSelectedArtifactText();
|
|
282
|
+
const handler = layout?.onDocumentToolbarCopyMenuSelect;
|
|
283
|
+
try {
|
|
284
|
+
if (handler) {
|
|
285
|
+
await handler({ actionId, artifactId: id, markdown, jsonPayload });
|
|
286
|
+
} else if (actionId === "markdown" || actionId === "md") {
|
|
287
|
+
await navigator.clipboard.writeText(markdown);
|
|
288
|
+
} else if (actionId === "json" || actionId === "source") {
|
|
289
|
+
await navigator.clipboard.writeText(jsonPayload);
|
|
290
|
+
} else {
|
|
291
|
+
await navigator.clipboard.writeText(markdown || jsonPayload);
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
/* ignore */
|
|
295
|
+
}
|
|
296
|
+
closeMenu();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
refreshBtn.addEventListener("click", async () => {
|
|
301
|
+
try {
|
|
302
|
+
await layout?.onDocumentToolbarRefresh?.();
|
|
303
|
+
} catch {
|
|
304
|
+
/* ignore */
|
|
305
|
+
}
|
|
306
|
+
render();
|
|
307
|
+
});
|
|
308
|
+
closeIconBtn.addEventListener("click", () => {
|
|
309
|
+
dismissLocalUi();
|
|
310
|
+
options.onDismiss?.();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const syncViewToggleState = () => {
|
|
314
|
+
if (!documentChrome) return;
|
|
315
|
+
viewBtn.setAttribute("aria-pressed", viewMode === "rendered" ? "true" : "false");
|
|
316
|
+
codeBtn.setAttribute("aria-pressed", viewMode === "source" ? "true" : "false");
|
|
317
|
+
};
|
|
318
|
+
viewBtn.addEventListener("click", () => {
|
|
319
|
+
viewMode = "rendered";
|
|
320
|
+
syncViewToggleState();
|
|
321
|
+
render();
|
|
322
|
+
});
|
|
323
|
+
codeBtn.addEventListener("click", () => {
|
|
324
|
+
viewMode = "source";
|
|
325
|
+
syncViewToggleState();
|
|
326
|
+
render();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const centerTitle = createElement(
|
|
330
|
+
"span",
|
|
331
|
+
"persona-min-w-0 persona-flex-1 persona-text-xs persona-font-medium persona-text-persona-primary persona-truncate persona-text-center md:persona-text-left"
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (documentChrome) {
|
|
335
|
+
toolbar.replaceChildren();
|
|
336
|
+
leftTools.append(viewBtn, codeBtn);
|
|
337
|
+
if (copyWrap) {
|
|
338
|
+
actionsRight.append(copyWrap, refreshBtn, closeIconBtn);
|
|
339
|
+
} else {
|
|
340
|
+
actionsRight.append(copyBtn, refreshBtn, closeIconBtn);
|
|
341
|
+
}
|
|
342
|
+
toolbar.append(leftTools, centerTitle, actionsRight);
|
|
343
|
+
syncViewToggleState();
|
|
344
|
+
} else {
|
|
345
|
+
toolbar.appendChild(titleEl);
|
|
346
|
+
toolbar.appendChild(closeBtn);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (panePadding) {
|
|
350
|
+
toolbar.style.paddingLeft = panePadding;
|
|
351
|
+
toolbar.style.paddingRight = panePadding;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const list = createElement(
|
|
355
|
+
"div",
|
|
356
|
+
"persona-artifact-list persona-shrink-0 persona-flex persona-gap-1 persona-overflow-x-auto persona-p-2 persona-border-b persona-border-persona-border"
|
|
357
|
+
);
|
|
358
|
+
const content = createElement(
|
|
359
|
+
"div",
|
|
360
|
+
"persona-artifact-content persona-flex-1 persona-min-h-0 persona-overflow-y-auto persona-p-3"
|
|
361
|
+
);
|
|
362
|
+
if (panePadding) {
|
|
363
|
+
list.style.paddingLeft = panePadding;
|
|
364
|
+
list.style.paddingRight = panePadding;
|
|
365
|
+
content.style.padding = panePadding;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
shell.appendChild(toolbar);
|
|
369
|
+
shell.appendChild(list);
|
|
370
|
+
shell.appendChild(content);
|
|
371
|
+
|
|
372
|
+
let records: PersonaArtifactRecord[] = [];
|
|
373
|
+
let selectedId: string | null = null;
|
|
374
|
+
let mobileOpen = false;
|
|
375
|
+
|
|
376
|
+
const render = () => {
|
|
377
|
+
const hideTabs = documentChrome && records.length <= 1;
|
|
378
|
+
list.classList.toggle("persona-hidden", hideTabs);
|
|
379
|
+
|
|
380
|
+
list.replaceChildren();
|
|
381
|
+
for (const r of records) {
|
|
382
|
+
const tab = createElement(
|
|
383
|
+
"button",
|
|
384
|
+
"persona-artifact-tab persona-shrink-0 persona-rounded-lg persona-px-2 persona-py-1 persona-text-xs persona-border persona-border-transparent persona-text-persona-primary hover:persona-bg-persona-container"
|
|
385
|
+
);
|
|
386
|
+
tab.type = "button";
|
|
387
|
+
tab.textContent = r.title || r.id.slice(0, 8);
|
|
388
|
+
if (r.id === selectedId) {
|
|
389
|
+
tab.classList.add("persona-bg-persona-container", "persona-border-persona-border");
|
|
390
|
+
}
|
|
391
|
+
tab.addEventListener("click", () => options.onSelect(r.id));
|
|
392
|
+
list.appendChild(tab);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
content.replaceChildren();
|
|
396
|
+
const sel =
|
|
397
|
+
(selectedId && records.find((x) => x.id === selectedId)) ||
|
|
398
|
+
records[records.length - 1];
|
|
399
|
+
if (!sel) return;
|
|
400
|
+
|
|
401
|
+
if (documentChrome) {
|
|
402
|
+
const kind = sel.artifactType === "markdown" ? "MD" : sel.component ?? "Component";
|
|
403
|
+
const rawTitle = (sel.title || "Document").trim();
|
|
404
|
+
const baseTitle = rawTitle.replace(/\s*·\s*MD\s*$/i, "").trim() || "Document";
|
|
405
|
+
centerTitle.textContent = `${baseTitle} · ${kind}`;
|
|
406
|
+
} else {
|
|
407
|
+
titleEl.textContent = "Artifacts";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (sel.artifactType === "markdown") {
|
|
411
|
+
if (documentChrome && viewMode === "source") {
|
|
412
|
+
const pre = createElement(
|
|
413
|
+
"pre",
|
|
414
|
+
"persona-font-mono persona-text-xs persona-whitespace-pre-wrap persona-break-words persona-text-persona-primary"
|
|
415
|
+
);
|
|
416
|
+
pre.textContent = sel.markdown ?? "";
|
|
417
|
+
content.appendChild(pre);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const wrap = createElement("div", "persona-text-sm persona-leading-relaxed persona-markdown-bubble");
|
|
421
|
+
wrap.innerHTML = toHtml(sel.markdown ?? "");
|
|
422
|
+
content.appendChild(wrap);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const renderer = sel.component ? componentRegistry.get(sel.component) : undefined;
|
|
427
|
+
if (renderer) {
|
|
428
|
+
const stubMessage: AgentWidgetMessage = {
|
|
429
|
+
id: sel.id,
|
|
430
|
+
role: "assistant",
|
|
431
|
+
content: "",
|
|
432
|
+
createdAt: new Date().toISOString()
|
|
433
|
+
};
|
|
434
|
+
const ctx: ComponentContext = {
|
|
435
|
+
message: stubMessage,
|
|
436
|
+
config,
|
|
437
|
+
updateProps: () => {}
|
|
438
|
+
};
|
|
439
|
+
try {
|
|
440
|
+
const el = renderer(sel.props ?? {}, ctx);
|
|
441
|
+
if (el) {
|
|
442
|
+
content.appendChild(el);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
/* fall through */
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
content.appendChild(fallbackComponentCard(sel));
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const applyLayoutVisibility = () => {
|
|
453
|
+
const has = records.length > 0;
|
|
454
|
+
shell.classList.toggle("persona-hidden", !has);
|
|
455
|
+
if (backdrop) {
|
|
456
|
+
const root =
|
|
457
|
+
typeof shell.closest === "function" ? shell.closest("#persona-root") : null;
|
|
458
|
+
const narrowHost = root?.classList.contains("persona-artifact-narrow-host") ?? false;
|
|
459
|
+
const isMobile =
|
|
460
|
+
narrowHost ||
|
|
461
|
+
(typeof window !== "undefined" && window.matchMedia("(max-width: 640px)").matches);
|
|
462
|
+
if (has && isMobile && mobileOpen) {
|
|
463
|
+
backdrop.classList.remove("persona-hidden");
|
|
464
|
+
shell.classList.add("persona-artifact-drawer-open");
|
|
465
|
+
} else if (!isMobile) {
|
|
466
|
+
backdrop.classList.add("persona-hidden");
|
|
467
|
+
shell.classList.remove("persona-artifact-drawer-open");
|
|
468
|
+
} else {
|
|
469
|
+
// isMobile && !(has && mobileOpen): e.g. dismissed drawer — keep closed chrome in sync
|
|
470
|
+
backdrop.classList.add("persona-hidden");
|
|
471
|
+
shell.classList.remove("persona-artifact-drawer-open");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
element: shell,
|
|
478
|
+
backdrop,
|
|
479
|
+
update(state: { artifacts: PersonaArtifactRecord[]; selectedId: string | null }) {
|
|
480
|
+
records = state.artifacts;
|
|
481
|
+
selectedId =
|
|
482
|
+
state.selectedId ??
|
|
483
|
+
state.artifacts[state.artifacts.length - 1]?.id ??
|
|
484
|
+
null;
|
|
485
|
+
if (records.length > 0) {
|
|
486
|
+
mobileOpen = true;
|
|
487
|
+
}
|
|
488
|
+
render();
|
|
489
|
+
applyLayoutVisibility();
|
|
490
|
+
},
|
|
491
|
+
setMobileOpen(open: boolean) {
|
|
492
|
+
mobileOpen = open;
|
|
493
|
+
if (!open && backdrop) {
|
|
494
|
+
backdrop.classList.add("persona-hidden");
|
|
495
|
+
shell.classList.remove("persona-artifact-drawer-open");
|
|
496
|
+
} else {
|
|
497
|
+
applyLayoutVisibility();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|