e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
711 lines
22 KiB
TypeScript
711 lines
22 KiB
TypeScript
import type { BuilderConfig } from "@/components/builder/types";
|
|
|
|
const REGISTRY_BASE_URL = "https://r.assistant-ui.com";
|
|
|
|
export function determineRegistryDependencies(config: BuilderConfig): string[] {
|
|
const { components } = config;
|
|
const deps: string[] = [
|
|
"button",
|
|
`${REGISTRY_BASE_URL}/tooltip-icon-button.json`,
|
|
];
|
|
|
|
if (components.markdown) {
|
|
deps.push(`${REGISTRY_BASE_URL}/markdown-text.json`);
|
|
deps.push(`${REGISTRY_BASE_URL}/tool-fallback.json`);
|
|
}
|
|
|
|
if (components.attachments) {
|
|
deps.push(`${REGISTRY_BASE_URL}/attachment.json`);
|
|
}
|
|
|
|
return deps;
|
|
}
|
|
|
|
export function generateCssVars(
|
|
config: BuilderConfig,
|
|
mode: "light" | "dark",
|
|
): Record<string, string> {
|
|
const { styles } = config;
|
|
const vars: Record<string, string> = {};
|
|
|
|
const accentColor =
|
|
mode === "light" ? styles.colors.accent.light : styles.colors.accent.dark;
|
|
vars["--aui-accent"] = accentColor;
|
|
vars["--aui-accent-foreground"] = isLightColor(accentColor)
|
|
? "#000000"
|
|
: "#ffffff";
|
|
|
|
if (styles.colors.background) {
|
|
vars["--aui-background"] =
|
|
mode === "light"
|
|
? styles.colors.background.light
|
|
: styles.colors.background.dark;
|
|
}
|
|
|
|
if (styles.colors.foreground) {
|
|
vars["--aui-foreground"] =
|
|
mode === "light"
|
|
? styles.colors.foreground.light
|
|
: styles.colors.foreground.dark;
|
|
}
|
|
|
|
if (styles.colors.muted) {
|
|
vars["--aui-muted"] =
|
|
mode === "light" ? styles.colors.muted.light : styles.colors.muted.dark;
|
|
}
|
|
|
|
if (styles.colors.mutedForeground) {
|
|
vars["--aui-muted-foreground"] =
|
|
mode === "light"
|
|
? styles.colors.mutedForeground.light
|
|
: styles.colors.mutedForeground.dark;
|
|
}
|
|
|
|
if (styles.colors.border) {
|
|
vars["--aui-border"] =
|
|
mode === "light" ? styles.colors.border.light : styles.colors.border.dark;
|
|
}
|
|
|
|
if (styles.colors.userMessage) {
|
|
vars["--aui-user-message"] =
|
|
mode === "light"
|
|
? styles.colors.userMessage.light
|
|
: styles.colors.userMessage.dark;
|
|
}
|
|
|
|
if (styles.colors.composer) {
|
|
vars["--aui-composer"] =
|
|
mode === "light"
|
|
? styles.colors.composer.light
|
|
: styles.colors.composer.dark;
|
|
}
|
|
|
|
vars["--aui-max-width"] = styles.maxWidth;
|
|
vars["--aui-border-radius"] = getBorderRadiusValue(styles.borderRadius);
|
|
vars["--aui-font-family"] = styles.fontFamily;
|
|
|
|
return vars;
|
|
}
|
|
|
|
function getBorderRadiusValue(radius: string): string {
|
|
const map: Record<string, string> = {
|
|
none: "0",
|
|
sm: "0.125rem",
|
|
md: "0.375rem",
|
|
lg: "0.5rem",
|
|
full: "1.5rem",
|
|
};
|
|
return map[radius] || "0.5rem";
|
|
}
|
|
|
|
function isLightColor(hexColor: string): boolean {
|
|
const hex = hexColor.replace("#", "");
|
|
const r = parseInt(hex.substring(0, 2), 16);
|
|
const g = parseInt(hex.substring(2, 4), 16);
|
|
const b = parseInt(hex.substring(4, 6), 16);
|
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
return luminance > 0.5;
|
|
}
|
|
|
|
export function generateRegistryJson(config: BuilderConfig) {
|
|
const registryDependencies = determineRegistryDependencies(config);
|
|
const threadCode = generateThreadCode(config);
|
|
|
|
return {
|
|
name: "assistant-ui-thread",
|
|
type: "registry:block",
|
|
dependencies: [
|
|
"@assistant-ui/react",
|
|
"@assistant-ui/react-ui",
|
|
"lucide-react",
|
|
...(config.components.markdown ? ["@assistant-ui/react-markdown"] : []),
|
|
],
|
|
registryDependencies,
|
|
files: [
|
|
{
|
|
path: "components/assistant-ui/thread.tsx",
|
|
content: threadCode,
|
|
type: "registry:component",
|
|
},
|
|
],
|
|
cssVars: {
|
|
light: generateCssVars(config, "light"),
|
|
dark: generateCssVars(config, "dark"),
|
|
},
|
|
};
|
|
}
|
|
|
|
function generateThreadCode(config: BuilderConfig): string {
|
|
const { components, styles } = config;
|
|
|
|
const externalImports = [
|
|
generateIconImports(config),
|
|
`import {`,
|
|
` ActionBarPrimitive,`,
|
|
` AuiIf,`,
|
|
components.branchPicker ? ` BranchPickerPrimitive,` : null,
|
|
` ComposerPrimitive,`,
|
|
` ErrorPrimitive,`,
|
|
` MessagePrimitive,`,
|
|
` ThreadPrimitive,`,
|
|
`} from "@assistant-ui/react";`,
|
|
components.markdown && components.typingIndicator === "dot"
|
|
? `import "@assistant-ui/react-markdown/styles/dot.css";`
|
|
: null,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
|
|
const internalImports = [
|
|
`import { Button } from "@/components/ui/button";`,
|
|
`import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";`,
|
|
components.markdown
|
|
? `import { MarkdownText } from "@/components/assistant-ui/markdown-text";`
|
|
: null,
|
|
components.markdown
|
|
? `import { ToolFallback } from "@/components/assistant-ui/tool-fallback";`
|
|
: null,
|
|
components.attachments
|
|
? `import {
|
|
ComposerAddAttachment,
|
|
ComposerAttachments,
|
|
UserMessageAttachments,
|
|
} from "@/components/assistant-ui/attachment";`
|
|
: null,
|
|
`import { cn } from "@/lib/utils";`,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
|
|
const imports = `"use client";
|
|
|
|
${externalImports}
|
|
|
|
${internalImports}`;
|
|
|
|
const borderRadiusClass = getBorderRadiusClass(styles.borderRadius);
|
|
const fontSizeClass = getFontSizeClass(styles.fontSize);
|
|
const messageSpacingClass = getMessageSpacingClass(styles.messageSpacing);
|
|
const accentColor = styles.colors.accent.light;
|
|
const accentForeground = isLightColor(accentColor) ? "#000000" : "#ffffff";
|
|
|
|
const threadComponent = `
|
|
export function Thread() {
|
|
return (
|
|
<ThreadPrimitive.Root
|
|
className="flex h-full flex-col bg-background ${fontSizeClass}"
|
|
style={{
|
|
"--thread-max-width": "${styles.maxWidth}",
|
|
"--accent-color": "${accentColor}",
|
|
"--accent-foreground": "${accentForeground}",${styles.fontFamily !== "system-ui" ? `\n fontFamily: "${styles.fontFamily}",` : ""}
|
|
}}
|
|
>
|
|
<ThreadPrimitive.Viewport
|
|
turnAnchor="top"
|
|
className="relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
|
|
>
|
|
${
|
|
components.threadWelcome
|
|
? `<AuiIf condition={(s) => s.thread.isEmpty}>
|
|
<ThreadWelcome />
|
|
</AuiIf>`
|
|
: ""
|
|
}
|
|
|
|
<ThreadPrimitive.Messages
|
|
components={{
|
|
UserMessage,${components.editMessage ? `\n EditComposer,` : ""}
|
|
AssistantMessage,
|
|
}}
|
|
/>
|
|
|
|
<ThreadPrimitive.ViewportFooter className="sticky bottom-0 mx-auto mt-auto flex w-full max-w-[var(--thread-max-width)] flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4">
|
|
${components.scrollToBottom ? "<ThreadScrollToBottom />" : ""}
|
|
<Composer />
|
|
</ThreadPrimitive.ViewportFooter>
|
|
</ThreadPrimitive.Viewport>
|
|
</ThreadPrimitive.Root>
|
|
);
|
|
}`;
|
|
|
|
const additionalComponents = [
|
|
components.threadWelcome
|
|
? generateWelcomeComponent(config, borderRadiusClass)
|
|
: "",
|
|
generateComposerComponent(config, borderRadiusClass),
|
|
components.scrollToBottom ? generateScrollToBottomComponent() : "",
|
|
generateUserMessageComponent(
|
|
config,
|
|
borderRadiusClass,
|
|
messageSpacingClass,
|
|
),
|
|
components.editMessage
|
|
? generateEditComposerComponent(borderRadiusClass)
|
|
: "",
|
|
generateAssistantMessageComponent(config, messageSpacingClass),
|
|
generateActionBarComponent(config),
|
|
components.branchPicker ? generateBranchPickerComponent() : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
|
|
return imports + threadComponent + additionalComponents;
|
|
}
|
|
|
|
function generateIconImports(config: BuilderConfig): string {
|
|
const { components } = config;
|
|
const icons: string[] = ["ArrowUpIcon", "DownloadIcon", "SquareIcon"];
|
|
|
|
if (components.scrollToBottom) icons.push("ArrowDownIcon");
|
|
if (components.editMessage) icons.push("PencilIcon");
|
|
if (components.branchPicker)
|
|
icons.push("ChevronLeftIcon", "ChevronRightIcon");
|
|
if (components.actionBar.copy) icons.push("CheckIcon", "CopyIcon");
|
|
if (components.actionBar.reload) icons.push("RefreshCwIcon");
|
|
if (components.actionBar.speak) icons.push("Volume2Icon");
|
|
if (components.actionBar.feedback)
|
|
icons.push("ThumbsUpIcon", "ThumbsDownIcon");
|
|
if (components.avatar) icons.push("BotIcon", "UserIcon");
|
|
if (components.loadingIndicator !== "none") icons.push("LoaderIcon");
|
|
if (components.reasoning) icons.push("ChevronDownIcon");
|
|
|
|
return `import {\n ${[...new Set(icons)].sort().join(",\n ")},\n} from "lucide-react";`;
|
|
}
|
|
|
|
function getBorderRadiusClass(radius: string): string {
|
|
return (
|
|
{
|
|
none: "rounded-none",
|
|
sm: "rounded-sm",
|
|
md: "rounded-md",
|
|
lg: "rounded-lg",
|
|
full: "rounded-3xl",
|
|
}[radius] || "rounded-lg"
|
|
);
|
|
}
|
|
|
|
function getFontSizeClass(fontSize: string): string {
|
|
return (
|
|
{
|
|
sm: "text-sm",
|
|
base: "text-base",
|
|
lg: "text-lg",
|
|
}[fontSize] || "text-base"
|
|
);
|
|
}
|
|
|
|
function getMessageSpacingClass(spacing: string): string {
|
|
return (
|
|
{
|
|
compact: "py-2",
|
|
comfortable: "py-4",
|
|
spacious: "py-6",
|
|
}[spacing] || "py-4"
|
|
);
|
|
}
|
|
|
|
function generateWelcomeComponent(
|
|
config: BuilderConfig,
|
|
borderRadiusClass: string,
|
|
): string {
|
|
const { components } = config;
|
|
return `
|
|
function ThreadWelcome() {
|
|
return (
|
|
<div className="mx-auto my-auto flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col">
|
|
<div className="flex w-full flex-grow flex-col items-center justify-center">
|
|
<div className="flex size-full flex-col justify-center px-8">
|
|
<div className="text-2xl font-semibold">Hello there!</div>
|
|
<div className="text-2xl text-muted-foreground/65">
|
|
How can I help you today?
|
|
</div>
|
|
</div>
|
|
</div>
|
|
${
|
|
components.suggestions
|
|
? `<div className="grid w-full gap-2 pb-4 md:grid-cols-2">
|
|
<ThreadPrimitive.Suggestion prompt="What's the weather in San Francisco?" asChild>
|
|
<Button variant="ghost" className="h-auto w-full flex-col items-start justify-start gap-1 border ${borderRadiusClass} px-5 py-4 text-left text-sm">
|
|
<span className="font-medium">What's the weather</span>
|
|
<span className="text-muted-foreground">in San Francisco?</span>
|
|
</Button>
|
|
</ThreadPrimitive.Suggestion>
|
|
<ThreadPrimitive.Suggestion prompt="Explain React hooks like useState" asChild>
|
|
<Button variant="ghost" className="h-auto w-full flex-col items-start justify-start gap-1 border ${borderRadiusClass} px-5 py-4 text-left text-sm">
|
|
<span className="font-medium">Explain React hooks</span>
|
|
<span className="text-muted-foreground">like useState</span>
|
|
</Button>
|
|
</ThreadPrimitive.Suggestion>
|
|
</div>`
|
|
: ""
|
|
}
|
|
</div>
|
|
);
|
|
}`;
|
|
}
|
|
|
|
function generateComposerComponent(
|
|
config: BuilderConfig,
|
|
borderRadiusClass: string,
|
|
): string {
|
|
const { components } = config;
|
|
return `
|
|
function Composer() {
|
|
return (
|
|
<ComposerPrimitive.Root className="relative flex w-full flex-col">
|
|
<ComposerPrimitive.AttachmentDropzone className="flex w-full flex-col ${borderRadiusClass} border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50">
|
|
${components.attachments ? "<ComposerAttachments />" : ""}
|
|
<ComposerPrimitive.Input
|
|
placeholder="Send a message..."
|
|
className="mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0"
|
|
rows={1}
|
|
autoFocus
|
|
aria-label="Message input"
|
|
/>
|
|
<ComposerAction />
|
|
</ComposerPrimitive.AttachmentDropzone>
|
|
</ComposerPrimitive.Root>
|
|
);
|
|
}
|
|
|
|
function ComposerAction() {
|
|
return (
|
|
<div className="relative mx-2 mb-2 flex items-center justify-between">
|
|
${components.attachments ? "<ComposerAddAttachment />" : "<div />"}
|
|
|
|
<AuiIf condition={(s) => !s.thread.isRunning}>
|
|
<ComposerPrimitive.Send asChild>
|
|
<TooltipIconButton
|
|
tooltip="Send message"
|
|
side="bottom"
|
|
type="submit"
|
|
variant="default"
|
|
size="icon"
|
|
className="size-8 rounded-full"
|
|
style={{
|
|
backgroundColor: "var(--accent-color)",
|
|
color: "var(--accent-foreground)",
|
|
}}
|
|
aria-label="Send message"
|
|
>
|
|
<ArrowUpIcon className="size-4" />
|
|
</TooltipIconButton>
|
|
</ComposerPrimitive.Send>
|
|
</AuiIf>
|
|
|
|
<AuiIf condition={(s) => s.thread.isRunning}>
|
|
<ComposerPrimitive.Cancel asChild>
|
|
<Button
|
|
type="button"
|
|
variant="default"
|
|
size="icon"
|
|
className="size-8 rounded-full"
|
|
style={{
|
|
backgroundColor: "var(--accent-color)",
|
|
color: "var(--accent-foreground)",
|
|
}}
|
|
aria-label="Stop generating"
|
|
>
|
|
<SquareIcon className="size-3 fill-current" />
|
|
</Button>
|
|
</ComposerPrimitive.Cancel>
|
|
</AuiIf>
|
|
</div>
|
|
);
|
|
}`;
|
|
}
|
|
|
|
function generateScrollToBottomComponent(): string {
|
|
return `
|
|
function ThreadScrollToBottom() {
|
|
return (
|
|
<ThreadPrimitive.ScrollToBottom asChild>
|
|
<TooltipIconButton
|
|
tooltip="Scroll to bottom"
|
|
variant="outline"
|
|
className="absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible"
|
|
>
|
|
<ArrowDownIcon />
|
|
</TooltipIconButton>
|
|
</ThreadPrimitive.ScrollToBottom>
|
|
);
|
|
}`;
|
|
}
|
|
|
|
function generateUserMessageComponent(
|
|
config: BuilderConfig,
|
|
borderRadiusClass: string,
|
|
messageSpacingClass: string,
|
|
): string {
|
|
const { components, styles } = config;
|
|
const animationClass = styles.animations
|
|
? " fade-in slide-in-from-bottom-1 animate-in duration-150"
|
|
: "";
|
|
|
|
return `
|
|
function UserMessage() {
|
|
return (
|
|
<MessagePrimitive.Root
|
|
className="mx-auto grid w-full max-w-[var(--thread-max-width)] auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 ${messageSpacingClass}${animationClass}"
|
|
data-role="user"
|
|
>
|
|
${components.attachments ? "<UserMessageAttachments />" : ""}
|
|
|
|
<div className="relative col-start-2 min-w-0">
|
|
<div className="${borderRadiusClass} bg-muted px-4 py-2.5 break-words text-foreground">
|
|
<MessagePrimitive.Parts />
|
|
</div>
|
|
${
|
|
components.editMessage
|
|
? `<div className="absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2">
|
|
<UserActionBar />
|
|
</div>`
|
|
: ""
|
|
}
|
|
</div>
|
|
|
|
${components.branchPicker ? `<BranchPicker className="col-span-full col-start-1 row-start-3 -mr-1 justify-end" />` : ""}
|
|
</MessagePrimitive.Root>
|
|
);
|
|
}
|
|
|
|
${
|
|
components.editMessage
|
|
? `function UserActionBar() {
|
|
return (
|
|
<ActionBarPrimitive.Root
|
|
hideWhenRunning
|
|
autohide="not-last"
|
|
className="flex flex-col items-end"
|
|
>
|
|
<ActionBarPrimitive.Edit asChild>
|
|
<TooltipIconButton tooltip="Edit" className="p-4">
|
|
<PencilIcon />
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.Edit>
|
|
</ActionBarPrimitive.Root>
|
|
);
|
|
}`
|
|
: ""
|
|
}`;
|
|
}
|
|
|
|
function generateEditComposerComponent(borderRadiusClass: string): string {
|
|
return `
|
|
function EditComposer() {
|
|
return (
|
|
<MessagePrimitive.Root className="mx-auto flex w-full max-w-[var(--thread-max-width)] flex-col px-2 py-3">
|
|
<ComposerPrimitive.Root className="ml-auto flex w-full max-w-[85%] flex-col ${borderRadiusClass} bg-muted">
|
|
<ComposerPrimitive.Input
|
|
className="min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none"
|
|
autoFocus
|
|
/>
|
|
<div className="mx-3 mb-3 flex items-center gap-2 self-end">
|
|
<ComposerPrimitive.Cancel asChild>
|
|
<Button variant="ghost" size="sm">Cancel</Button>
|
|
</ComposerPrimitive.Cancel>
|
|
<ComposerPrimitive.Send asChild>
|
|
<Button size="sm">Update</Button>
|
|
</ComposerPrimitive.Send>
|
|
</div>
|
|
</ComposerPrimitive.Root>
|
|
</MessagePrimitive.Root>
|
|
);
|
|
}`;
|
|
}
|
|
|
|
function generateAssistantMessageComponent(
|
|
config: BuilderConfig,
|
|
messageSpacingClass: string,
|
|
): string {
|
|
const { components, styles } = config;
|
|
const animationClass = styles.animations
|
|
? " fade-in slide-in-from-bottom-1 animate-in duration-150"
|
|
: "";
|
|
|
|
const reasoningSection = components.reasoning
|
|
? `
|
|
<div className="mb-3 overflow-hidden rounded-lg border border-dashed border-muted-foreground/30 bg-muted/30">
|
|
<details className="group">
|
|
<summary className="flex cursor-pointer items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:bg-muted/50">
|
|
<ChevronDownIcon className="size-4 transition-transform group-open:rotate-180" />
|
|
<span className="font-medium">Thinking...</span>
|
|
</summary>
|
|
<div className="border-t border-dashed border-muted-foreground/30 px-3 py-2 text-sm italic text-muted-foreground">
|
|
</div>
|
|
</details>
|
|
</div>`
|
|
: "";
|
|
|
|
return `
|
|
function AssistantMessage() {
|
|
return (
|
|
<MessagePrimitive.Root
|
|
className="relative mx-auto w-full max-w-[var(--thread-max-width)] ${messageSpacingClass}${animationClass}"
|
|
data-role="assistant"
|
|
>
|
|
${
|
|
components.avatar
|
|
? `<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
|
<BotIcon className="size-4" />
|
|
</div>`
|
|
: ""
|
|
}
|
|
<div className="break-words px-2 leading-relaxed text-foreground">${reasoningSection}
|
|
<MessagePrimitive.Parts
|
|
components={{
|
|
${components.markdown ? `Text: MarkdownText,` : ""}
|
|
${components.markdown ? `tools: { Fallback: ToolFallback },` : ""}
|
|
}}
|
|
/>
|
|
<MessageError />${
|
|
components.loadingIndicator !== "none"
|
|
? `
|
|
<AuiIf condition={(s) => s.thread.isRunning && s.message.content.length === 0}>
|
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
<LoaderIcon className="size-4 animate-spin" />${
|
|
components.loadingIndicator === "text"
|
|
? `
|
|
<span className="text-sm">${components.loadingText}</span>`
|
|
: ""
|
|
}
|
|
</div>
|
|
</AuiIf>`
|
|
: ""
|
|
}
|
|
</div>
|
|
|
|
<div className="mt-1 ml-2 flex min-h-6 items-center">
|
|
${components.branchPicker ? "<BranchPicker />" : ""}
|
|
<AssistantActionBar />
|
|
</div>
|
|
${
|
|
components.followUpSuggestions
|
|
? `
|
|
<AuiIf condition={(s) => !s.thread.isRunning}>
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
<ThreadPrimitive.Suggestion
|
|
prompt="Tell me more"
|
|
className="rounded-full border bg-background px-3 py-1 text-sm hover:bg-muted"
|
|
>
|
|
Tell me more
|
|
</ThreadPrimitive.Suggestion>
|
|
<ThreadPrimitive.Suggestion
|
|
prompt="Can you explain differently?"
|
|
className="rounded-full border bg-background px-3 py-1 text-sm hover:bg-muted"
|
|
>
|
|
Explain differently
|
|
</ThreadPrimitive.Suggestion>
|
|
</div>
|
|
</AuiIf>`
|
|
: ""
|
|
}
|
|
</MessagePrimitive.Root>
|
|
);
|
|
}
|
|
|
|
function MessageError() {
|
|
return (
|
|
<MessagePrimitive.Error>
|
|
<ErrorPrimitive.Root className="mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-destructive text-sm dark:bg-destructive/5 dark:text-red-200">
|
|
<ErrorPrimitive.Message className="line-clamp-2" />
|
|
</ErrorPrimitive.Root>
|
|
</MessagePrimitive.Error>
|
|
);
|
|
}`;
|
|
}
|
|
|
|
function generateActionBarComponent(config: BuilderConfig): string {
|
|
const { components } = config;
|
|
|
|
const feedbackButtons = components.actionBar.feedback
|
|
? `
|
|
<ActionBarPrimitive.FeedbackPositive asChild>
|
|
<TooltipIconButton tooltip="Good response">
|
|
<ThumbsUpIcon />
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.FeedbackPositive>
|
|
<ActionBarPrimitive.FeedbackNegative asChild>
|
|
<TooltipIconButton tooltip="Bad response">
|
|
<ThumbsDownIcon />
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.FeedbackNegative>`
|
|
: "";
|
|
|
|
return `
|
|
function AssistantActionBar() {
|
|
return (
|
|
<ActionBarPrimitive.Root
|
|
hideWhenRunning
|
|
autohide="not-last"
|
|
className="-ml-1 flex gap-1 text-muted-foreground"
|
|
>
|
|
${
|
|
components.actionBar.copy
|
|
? `<ActionBarPrimitive.Copy asChild>
|
|
<TooltipIconButton tooltip="Copy">
|
|
<AuiIf condition={(s) => s.message.isCopied}>
|
|
<CheckIcon />
|
|
</AuiIf>
|
|
<AuiIf condition={(s) => !s.message.isCopied}>
|
|
<CopyIcon />
|
|
</AuiIf>
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.Copy>`
|
|
: ""
|
|
}
|
|
<ActionBarPrimitive.ExportMarkdown asChild>
|
|
<TooltipIconButton tooltip="Export as Markdown">
|
|
<DownloadIcon />
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.ExportMarkdown>
|
|
${
|
|
components.actionBar.reload
|
|
? `<ActionBarPrimitive.Reload asChild>
|
|
<TooltipIconButton tooltip="Refresh">
|
|
<RefreshCwIcon />
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.Reload>`
|
|
: ""
|
|
}
|
|
${
|
|
components.actionBar.speak
|
|
? `<ActionBarPrimitive.Speak asChild>
|
|
<TooltipIconButton tooltip="Read aloud">
|
|
<Volume2Icon />
|
|
</TooltipIconButton>
|
|
</ActionBarPrimitive.Speak>`
|
|
: ""
|
|
}${feedbackButtons}
|
|
</ActionBarPrimitive.Root>
|
|
);
|
|
}`;
|
|
}
|
|
|
|
function generateBranchPickerComponent(): string {
|
|
return `
|
|
function BranchPicker({ className, ...rest }: { className?: string }) {
|
|
return (
|
|
<BranchPickerPrimitive.Root
|
|
hideWhenSingleBranch
|
|
className={cn("mr-2 -ml-2 inline-flex items-center text-xs text-muted-foreground", className)}
|
|
{...rest}
|
|
>
|
|
<BranchPickerPrimitive.Previous asChild>
|
|
<TooltipIconButton tooltip="Previous">
|
|
<ChevronLeftIcon />
|
|
</TooltipIconButton>
|
|
</BranchPickerPrimitive.Previous>
|
|
<span className="font-medium">
|
|
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
</span>
|
|
<BranchPickerPrimitive.Next asChild>
|
|
<TooltipIconButton tooltip="Next">
|
|
<ChevronRightIcon />
|
|
</TooltipIconButton>
|
|
</BranchPickerPrimitive.Next>
|
|
</BranchPickerPrimitive.Root>
|
|
);
|
|
}`;
|
|
}
|