Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

273 lines
9.4 KiB
TypeScript

"use client";
import { useState, type ReactElement, isValidElement } from "react";
import { CheckIcon, CopyIcon } from "lucide-react";
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react";
import type { BuilderConfig } from "./types";
import { configMatchesPreset } from "./presets";
import { encodeConfig } from "@/lib/playground-url-state";
import { BASE_URL } from "@/lib/constants";
import { analytics } from "@/lib/analytics";
interface CreateDialogProps {
config: BuilderConfig;
children: React.ReactNode;
container?: React.RefObject<HTMLElement | null>;
onOpenCodeView?: () => void;
}
export function CreateDialog({
config,
children,
container,
onOpenCodeView,
}: CreateDialogProps) {
const [open, setOpen] = useState(false);
const commands = generateCliCommands(config);
const handleOpenChange = (isOpen: boolean) => {
if (isOpen) {
analytics.builder.createDialogOpened();
}
setOpen(isOpen);
};
const handleOpenCodeView = () => {
setOpen(false);
onOpenCodeView?.();
};
return (
<DialogPrimitive.Root open={open} onOpenChange={handleOpenChange}>
{isValidElement(children) ? (
<DialogPrimitive.Trigger render={children as ReactElement} />
) : (
<DialogPrimitive.Trigger>{children}</DialogPrimitive.Trigger>
)}
<DialogPrimitive.Portal container={container?.current}>
<DialogPrimitive.Backdrop className="data-closed:fade-out-0 data-open:fade-in-0 data-closed:animate-out data-open:animate-in absolute inset-0 z-50 bg-black/50" />
<DialogPrimitive.Popup className="data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background data-closed:animate-out data-open:animate-in absolute top-1/2 left-1/2 z-50 grid max-h-[85vh] w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 overflow-hidden rounded-lg border p-6 shadow-lg duration-200">
<DialogPrimitive.Title className="text-lg leading-none font-semibold">
Create your assistant
</DialogPrimitive.Title>
<div className="space-y-4 overflow-y-auto text-sm">
<CommandBlock
label={commands.primary.label}
{...(commands.primary.description && {
description: commands.primary.description,
})}
{...(commands.primary.command && {
command: commands.primary.command,
})}
commandType="create"
/>
<CommandBlock
label={commands.alternative.label}
{...(commands.alternative.command && {
command: commands.alternative.command,
})}
commandType="shadcn"
/>
<div className="border-t pt-4">
<p className="text-muted-foreground mb-3">Or set up manually:</p>
<div className="space-y-3">
{commands.manual.slice(0, 2).map((cmd, index) => (
<CommandBlock
key={index}
label={`${index + 1}. ${cmd.label}`}
{...(cmd.command && { command: cmd.command })}
{...(cmd.description && { description: cmd.description })}
commandType={index === 0 ? "manual_init" : "manual_add"}
/>
))}
<div>
<div className="text-foreground mb-1 font-medium">
3. Copy code
</div>
<p className="text-muted-foreground">
Copy the code from the{" "}
<button
type="button"
onClick={handleOpenCodeView}
className="text-foreground hover:text-foreground/80 underline underline-offset-2"
>
Code view
</button>{" "}
into your thread.tsx
</p>
</div>
</div>
</div>
<div className="border-t pt-4">
<p className="text-foreground mb-2 font-medium">Configuration</p>
<div className="text-muted-foreground space-y-1">
{commands.summary.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
</div>
</div>
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Popup>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
}
function CommandBlock({
label,
description,
command,
commandType,
}: {
label: string;
description?: string;
command?: string;
commandType?: "create" | "shadcn" | "manual_init" | "manual_add";
}) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
if (!command) return;
if (commandType) {
analytics.builder.commandCopied(commandType);
}
await navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div>
<div className="text-foreground mb-1 font-medium">{label}</div>
{description && (
<p className="text-muted-foreground mb-1.5">{description}</p>
)}
{command && (
<div className="group relative">
<pre className="bg-muted scrollbar-none overflow-x-auto rounded-md p-2 font-mono text-xs">
{command}
</pre>
<div className="bg-muted pointer-events-none absolute inset-y-0 right-0 flex items-center rounded-r-md px-1">
<div className="to-muted pointer-events-none absolute inset-y-0 -left-3 w-3 bg-gradient-to-r from-transparent" />
<button
type="button"
onClick={handleCopy}
className="text-muted-foreground hover:bg-foreground/10 hover:text-foreground pointer-events-auto rounded p-1 opacity-0 transition-opacity group-hover:opacity-100"
title="Copy command"
>
{copied ? (
<CheckIcon className="size-3.5" />
) : (
<CopyIcon className="size-3.5" />
)}
</button>
</div>
</div>
)}
</div>
);
}
interface CliCommand {
label: string;
description?: string;
command?: string;
}
interface CliCommands {
primary: CliCommand;
alternative: CliCommand;
manual: CliCommand[];
summary: string[];
}
function generateCliCommands(config: BuilderConfig): CliCommands {
const { components } = config;
const matchingPreset = configMatchesPreset(config);
const playgroundInitUrl = `${BASE_URL}/playground/init`;
const presetUrl = matchingPreset
? `${playgroundInitUrl}?preset=${matchingPreset.id}`
: `${playgroundInitUrl}?c=${encodeConfig(config)}`;
const componentsToAdd: string[] = ["thread"];
if (components.markdown) {
componentsToAdd.push("markdown-text", "tool-fallback");
}
componentsToAdd.push("tooltip-icon-button");
if (components.attachments) {
componentsToAdd.push("attachment");
}
if (components.reasoning) {
componentsToAdd.push("reasoning");
}
if (components.sources) {
componentsToAdd.push("sources");
}
const addCommand = `npx assistant-ui@latest add ${componentsToAdd.join(" ")}`;
const enabledFeatures: string[] = [];
if (components.markdown) enabledFeatures.push("Markdown");
if (components.attachments) enabledFeatures.push("Attachments");
if (components.branchPicker) enabledFeatures.push("Branch Picker");
if (components.editMessage) enabledFeatures.push("Edit Message");
if (components.threadWelcome) enabledFeatures.push("Welcome Screen");
if (components.suggestions) enabledFeatures.push("Suggestions");
if (components.scrollToBottom) enabledFeatures.push("Scroll to Bottom");
if (components.reasoning) enabledFeatures.push("Reasoning");
if (components.sources) enabledFeatures.push("Sources");
if (components.followUpSuggestions) enabledFeatures.push("Follow-ups");
if (components.avatar) enabledFeatures.push("Avatar");
if (components.actionBar.copy) enabledFeatures.push("Copy");
if (components.actionBar.reload) enabledFeatures.push("Reload");
if (components.actionBar.speak) enabledFeatures.push("Speak");
if (components.actionBar.feedback) enabledFeatures.push("Feedback");
const summary: string[] = [
`Style: ${config.styles.borderRadius} radius, ${config.styles.fontFamily}`,
`Enabled: ${enabledFeatures.length > 0 ? enabledFeatures.join(", ") : "None"}`,
];
return {
primary: {
label: "One-command setup",
description: "Install with your current configuration",
command: `npx assistant-ui@latest create my-app --preset "${presetUrl}"`,
},
alternative: {
label: "Using shadcn",
command: `npx shadcn@latest add "${presetUrl}"`,
},
manual: [
{
label: "Initialize",
command: "npx assistant-ui@latest init",
},
{
label: "Add components",
command: addCommand,
},
{
label: "Copy code",
description: "Copy the code from the Code view into your thread.tsx",
},
],
summary,
};
}