@truefoundry/agent-ui-sdk 0.0.1
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 +634 -0
- package/dist/index.d.ts +718 -0
- package/dist/index.js +2736 -0
- package/dist/index.js.map +1 -0
- package/package.json +83 -0
- package/src/atoms/AskUserPrompt.tsx +102 -0
- package/src/atoms/AttachmentCard.tsx +92 -0
- package/src/atoms/AttachmentPickerButton.tsx +19 -0
- package/src/atoms/AttachmentPreviewDialog.tsx +34 -0
- package/src/atoms/BranchIndicator.tsx +41 -0
- package/src/atoms/CodeBlockHeader.tsx +44 -0
- package/src/atoms/ComposerShell.tsx +112 -0
- package/src/atoms/Markdown.test.tsx +28 -0
- package/src/atoms/Markdown.tsx +288 -0
- package/src/atoms/McpAuthPrompt.tsx +60 -0
- package/src/atoms/MessageActionBar.tsx +60 -0
- package/src/atoms/MessageBubble.tsx +80 -0
- package/src/atoms/MessageErrorBanner.tsx +26 -0
- package/src/atoms/MessageIndicator.tsx +23 -0
- package/src/atoms/OpenUIBlock.test.tsx +37 -0
- package/src/atoms/OpenUIBlock.tsx +50 -0
- package/src/atoms/ReasoningCard.tsx +52 -0
- package/src/atoms/SandboxArtifactList.tsx +63 -0
- package/src/atoms/ScrollToBottomButton.tsx +34 -0
- package/src/atoms/Skeletons.tsx +32 -0
- package/src/atoms/ThreadListMisc.tsx +76 -0
- package/src/atoms/ThreadListRow.tsx +81 -0
- package/src/atoms/ThreadShell.tsx +93 -0
- package/src/atoms/Toast.tsx +60 -0
- package/src/atoms/ToolApprovalBar.tsx +92 -0
- package/src/atoms/ToolCallCard.tsx +186 -0
- package/src/atoms/ToolGroupCard.tsx +52 -0
- package/src/atoms/UserMessageActionBar.tsx +62 -0
- package/src/atoms/WelcomeScreen.tsx +22 -0
- package/src/atoms/lib/cn.ts +6 -0
- package/src/atoms/primitives/Avatar.tsx +57 -0
- package/src/atoms/primitives/Button.tsx +73 -0
- package/src/atoms/primitives/Collapsible.tsx +32 -0
- package/src/atoms/primitives/Dialog.tsx +152 -0
- package/src/atoms/primitives/IconButton.tsx +43 -0
- package/src/atoms/primitives/Skeleton.tsx +17 -0
- package/src/atoms/primitives/Tooltip.tsx +58 -0
- package/src/containers/AskUserContainer.tsx +49 -0
- package/src/containers/AssistantMessageContainer.test.tsx +56 -0
- package/src/containers/AssistantMessageContainer.tsx +128 -0
- package/src/containers/AssistantTextContainer.test.tsx +111 -0
- package/src/containers/AssistantTextContainer.tsx +54 -0
- package/src/containers/AttachmentsContainer.tsx +83 -0
- package/src/containers/ComposerContainer.tsx +66 -0
- package/src/containers/ErrorToasterContainer.tsx +76 -0
- package/src/containers/McpAuthContainer.test.tsx +100 -0
- package/src/containers/McpAuthContainer.tsx +22 -0
- package/src/containers/MessageImageContainer.tsx +37 -0
- package/src/containers/ReasoningContainer.tsx +32 -0
- package/src/containers/RuntimeHarness.tsx +39 -0
- package/src/containers/Thread.tsx +9 -0
- package/src/containers/ThreadContainer.test.tsx +52 -0
- package/src/containers/ThreadContainer.tsx +87 -0
- package/src/containers/ThreadListContainer.tsx +63 -0
- package/src/containers/ToolCallContainer.test.tsx +135 -0
- package/src/containers/ToolCallContainer.tsx +167 -0
- package/src/containers/ToolGroupContainer.test.tsx +56 -0
- package/src/containers/ToolGroupContainer.tsx +24 -0
- package/src/containers/UserEditComposerContainer.test.tsx +52 -0
- package/src/containers/UserEditComposerContainer.tsx +86 -0
- package/src/containers/UserMessageContainer.test.tsx +99 -0
- package/src/containers/UserMessageContainer.tsx +26 -0
- package/src/containers/nestedApprovalBridge.ts +20 -0
- package/src/containers/useAttachmentPreviewSrc.ts +42 -0
- package/src/hooks/useComposerBusyState.test.ts +78 -0
- package/src/hooks/useComposerBusyState.ts +69 -0
- package/src/index.ts +193 -0
- package/src/openui.css +3 -0
- package/src/testSetup.ts +42 -0
- package/src/theme/SlotsProvider.test.tsx +64 -0
- package/src/theme/SlotsProvider.tsx +41 -0
- package/src/theme/defaultSlots.ts +131 -0
- package/src/theme/tokens.ts +98 -0
- package/src/theme/useClassDarkMode.ts +24 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Children, isValidElement, useMemo, type ReactNode } from "react";
|
|
2
|
+
import ReactMarkdown, { type Components } from "react-markdown";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
|
|
5
|
+
import { cn } from "./lib/cn.js";
|
|
6
|
+
import { CodeBlockHeader } from "./CodeBlockHeader.js";
|
|
7
|
+
import { OpenUIBlock } from "./OpenUIBlock.js";
|
|
8
|
+
import { SandboxArtifactList, type SandboxArtifactLink } from "./SandboxArtifactList.js";
|
|
9
|
+
|
|
10
|
+
function extractCodeText(node: ReactNode): string {
|
|
11
|
+
if (typeof node === "string") return node;
|
|
12
|
+
if (typeof node === "number") return String(node);
|
|
13
|
+
if (Array.isArray(node)) return node.map(extractCodeText).join("");
|
|
14
|
+
if (isValidElement(node)) {
|
|
15
|
+
return extractCodeText((node.props as { children?: ReactNode }).children);
|
|
16
|
+
}
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SANDBOX_ARTIFACT_TOKEN = "sandbox_artifact";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* By the time this runs, `[Label](path)` has already been parsed into a real
|
|
24
|
+
* `<a href="path">Label</a>` element by react-markdown — the raw path text is
|
|
25
|
+
* gone from any flattened string, only present as the anchor's `href` prop.
|
|
26
|
+
* So detection walks `children` as React elements, not a flattened string.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The gateway emits sandbox artifact links as a fenced code block tagged
|
|
30
|
+
* ```sandbox_artifact(s)``` containing raw `[label](path)` markdown-link text
|
|
31
|
+
* (never parsed into real anchors, since it's inside a code fence).
|
|
32
|
+
*/
|
|
33
|
+
function parseSandboxArtifactCodeBlock(code: string): SandboxArtifactLink[] | null {
|
|
34
|
+
const links: SandboxArtifactLink[] = [];
|
|
35
|
+
for (const match of code.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) {
|
|
36
|
+
links.push({ label: match[1]!, path: match[2]! });
|
|
37
|
+
}
|
|
38
|
+
return links.length > 0 ? links : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseSandboxArtifactParagraph(children: ReactNode): SandboxArtifactLink[] | null {
|
|
42
|
+
const items = Children.toArray(children);
|
|
43
|
+
if (items.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const first = items[0];
|
|
46
|
+
if (typeof first !== "string") return null;
|
|
47
|
+
const stripped = first.replace(/^\s*/, "");
|
|
48
|
+
if (!stripped.startsWith(SANDBOX_ARTIFACT_TOKEN)) return null;
|
|
49
|
+
if (!/^\s*$/.test(stripped.slice(SANDBOX_ARTIFACT_TOKEN.length))) return null;
|
|
50
|
+
|
|
51
|
+
const links: SandboxArtifactLink[] = [];
|
|
52
|
+
for (let i = 1; i < items.length; i++) {
|
|
53
|
+
const item = items[i];
|
|
54
|
+
if (typeof item === "string") {
|
|
55
|
+
const trimmed = item.trim();
|
|
56
|
+
if (trimmed === "" || trimmed === SANDBOX_ARTIFACT_TOKEN) continue;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (isValidElement(item) && typeof (item.props as { href?: unknown }).href === "string") {
|
|
60
|
+
links.push({
|
|
61
|
+
label: extractCodeText((item.props as { children?: ReactNode }).children),
|
|
62
|
+
path: (item.props as { href: string }).href,
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return links.length > 0 ? links : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createMarkdownComponents(
|
|
72
|
+
isStreaming?: boolean,
|
|
73
|
+
onDownloadArtifact?: (path: string) => void | Promise<unknown>,
|
|
74
|
+
): Components {
|
|
75
|
+
return {
|
|
76
|
+
h1: ({ className, ...props }) => (
|
|
77
|
+
<h1
|
|
78
|
+
className={cn(
|
|
79
|
+
"aui-md-h1 mt-5 mb-2 scroll-m-20 text-xl font-semibold first:mt-0 last:mb-0",
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
),
|
|
85
|
+
h2: ({ className, ...props }) => (
|
|
86
|
+
<h2
|
|
87
|
+
className={cn(
|
|
88
|
+
"aui-md-h2 mt-5 mb-2 scroll-m-20 text-lg font-semibold first:mt-0 last:mb-0",
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
),
|
|
94
|
+
h3: ({ className, ...props }) => (
|
|
95
|
+
<h3
|
|
96
|
+
className={cn(
|
|
97
|
+
"aui-md-h3 mt-4 mb-1.5 scroll-m-20 text-base font-semibold first:mt-0 last:mb-0",
|
|
98
|
+
className,
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
),
|
|
103
|
+
h4: ({ className, ...props }) => (
|
|
104
|
+
<h4
|
|
105
|
+
className={cn(
|
|
106
|
+
"aui-md-h4 mt-3.5 mb-1 scroll-m-20 text-base font-medium first:mt-0 last:mb-0",
|
|
107
|
+
className,
|
|
108
|
+
)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
),
|
|
112
|
+
h5: ({ className, ...props }) => (
|
|
113
|
+
<h5
|
|
114
|
+
className={cn("aui-md-h5 mt-3 mb-1 text-sm font-semibold first:mt-0 last:mb-0", className)}
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
),
|
|
118
|
+
h6: ({ className, ...props }) => (
|
|
119
|
+
<h6
|
|
120
|
+
className={cn("aui-md-h6 mt-3 mb-1 text-sm font-medium first:mt-0 last:mb-0", className)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
),
|
|
124
|
+
p: ({ className, children, ...props }) => {
|
|
125
|
+
const artifacts = parseSandboxArtifactParagraph(children);
|
|
126
|
+
if (artifacts != null) {
|
|
127
|
+
return <SandboxArtifactList artifacts={artifacts} onDownload={onDownloadArtifact} />;
|
|
128
|
+
}
|
|
129
|
+
return (
|
|
130
|
+
<p className={cn("aui-md-p my-3 leading-relaxed first:mt-0 last:mb-0", className)} {...props}>
|
|
131
|
+
{children}
|
|
132
|
+
</p>
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
a: ({ className, ...props }) => (
|
|
136
|
+
<a
|
|
137
|
+
className={cn(
|
|
138
|
+
"aui-md-a text-primary hover:text-primary/80 underline underline-offset-2",
|
|
139
|
+
className,
|
|
140
|
+
)}
|
|
141
|
+
target="_blank"
|
|
142
|
+
rel="noopener noreferrer"
|
|
143
|
+
{...props}
|
|
144
|
+
/>
|
|
145
|
+
),
|
|
146
|
+
blockquote: ({ className, ...props }) => (
|
|
147
|
+
<blockquote
|
|
148
|
+
className={cn(
|
|
149
|
+
"aui-md-blockquote border-muted-foreground/30 text-muted-foreground my-3 border-s-2 ps-4",
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
154
|
+
),
|
|
155
|
+
ul: ({ className, ...props }) => (
|
|
156
|
+
<ul
|
|
157
|
+
className={cn(
|
|
158
|
+
"aui-md-ul marker:text-muted-foreground my-3 ms-5 list-disc [&>li]:mt-1",
|
|
159
|
+
className,
|
|
160
|
+
)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
),
|
|
164
|
+
ol: ({ className, ...props }) => (
|
|
165
|
+
<ol
|
|
166
|
+
className={cn(
|
|
167
|
+
"aui-md-ol marker:text-muted-foreground my-3 ms-5 list-decimal [&>li]:mt-1",
|
|
168
|
+
className,
|
|
169
|
+
)}
|
|
170
|
+
{...props}
|
|
171
|
+
/>
|
|
172
|
+
),
|
|
173
|
+
hr: ({ className, ...props }) => (
|
|
174
|
+
<hr className={cn("aui-md-hr border-muted-foreground/20 my-3", className)} {...props} />
|
|
175
|
+
),
|
|
176
|
+
table: ({ className, ...props }) => (
|
|
177
|
+
<table
|
|
178
|
+
className={cn(
|
|
179
|
+
"aui-md-table my-3 w-full border-separate border-spacing-0 overflow-y-auto",
|
|
180
|
+
className,
|
|
181
|
+
)}
|
|
182
|
+
{...props}
|
|
183
|
+
/>
|
|
184
|
+
),
|
|
185
|
+
th: ({ className, ...props }) => (
|
|
186
|
+
<th
|
|
187
|
+
className={cn(
|
|
188
|
+
"aui-md-th bg-muted px-3 py-1.5 text-start font-medium first:rounded-ss-lg last:rounded-se-lg [[align=center]]:text-center [[align=right]]:text-right",
|
|
189
|
+
className,
|
|
190
|
+
)}
|
|
191
|
+
{...props}
|
|
192
|
+
/>
|
|
193
|
+
),
|
|
194
|
+
td: ({ className, ...props }) => (
|
|
195
|
+
<td
|
|
196
|
+
className={cn(
|
|
197
|
+
"aui-md-td border-muted-foreground/20 border-s border-b px-3 py-1.5 text-start last:border-e [[align=center]]:text-center [[align=right]]:text-right",
|
|
198
|
+
className,
|
|
199
|
+
)}
|
|
200
|
+
{...props}
|
|
201
|
+
/>
|
|
202
|
+
),
|
|
203
|
+
tr: ({ className, ...props }) => (
|
|
204
|
+
<tr
|
|
205
|
+
className={cn(
|
|
206
|
+
"aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-es-lg [&:last-child>td:last-child]:rounded-ee-lg",
|
|
207
|
+
className,
|
|
208
|
+
)}
|
|
209
|
+
{...props}
|
|
210
|
+
/>
|
|
211
|
+
),
|
|
212
|
+
li: ({ className, ...props }) => <li className={cn("aui-md-li leading-relaxed", className)} {...props} />,
|
|
213
|
+
strong: ({ className, ...props }) => (
|
|
214
|
+
<strong className={cn("aui-md-strong font-semibold", className)} {...props} />
|
|
215
|
+
),
|
|
216
|
+
sup: ({ className, ...props }) => (
|
|
217
|
+
<sup className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)} {...props} />
|
|
218
|
+
),
|
|
219
|
+
pre: ({ className, children, ...props }) => {
|
|
220
|
+
const codeElement = isValidElement(children) ? children : undefined;
|
|
221
|
+
const codeClassName = (codeElement?.props as { className?: string } | undefined)?.className ?? "";
|
|
222
|
+
const language = /language-(\S+)/.exec(codeClassName)?.[1] ?? "text";
|
|
223
|
+
const code = extractCodeText(children);
|
|
224
|
+
if (language === "openui") {
|
|
225
|
+
return <OpenUIBlock source={code} isStreaming={isStreaming} />;
|
|
226
|
+
}
|
|
227
|
+
if (language === "sandbox_artifact" || language === "sandbox_artifacts") {
|
|
228
|
+
const artifacts = parseSandboxArtifactCodeBlock(code);
|
|
229
|
+
if (artifacts != null) {
|
|
230
|
+
return <SandboxArtifactList artifacts={artifacts} onDownload={onDownloadArtifact} />;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return (
|
|
234
|
+
<>
|
|
235
|
+
<CodeBlockHeader language={language} code={code} />
|
|
236
|
+
<pre
|
|
237
|
+
className={cn(
|
|
238
|
+
"aui-md-pre border-border/50 bg-muted/30 overflow-x-auto rounded-t-none rounded-b-xl border border-t-0 p-3.5 text-[13px] leading-relaxed",
|
|
239
|
+
className,
|
|
240
|
+
)}
|
|
241
|
+
{...props}
|
|
242
|
+
>
|
|
243
|
+
{children}
|
|
244
|
+
</pre>
|
|
245
|
+
</>
|
|
246
|
+
);
|
|
247
|
+
},
|
|
248
|
+
code: ({ className, ...props }) => {
|
|
249
|
+
const isCodeBlock = /language-/.test(className ?? "");
|
|
250
|
+
return (
|
|
251
|
+
<code
|
|
252
|
+
className={cn(
|
|
253
|
+
!isCodeBlock && "aui-md-inline-code bg-muted rounded-md px-1.5 py-0.5 font-mono text-[0.85em]",
|
|
254
|
+
className,
|
|
255
|
+
)}
|
|
256
|
+
{...props}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type MarkdownProps = {
|
|
264
|
+
content: string;
|
|
265
|
+
className?: string;
|
|
266
|
+
isStreaming?: boolean;
|
|
267
|
+
onDownloadArtifact?: (path: string) => void | Promise<unknown>;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export function Markdown({ content, className, isStreaming, onDownloadArtifact }: MarkdownProps) {
|
|
271
|
+
const components = useMemo(
|
|
272
|
+
() => createMarkdownComponents(isStreaming, onDownloadArtifact),
|
|
273
|
+
[isStreaming, onDownloadArtifact],
|
|
274
|
+
);
|
|
275
|
+
return (
|
|
276
|
+
<div className={cn("aui-md", className)}>
|
|
277
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
|
278
|
+
{content}
|
|
279
|
+
</ReactMarkdown>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
declare module "../theme/SlotsProvider.js" {
|
|
285
|
+
interface AtomSlots {
|
|
286
|
+
Markdown: typeof Markdown;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ExternalLinkIcon } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./lib/cn.js";
|
|
4
|
+
import { Button } from "./primitives/Button.js";
|
|
5
|
+
|
|
6
|
+
export type McpAuthServer = { id: string; name: string; authUrl: string };
|
|
7
|
+
|
|
8
|
+
export type McpAuthPromptProps = {
|
|
9
|
+
servers: McpAuthServer[];
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
onContinue: () => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function McpAuthPrompt({ servers, disabled, onContinue, className }: McpAuthPromptProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div data-slot="aui_mcp-auth-continue-composer" className={cn("flex w-full flex-col gap-3", className)}>
|
|
18
|
+
<div className="border-primary/20 bg-background overflow-hidden rounded-2xl border">
|
|
19
|
+
<div className="bg-primary/10 border-primary/20 border-b px-4 py-2.5">
|
|
20
|
+
<p className="text-primary text-sm font-semibold">MCP Authentication Required</p>
|
|
21
|
+
</div>
|
|
22
|
+
<ul className="divide-border/60 divide-y">
|
|
23
|
+
{servers.map((server) => (
|
|
24
|
+
<li
|
|
25
|
+
key={server.id}
|
|
26
|
+
className="flex items-center justify-between gap-3 px-4 py-2.5"
|
|
27
|
+
>
|
|
28
|
+
<p className="text-sm">
|
|
29
|
+
<span className="text-muted-foreground">MCP Server Name</span>
|
|
30
|
+
<span className="text-muted-foreground mx-1.5">:</span>
|
|
31
|
+
<span className="font-semibold">{server.name}</span>
|
|
32
|
+
</p>
|
|
33
|
+
<Button
|
|
34
|
+
asChild
|
|
35
|
+
size="sm"
|
|
36
|
+
className="bg-foreground text-background hover:bg-foreground/80 rounded-full"
|
|
37
|
+
>
|
|
38
|
+
<a href={server.authUrl} target="_blank" rel="noopener noreferrer">
|
|
39
|
+
Connect
|
|
40
|
+
<ExternalLinkIcon className="size-3.5" />
|
|
41
|
+
</a>
|
|
42
|
+
</Button>
|
|
43
|
+
</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="flex justify-end px-2.5">
|
|
48
|
+
<Button type="button" className="rounded-full px-4" disabled={disabled} onClick={onContinue}>
|
|
49
|
+
Continue
|
|
50
|
+
</Button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare module "../theme/SlotsProvider.js" {
|
|
57
|
+
interface AtomSlots {
|
|
58
|
+
McpAuthPrompt: typeof McpAuthPrompt;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CheckIcon, CopyIcon, DownloadIcon, MoreHorizontalIcon } from "lucide-react";
|
|
2
|
+
import { DropdownMenu } from "radix-ui";
|
|
3
|
+
|
|
4
|
+
import { cn } from "./lib/cn.js";
|
|
5
|
+
import { IconButton } from "./primitives/IconButton.js";
|
|
6
|
+
|
|
7
|
+
export type MessageActionBarProps = {
|
|
8
|
+
isCopied: boolean;
|
|
9
|
+
onCopy: () => void;
|
|
10
|
+
onExportMarkdown: () => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function MessageActionBar({ isCopied, onCopy, onExportMarkdown, className }: MessageActionBarProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={cn(
|
|
18
|
+
"aui-assistant-action-bar-root text-muted-foreground animate-in fade-in flex gap-1 duration-200",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
<IconButton tooltip="Copy" onClick={onCopy}>
|
|
23
|
+
{isCopied ? (
|
|
24
|
+
<CheckIcon className="animate-in zoom-in-50 fade-in duration-200 ease-out" />
|
|
25
|
+
) : (
|
|
26
|
+
<CopyIcon className="animate-in zoom-in-75 fade-in duration-150" />
|
|
27
|
+
)}
|
|
28
|
+
</IconButton>
|
|
29
|
+
<DropdownMenu.Root>
|
|
30
|
+
<DropdownMenu.Trigger asChild>
|
|
31
|
+
<IconButton tooltip="More" className="data-[state=open]:bg-accent">
|
|
32
|
+
<MoreHorizontalIcon />
|
|
33
|
+
</IconButton>
|
|
34
|
+
</DropdownMenu.Trigger>
|
|
35
|
+
<DropdownMenu.Portal>
|
|
36
|
+
<DropdownMenu.Content
|
|
37
|
+
side="bottom"
|
|
38
|
+
align="start"
|
|
39
|
+
sideOffset={6}
|
|
40
|
+
className="aui-action-bar-more-content bg-popover/95 text-popover-foreground data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:animate-out data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-xl border p-1.5 shadow-lg backdrop-blur-sm"
|
|
41
|
+
>
|
|
42
|
+
<DropdownMenu.Item
|
|
43
|
+
onSelect={onExportMarkdown}
|
|
44
|
+
className="aui-action-bar-more-item hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground flex cursor-pointer items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm outline-none select-none"
|
|
45
|
+
>
|
|
46
|
+
<DownloadIcon className="size-4" />
|
|
47
|
+
Export as Markdown
|
|
48
|
+
</DropdownMenu.Item>
|
|
49
|
+
</DropdownMenu.Content>
|
|
50
|
+
</DropdownMenu.Portal>
|
|
51
|
+
</DropdownMenu.Root>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare module "../theme/SlotsProvider.js" {
|
|
57
|
+
interface AtomSlots {
|
|
58
|
+
MessageActionBar: typeof MessageActionBar;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./lib/cn.js";
|
|
4
|
+
|
|
5
|
+
export type AssistantMessageBubbleProps = {
|
|
6
|
+
variant: "assistant";
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
error?: ReactNode;
|
|
9
|
+
branchIndicator?: ReactNode;
|
|
10
|
+
actionBar?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type UserMessageBubbleProps = {
|
|
15
|
+
variant: "user";
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
attachments?: ReactNode;
|
|
18
|
+
branchIndicator?: ReactNode;
|
|
19
|
+
actionBar?: ReactNode;
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type MessageBubbleProps = AssistantMessageBubbleProps | UserMessageBubbleProps;
|
|
24
|
+
|
|
25
|
+
export function MessageBubble(props: MessageBubbleProps) {
|
|
26
|
+
if (props.variant === "assistant") {
|
|
27
|
+
const { children, error, branchIndicator, actionBar, className } = props;
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
data-slot="aui_assistant-message-root"
|
|
31
|
+
className={cn("fade-in slide-in-from-bottom-1 animate-in relative duration-150", className)}
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
data-slot="aui_assistant-message-content"
|
|
35
|
+
className="text-foreground flex flex-col gap-3 px-2 leading-relaxed wrap-break-word [contain-intrinsic-size:auto_24px] [content-visibility:auto]"
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
{error}
|
|
39
|
+
</div>
|
|
40
|
+
<div data-slot="aui_assistant-message-footer" className="ms-2 -mb-7.5 min-h-7.5 pt-1.5 flex items-center">
|
|
41
|
+
{branchIndicator}
|
|
42
|
+
{actionBar}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { children, attachments, branchIndicator, actionBar, className } = props;
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
data-slot="aui_user-message-root"
|
|
52
|
+
className={cn(
|
|
53
|
+
"fade-in slide-in-from-bottom-1 animate-in grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 duration-150 [contain-intrinsic-size:auto_60px] [content-visibility:auto] [&:where(>*)]:col-start-2",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{attachments}
|
|
58
|
+
<div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
|
|
59
|
+
<div className="aui-user-message-content bg-muted text-foreground rounded-xl px-4 py-2 wrap-break-word empty:hidden">
|
|
60
|
+
{children}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
{(branchIndicator != null || actionBar != null) && (
|
|
64
|
+
<div
|
|
65
|
+
data-slot="aui_user-message-footer"
|
|
66
|
+
className="col-span-full col-start-1 row-start-3 -me-1 flex items-center justify-end gap-1"
|
|
67
|
+
>
|
|
68
|
+
{branchIndicator}
|
|
69
|
+
{actionBar}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare module "../theme/SlotsProvider.js" {
|
|
77
|
+
interface AtomSlots {
|
|
78
|
+
MessageBubble: typeof MessageBubble;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cn } from "./lib/cn.js";
|
|
2
|
+
|
|
3
|
+
export type MessageErrorBannerProps = {
|
|
4
|
+
message: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function MessageErrorBanner({ message, className }: MessageErrorBannerProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
role="alert"
|
|
12
|
+
className={cn(
|
|
13
|
+
"aui-message-error-root border-destructive bg-destructive/10 text-destructive dark:bg-destructive/5 mt-2 rounded-md border p-3 text-sm dark:text-red-200",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
<span className="aui-message-error-message line-clamp-2">{message}</span>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module "../theme/SlotsProvider.js" {
|
|
23
|
+
interface AtomSlots {
|
|
24
|
+
MessageErrorBanner: typeof MessageErrorBanner;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cn } from "./lib/cn.js";
|
|
2
|
+
|
|
3
|
+
export type MessageIndicatorProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function MessageIndicator({ className }: MessageIndicatorProps) {
|
|
8
|
+
return (
|
|
9
|
+
<span
|
|
10
|
+
data-slot="aui_assistant-message-indicator"
|
|
11
|
+
className={cn("animate-pulse font-sans", className)}
|
|
12
|
+
aria-label="Assistant is working"
|
|
13
|
+
>
|
|
14
|
+
{"●"}
|
|
15
|
+
</span>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module "../theme/SlotsProvider.js" {
|
|
20
|
+
interface AtomSlots {
|
|
21
|
+
MessageIndicator: typeof MessageIndicator;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { OpenUIBlock } from "./OpenUIBlock.js";
|
|
6
|
+
|
|
7
|
+
describe("OpenUIBlock", () => {
|
|
8
|
+
it("renders OpenUI source through Renderer", () => {
|
|
9
|
+
render(<OpenUIBlock source={'Card() { title: "Hello" }'} />);
|
|
10
|
+
expect(screen.getByTestId("openui-renderer")).toHaveTextContent('Card() { title: "Hello" }');
|
|
11
|
+
expect(screen.getByTestId("openui-renderer")).toHaveAttribute("data-streaming", "false");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("forwards isStreaming to Renderer", () => {
|
|
15
|
+
render(<OpenUIBlock source="Card() {}" isStreaming />);
|
|
16
|
+
expect(screen.getByTestId("openui-renderer")).toHaveAttribute("data-streaming", "true");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("exposes a stable wrapper slot for integration tests", () => {
|
|
20
|
+
render(<OpenUIBlock source="Card() {}" />);
|
|
21
|
+
expect(screen.getByTestId("openui-renderer").closest("[data-slot='openui-block']")).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("wraps content in OpenUI ThemeProvider using the html .dark class by default", () => {
|
|
25
|
+
document.documentElement.classList.add("dark");
|
|
26
|
+
render(<OpenUIBlock source="Card() {}" />);
|
|
27
|
+
expect(screen.getByTestId("openui-theme-provider")).toHaveAttribute("data-mode", "dark");
|
|
28
|
+
document.documentElement.classList.remove("dark");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("honors an explicit mode override", () => {
|
|
32
|
+
document.documentElement.classList.add("dark");
|
|
33
|
+
render(<OpenUIBlock source="Card() {}" mode="light" />);
|
|
34
|
+
expect(screen.getByTestId("openui-theme-provider")).toHaveAttribute("data-mode", "light");
|
|
35
|
+
document.documentElement.classList.remove("dark");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { memo, useId } from "react";
|
|
2
|
+
import { Renderer } from "@openuidev/react-lang";
|
|
3
|
+
import { ThemeProvider } from "@openuidev/react-ui";
|
|
4
|
+
import { openuiLibrary } from "@openuidev/react-ui/genui-lib";
|
|
5
|
+
|
|
6
|
+
import { useClassDarkMode, type ThemeMode } from "../theme/useClassDarkMode.js";
|
|
7
|
+
import { cn } from "./lib/cn.js";
|
|
8
|
+
|
|
9
|
+
export type OpenUIBlockProps = {
|
|
10
|
+
source: string;
|
|
11
|
+
isStreaming?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Override light/dark mode. Defaults to syncing with a `.dark` class on `<html>`. */
|
|
14
|
+
mode?: ThemeMode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Renders an OpenUI Lang fenced code block (```openui ... ```). */
|
|
18
|
+
export const OpenUIBlock = memo(
|
|
19
|
+
function OpenUIBlock({ source, isStreaming, className, mode: modeProp }: OpenUIBlockProps) {
|
|
20
|
+
const detectedMode = useClassDarkMode();
|
|
21
|
+
const mode = modeProp ?? detectedMode;
|
|
22
|
+
const scopeClass = `aui-openui-theme-${useId().replace(/:/g, "")}`;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ThemeProvider mode={mode} cssSelector={`.${scopeClass}`}>
|
|
26
|
+
<div
|
|
27
|
+
data-slot="openui-block"
|
|
28
|
+
className={cn("aui-openui-block my-3", scopeClass, className)}
|
|
29
|
+
style={{ contain: "layout paint" }}
|
|
30
|
+
>
|
|
31
|
+
<Renderer
|
|
32
|
+
response={source}
|
|
33
|
+
library={openuiLibrary}
|
|
34
|
+
isStreaming={!!isStreaming}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
</ThemeProvider>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
(prev, next) =>
|
|
41
|
+
prev.source === next.source &&
|
|
42
|
+
!!prev.isStreaming === !!next.isStreaming &&
|
|
43
|
+
prev.mode === next.mode,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
declare module "../theme/SlotsProvider.js" {
|
|
47
|
+
interface AtomSlots {
|
|
48
|
+
OpenUIBlock: typeof OpenUIBlock;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "./lib/cn.js";
|
|
5
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./primitives/Collapsible.js";
|
|
6
|
+
|
|
7
|
+
export type ReasoningCardProps = {
|
|
8
|
+
streaming: boolean;
|
|
9
|
+
expanded: boolean;
|
|
10
|
+
onToggle: () => void;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function ReasoningCard({ streaming, expanded, onToggle, children, className }: ReasoningCardProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Collapsible
|
|
18
|
+
data-slot="reasoning-card"
|
|
19
|
+
open={expanded}
|
|
20
|
+
onOpenChange={onToggle}
|
|
21
|
+
className={cn("aui-reasoning-card w-full rounded-lg border px-3 py-2", className)}
|
|
22
|
+
>
|
|
23
|
+
<CollapsibleTrigger className="text-muted-foreground hover:text-foreground flex max-w-[75%] origin-left items-center gap-2 py-1.5 text-sm transition-[color,scale] active:scale-[0.98]">
|
|
24
|
+
<BrainIcon className="size-4 shrink-0" />
|
|
25
|
+
<span className="leading-none tabular-nums">
|
|
26
|
+
Reasoning
|
|
27
|
+
{streaming && (
|
|
28
|
+
<span aria-hidden className="shimmer pointer-events-none">
|
|
29
|
+
{" "}
|
|
30
|
+
…
|
|
31
|
+
</span>
|
|
32
|
+
)}
|
|
33
|
+
</span>
|
|
34
|
+
<ChevronDownIcon
|
|
35
|
+
className={cn(
|
|
36
|
+
"mt-0.5 size-4 shrink-0 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)]",
|
|
37
|
+
expanded ? "rotate-0" : "-rotate-90",
|
|
38
|
+
)}
|
|
39
|
+
/>
|
|
40
|
+
</CollapsibleTrigger>
|
|
41
|
+
<CollapsibleContent className="text-muted-foreground max-h-64 overflow-y-auto pt-2 pb-1 pl-6 text-sm leading-relaxed">
|
|
42
|
+
{children}
|
|
43
|
+
</CollapsibleContent>
|
|
44
|
+
</Collapsible>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare module "../theme/SlotsProvider.js" {
|
|
49
|
+
interface AtomSlots {
|
|
50
|
+
ReasoningCard: typeof ReasoningCard;
|
|
51
|
+
}
|
|
52
|
+
}
|