AI Elements

A component library and custom registry built on top of shadcn/ui to help you build AI-native applications faster.

Fully Composable

Every component is a building block. Combine small, focused pieces to create exactly the UI you need.

AI SDK Integration

Deep integration with the AI SDK. Streaming, status states and type safety built-in.

shadcn/ui Foundation

Built on shadcn/ui conventions. Your existing theme and setup apply automatically.

Components to build anything

Mix and match components to build chat experiences, workflows, IDEs, voice agents and more.

  • How do I set up the project?
  • What is the roadmap for Q4?
  • Update the default logo to this png.
    setup-guide.png
  • Please generate a changelog.
  • Add dark mode support.
  • Optimize database queries.
  • Set up CI/CD pipeline.
  • Write project documentation
    Complete the README and API docs
  • Implement authentication
  • Fix bug #42
    Resolve crash on settings page
  • Refactor queue logic
    Unify queue and todo state management
  • Add unit tests
    Increase test coverage for hooks
Rewrite AI Elements to SolidJS
Rewrite the AI Elements component library from React to SolidJS while maintaining compatibility with existing React-based shadcn/ui components using solid-js/compat, updating all 29 components and their test suite.

Fast, Flexible Installation

Install only what you need. The CLI adds components directly to your codebase with full source code access. No hidden dependencies, tree-shaking friendly.

$npx ai-elements@latest add conversation

Checking registry.

Installing dependencies.

Created 1 file:

- components/ai-elements/conversation.tsx

Skipped 1 files: (files might be identical, use --overwrite to overwrite)

- components/ui/button.tsx

Get started quickly

See AI Elements in action with one of our examples.

Chatbot

A full-featured chat interface with streaming, markdown and file attachments.

"use client";
import {  Conversation,  ConversationContent,  ConversationScrollButton,} from "@repo/elements/conversation";import {  Message,  MessageBranch,  MessageBranchContent,  MessageBranchNext,  MessageBranchPage,  MessageBranchPrevious,  MessageBranchSelector,  MessageContent,  MessageResponse,} from "@repo/elements/message";import {  PromptInput,  PromptInputButton,  PromptInputFooter,  type PromptInputMessage,  PromptInputTextarea,  PromptInputTools,} from "@repo/elements/prompt-input";import {  Reasoning,  ReasoningContent,  ReasoningTrigger,} from "@repo/elements/reasoning";import {  Source,  Sources,  SourcesContent,  SourcesTrigger,} from "@repo/elements/sources";import { Suggestion, Suggestions } from "@repo/elements/suggestion";import {  DropdownMenu,  DropdownMenuContent,  DropdownMenuItem,  DropdownMenuTrigger,} from "@repo/shadcn-ui/components/ui/dropdown-menu";import { cn } from "@repo/shadcn-ui/lib/utils";import type { ToolUIPart } from "ai";import {  AudioWaveformIcon,  BarChartIcon,  BoxIcon,  CameraIcon,  CodeSquareIcon,  FileIcon,  GlobeIcon,  GraduationCapIcon,  ImageIcon,  NotepadTextIcon,  PaperclipIcon,  ScreenShareIcon,} from "lucide-react";import { nanoid } from "nanoid";import { useCallback, useEffect, useState } from "react";import { toast } from "sonner";
interface MessageType {  key: string;  from: "user" | "assistant";  sources?: { href: string; title: string }[];  versions: {    id: string;    content: string;  }[];  reasoning?: {    content: string;    duration: number;  };  tools?: {    name: string;    description: string;    status: ToolUIPart["state"];    parameters: Record<string, unknown>;    result: string | undefined;    error: string | undefined;  }[];  isReasoningComplete?: boolean;  isContentComplete?: boolean;  isReasoningStreaming?: boolean;}
const mockMessages: MessageType[] = [  {    key: nanoid(),    from: "user",    versions: [      {        id: nanoid(),        content: "Can you explain how to use React hooks effectively?",      },    ],  },  {    key: nanoid(),    from: "assistant",    sources: [      {        href: "https://react.dev/reference/react",        title: "React Documentation",      },      {        href: "https://react.dev/reference/react-dom",        title: "React DOM Documentation",      },    ],    tools: [      {        name: "mcp",        description: "Searching React documentation",        status: "input-available",        parameters: {          query: "React hooks best practices",          source: "react.dev",        },        result: `{  "query": "React hooks best practices",  "results": [    {      "title": "Rules of Hooks",      "url": "https://react.dev/warnings/invalid-hook-call-warning",      "snippet": "Hooks must be called at the top level of your React function components or custom hooks. Don't call hooks inside loops, conditions, or nested functions."    },    {      "title": "useState Hook",      "url": "https://react.dev/reference/react/useState",      "snippet": "useState is a React Hook that lets you add state to your function components. It returns an array with two values: the current state and a function to update it."    },    {      "title": "useEffect Hook",      "url": "https://react.dev/reference/react/useEffect",      "snippet": "useEffect lets you synchronize a component with external systems. It runs after render and can be used to perform side effects like data fetching."    }  ]}`,        error: undefined,      },    ],    versions: [      {        id: nanoid(),        content: `# React Hooks Best Practices
React hooks are a powerful feature that let you use state and other React features without writing classes. Here are some tips for using them effectively:
## Rules of Hooks
1. **Only call hooks at the top level** of your component or custom hooks2. **Don't call hooks inside loops, conditions, or nested functions**
## Common Hooks
- **useState**: For local component state- **useEffect**: For side effects like data fetching- **useContext**: For consuming context- **useReducer**: For complex state logic- **useCallback**: For memoizing functions- **useMemo**: For memoizing values
## Example of useState and useEffect
\`\`\`jsxfunction ProfilePage({ userId }) {  const [user, setUser] = useState(null);    useEffect(() => {    // This runs after render and when userId changes    fetchUser(userId).then(userData => {      setUser(userData);    });  }, [userId]);    return user ? <Profile user={user} /> : <Loading />;}\`\`\`
Would you like me to explain any specific hook in more detail?`,      },    ],  },  {    key: nanoid(),    from: "user",    versions: [      {        id: nanoid(),        content:          "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?",      },      {        id: nanoid(),        content:          "I'm particularly interested in understanding the performance implications of useCallback and useMemo. Could you break down when each is most appropriate?",      },      {        id: nanoid(),        content:          "Thanks for the overview! Could you dive deeper into the specific use cases where useCallback and useMemo make the biggest difference in React applications?",      },    ],  },  {    key: nanoid(),    from: "assistant",    reasoning: {      content: `The user is asking for a detailed explanation of useCallback and useMemo. I should provide a clear and concise explanation of each hook's purpose and how they differ.      The useCallback hook is used to memoize functions to prevent unnecessary re-renders of child components that receive functions as props.
The useMemo hook is used to memoize values to avoid expensive recalculations on every render.
Both hooks help with performance optimization, but they serve different purposes.`,      duration: 10,    },    versions: [      {        id: nanoid(),        content: `## useCallback vs useMemo
Both hooks help with *performance optimization*, but they serve different purposes:
### useCallback
\`useCallback\` memoizes **functions** to prevent unnecessary re-renders of child components that receive functions as props.
\`\`\`jsx// Without useCallback - a new function is created on every renderconst handleClick = () => {  console.log(count);};
// With useCallback - the function is only recreated when dependencies changeconst handleClick = useCallback(() => {  console.log(count);}, [count]);\`\`\`
### useMemo
\`useMemo\` memoizes **values** to avoid expensive recalculations on every render.
\`\`\`jsx// Without useMemo - expensive calculation runs on every renderconst sortedList = expensiveSort(items);
// With useMemo - calculation only runs when items changeconst sortedList = useMemo(() => expensiveSort(items), [items]);\`\`\`
### When to use which?
- Use **useCallback** when:  - Passing callbacks to optimized child components that rely on reference equality  - Working with event handlers that you pass to child components
- Use **useMemo** when:  - You have computationally expensive calculations  - You want to avoid recreating objects that are used as dependencies for other hooks
### Performance Note
Don't overuse these hooks! They come with their own overhead. Only use them when you have identified a genuine performance issue.
### ~~Deprecated Methods~~
Note that ~~class-based lifecycle methods~~ like \`componentDidMount\` are now replaced by the \`useEffect\` hook in modern React development.`,      },    ],  },];
const suggestions = [  { icon: BarChartIcon, text: "Analyze data", color: "#76d0eb" },  { icon: BoxIcon, text: "Surprise me", color: "#76d0eb" },  { icon: NotepadTextIcon, text: "Summarize text", color: "#ea8444" },  { icon: CodeSquareIcon, text: "Code", color: "#6c71ff" },  { icon: GraduationCapIcon, text: "Get advice", color: "#76d0eb" },  { icon: null, text: "More" },];
const mockMessageResponses = [  "That's a great question! Let me help you understand this concept better. The key thing to remember is that proper implementation requires careful consideration of the underlying principles and best practices in the field.",  "I'd be happy to explain this topic in detail. From my understanding, there are several important factors to consider when approaching this problem. Let me break it down step by step for you.",  "This is an interesting topic that comes up frequently. The solution typically involves understanding the core concepts and applying them in the right context. Here's what I recommend...",  "Great choice of topic! This is something that many developers encounter. The approach I'd suggest is to start with the fundamentals and then build up to more complex scenarios.",  "That's definitely worth exploring. From what I can see, the best way to handle this is to consider both the theoretical aspects and practical implementation details.",];
const Example = () => {  const [text, setText] = useState<string>("");  const [useWebSearch, setUseWebSearch] = useState<boolean>(false);  const [useMicrophone, setUseMicrophone] = useState<boolean>(false);  const [_status, setStatus] = useState<    "submitted" | "streaming" | "ready" | "error"  >("ready");  const [messages, setMessages] = useState<MessageType[]>([]);  const [_streamingMessageId, setStreamingMessageId] = useState<string | null>(    null  );
  const streamReasoning = async (    messageKey: string,    _versionId: string,    reasoningContent: string  ) => {    const words = reasoningContent.split(" ");    let currentContent = "";
    for (let i = 0; i < words.length; i++) {      currentContent += (i > 0 ? " " : "") + words[i];
      setMessages((prev) =>        prev.map((msg) => {          if (msg.key === messageKey) {            return {              ...msg,              reasoning: msg.reasoning                ? { ...msg.reasoning, content: currentContent }                : undefined,            };          }          return msg;        })      );
      await new Promise((resolve) =>        setTimeout(resolve, Math.random() * 30 + 20)      );    }
    // Mark reasoning as complete    setMessages((prev) =>      prev.map((msg) => {        if (msg.key === messageKey) {          return {            ...msg,            isReasoningComplete: true,            isReasoningStreaming: false,          };        }        return msg;      })    );  };
  const streamContent = async (    messageKey: string,    versionId: string,    content: string  ) => {    const words = content.split(" ");    let currentContent = "";
    for (let i = 0; i < words.length; i++) {      currentContent += (i > 0 ? " " : "") + words[i];
      setMessages((prev) =>        prev.map((msg) => {          if (msg.key === messageKey) {            return {              ...msg,              versions: msg.versions.map((v) =>                v.id === versionId ? { ...v, content: currentContent } : v              ),            };          }          return msg;        })      );
      await new Promise((resolve) =>        setTimeout(resolve, Math.random() * 50 + 25)      );    }
    // Mark content as complete    setMessages((prev) =>      prev.map((msg) => {        if (msg.key === messageKey) {          return { ...msg, isContentComplete: true };        }        return msg;      })    );  };
  // biome-ignore lint/correctness/useExhaustiveDependencies: streamContent and streamReasoning only use stable setMessages  const streamMessageResponse = useCallback(    async (      messageKey: string,      versionId: string,      content: string,      reasoning?: { content: string; duration: number }    ) => {      setStatus("streaming");      setStreamingMessageId(versionId);
      // First stream the reasoning if it exists      if (reasoning) {        await streamReasoning(messageKey, versionId, reasoning.content);        await new Promise((resolve) => setTimeout(resolve, 500)); // Pause between reasoning and content      }
      // Then stream the content      await streamContent(messageKey, versionId, content);
      setStatus("ready");      setStreamingMessageId(null);    },    []  );
  const streamMessage = useCallback(    async (message: MessageType) => {      if (message.from === "user") {        setMessages((prev) => [...prev, message]);        return;      }
      // Add empty assistant message with reasoning structure      const newMessage = {        ...message,        versions: message.versions.map((v) => ({ ...v, content: "" })),        reasoning: message.reasoning          ? { ...message.reasoning, content: "" }          : undefined,        isReasoningComplete: false,        isContentComplete: false,        isReasoningStreaming: !!message.reasoning,      };
      setMessages((prev) => [...prev, newMessage]);
      // Get the first version for streaming      const firstVersion = message.versions[0];      if (!firstVersion) {        return;      }
      // Stream the response      await streamMessageResponse(        newMessage.key,        firstVersion.id,        firstVersion.content,        message.reasoning      );    },    [streamMessageResponse]  );
  const addUserMessage = useCallback(    (content: string) => {      const userMessage: MessageType = {        key: `user-${Date.now()}`,        from: "user",        versions: [          {            id: `user-${Date.now()}`,            content,          },        ],      };
      setMessages((prev) => [...prev, userMessage]);
      setTimeout(() => {        const assistantMessageKey = `assistant-${Date.now()}`;        const assistantMessageId = `version-${Date.now()}`;        const randomMessageResponse =          mockMessageResponses[            Math.floor(Math.random() * mockMessageResponses.length)          ];
        // Create reasoning for some responses        const shouldHaveReasoning = Math.random() > 0.5;        const reasoning = shouldHaveReasoning          ? {              content:                "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.",              duration: 3,            }          : undefined;
        const assistantMessage: MessageType = {          key: assistantMessageKey,          from: "assistant",          versions: [            {              id: assistantMessageId,              content: "",            },          ],          reasoning: reasoning ? { ...reasoning, content: "" } : undefined,          isReasoningComplete: false,          isContentComplete: false,          isReasoningStreaming: !!reasoning,        };
        setMessages((prev) => [...prev, assistantMessage]);        streamMessageResponse(          assistantMessageKey,          assistantMessageId,          randomMessageResponse,          reasoning        );      }, 500);    },    [streamMessageResponse]  );
  useEffect(() => {    // Reset state on mount to ensure fresh component    setMessages([]);
    const processMessages = async () => {      for (let i = 0; i < mockMessages.length; i++) {        await streamMessage(mockMessages[i]);
        if (i < mockMessages.length - 1) {          await new Promise((resolve) => setTimeout(resolve, 1000));        }      }    };
    // Small delay to ensure state is reset before starting    const timer = setTimeout(() => {      processMessages();    }, 100);
    // Cleanup function to cancel any ongoing operations    return () => {      clearTimeout(timer);      setMessages([]);    };  }, [streamMessage]);
  const handleSubmit = (message: PromptInputMessage) => {    const hasText = Boolean(message.text);    const hasAttachments = Boolean(message.files?.length);
    if (!(hasText || hasAttachments)) {      return;    }
    setStatus("submitted");    addUserMessage(message.text || "Sent with attachments");    setText("");  };
  const handleFileAction = (action: string) => {    toast.success("File action", {      description: action,    });  };
  const handleSuggestionClick = (suggestion: string) => {    setStatus("submitted");    addUserMessage(suggestion);  };
  return (    <div className="relative flex size-full flex-col divide-y overflow-hidden">      <Conversation>        <ConversationContent>          {messages.map(({ versions, ...message }) => (            <MessageBranch defaultBranch={0} key={message.key}>              <MessageBranchContent>                {versions.map((version) => (                  <Message                    from={message.from}                    key={`${message.key}-${version.id}`}                  >                    <div>                      {message.sources?.length && (                        <Sources>                          <SourcesTrigger count={message.sources.length} />                          <SourcesContent>                            {message.sources.map((source) => (                              <Source                                href={source.href}                                key={source.href}                                title={source.title}                              />                            ))}                          </SourcesContent>                        </Sources>                      )}                      {message.reasoning && (                        <Reasoning                          duration={message.reasoning.duration}                          isStreaming={message.isReasoningStreaming}                        >                          <ReasoningTrigger />                          <ReasoningContent>                            {message.reasoning.content}                          </ReasoningContent>                        </Reasoning>                      )}                      {(message.from === "user" ||                        message.isReasoningComplete ||                        !message.reasoning) && (                        <MessageContent                          className={cn(                            "group-[.is-user]:rounded-[24px] group-[.is-user]:bg-secondary group-[.is-user]:text-foreground",                            "group-[.is-assistant]:bg-transparent group-[.is-assistant]:p-0 group-[.is-assistant]:text-foreground"                          )}                        >                          <MessageResponse>{version.content}</MessageResponse>                        </MessageContent>                      )}                    </div>                  </Message>                ))}              </MessageBranchContent>              {versions.length > 1 && (                <MessageBranchSelector className="px-0" from={message.from}>                  <MessageBranchPrevious />                  <MessageBranchPage />                  <MessageBranchNext />                </MessageBranchSelector>              )}            </MessageBranch>          ))}        </ConversationContent>        <ConversationScrollButton />      </Conversation>      <div className="grid shrink-0 gap-4 p-4">        <PromptInput          className="divide-y-0 rounded-[28px]"          onSubmit={handleSubmit}        >          <PromptInputTextarea            className="px-5 md:text-base"            onChange={(event) => setText(event.target.value)}            placeholder="Ask anything"            value={text}          />          <PromptInputFooter className="p-2.5">            <PromptInputTools>              <DropdownMenu>                <DropdownMenuTrigger asChild>                  <PromptInputButton                    className="!rounded-full border font-medium"                    variant="outline"                  >                    <PaperclipIcon size={16} />                    <span>Attach</span>                  </PromptInputButton>                </DropdownMenuTrigger>                <DropdownMenuContent align="start">                  <DropdownMenuItem                    onClick={() => handleFileAction("upload-file")}                  >                    <FileIcon className="mr-2" size={16} />                    Upload file                  </DropdownMenuItem>                  <DropdownMenuItem                    onClick={() => handleFileAction("upload-photo")}                  >                    <ImageIcon className="mr-2" size={16} />                    Upload photo                  </DropdownMenuItem>                  <DropdownMenuItem                    onClick={() => handleFileAction("take-screenshot")}                  >                    <ScreenShareIcon className="mr-2" size={16} />                    Take screenshot                  </DropdownMenuItem>                  <DropdownMenuItem                    onClick={() => handleFileAction("take-photo")}                  >                    <CameraIcon className="mr-2" size={16} />                    Take photo                  </DropdownMenuItem>                </DropdownMenuContent>              </DropdownMenu>              <PromptInputButton                className="rounded-full border font-medium"                onClick={() => setUseWebSearch(!useWebSearch)}                variant="outline"              >                <GlobeIcon size={16} />                <span>Search</span>              </PromptInputButton>            </PromptInputTools>            <PromptInputButton              className="rounded-full font-medium text-foreground"              onClick={() => setUseMicrophone(!useMicrophone)}              variant="secondary"            >              <AudioWaveformIcon size={16} />              <span>Voice</span>            </PromptInputButton>          </PromptInputFooter>        </PromptInput>        <Suggestions className="px-4">          {suggestions.map(({ icon: Icon, text, color }) => (            <Suggestion              className="font-normal text-foreground"              key={text}              onClick={() => handleSuggestionClick(text)}              suggestion={text}            >              {Icon && <Icon size={16} style={{ color }} />}              {text}            </Suggestion>          ))}        </Suggestions>      </div>    </div>  );};
export default Example;

IDE

IDE-style interface with syntax highlighting, terminal output and file trees.

"use client";
import {  Checkpoint,  CheckpointIcon,  CheckpointTrigger,} from "@repo/elements/checkpoint";import { CodeBlock } from "@repo/elements/code-block";import { Conversation, ConversationContent } from "@repo/elements/conversation";import {  FileTree,  FileTreeFile,  FileTreeFolder,} from "@repo/elements/file-tree";import {  Message,  MessageContent,  MessageResponse,} from "@repo/elements/message";import {  Plan,  PlanAction,  PlanContent,  PlanDescription,  PlanHeader,  PlanTitle,  PlanTrigger,} from "@repo/elements/plan";import {  PromptInput,  PromptInputFooter,  type PromptInputMessage,  PromptInputSubmit,  PromptInputTextarea,} from "@repo/elements/prompt-input";import {  Queue,  QueueItem,  QueueItemContent,  QueueItemIndicator,  QueueList,  QueueSection,  QueueSectionContent,  QueueSectionLabel,  QueueSectionTrigger,} from "@repo/elements/queue";import {  Task,  TaskContent,  TaskItemFile,  TaskTrigger,} from "@repo/elements/task";import { Terminal, TerminalContent } from "@repo/elements/terminal";import { cn } from "@repo/shadcn-ui/lib/utils";import { CheckCircle2Icon, ListTodoIcon } from "lucide-react";import { nanoid } from "nanoid";import { useCallback, useEffect, useState } from "react";import type { BundledLanguage } from "shiki";
// Typesinterface MockFile {  path: string;  name: string;  language: BundledLanguage;  content: string;}
interface MessageType {  key: string;  from: "user" | "assistant";  content: string;}
interface TaskItem {  id: string;  title: string;  status: "pending" | "in_progress" | "completed";}
// Mock file contentsconst mockFiles: MockFile[] = [  {    path: "src/app.tsx",    name: "app.tsx",    language: "tsx",    content: `import { useState } from "react";import { Button } from "./components/button";import { Input } from "./components/input";import { validateForm } from "./utils/helpers";
export function App() {  const [name, setName] = useState("");  const [email, setEmail] = useState("");  const [errors, setErrors] = useState<string[]>([]);
  const handleSubmit = () => {    const validation = validateForm({ name, email });    if (!validation.isValid) {      setErrors(validation.errors);      return;    }    console.log("Form submitted:", { name, email });  };
  return (    <div className="container mx-auto p-4">      <h1 className="text-2xl font-bold mb-4">Contact Form</h1>      <Input        placeholder="Name"        value={name}        onChange={(e) => setName(e.target.value)}      />      <Input        placeholder="Email"        value={email}        onChange={(e) => setEmail(e.target.value)}      />      {errors.map((error) => (        <p key={error} className="text-red-500">{error}</p>      ))}      <Button onClick={handleSubmit}>Submit</Button>    </div>  );}`,  },  {    path: "src/components/button.tsx",    name: "button.tsx",    language: "tsx",    content: `import { forwardRef, type ButtonHTMLAttributes } from "react";import { cn } from "../utils/helpers";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {  variant?: "primary" | "secondary" | "ghost";  size?: "sm" | "md" | "lg";}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(  ({ className, variant = "primary", size = "md", ...props }, ref) => {    return (      <button        ref={ref}        className={cn(          "inline-flex items-center justify-center rounded-md font-medium",          "transition-colors focus-visible:outline-none focus-visible:ring-2",          variant === "primary" && "bg-blue-500 text-white hover:bg-blue-600",          variant === "secondary" && "bg-gray-200 text-gray-900 hover:bg-gray-300",          variant === "ghost" && "hover:bg-gray-100",          size === "sm" && "h-8 px-3 text-sm",          size === "md" && "h-10 px-4",          size === "lg" && "h-12 px-6 text-lg",          className        )}        {...props}      />    );  });
Button.displayName = "Button";`,  },  {    path: "src/components/input.tsx",    name: "input.tsx",    language: "tsx",    content: `import { forwardRef, type InputHTMLAttributes } from "react";import { cn } from "../utils/helpers";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {  error?: boolean;}
export const Input = forwardRef<HTMLInputElement, InputProps>(  ({ className, error, ...props }, ref) => {    return (      <input        ref={ref}        className={cn(          "flex h-10 w-full rounded-md border px-3 py-2 text-sm",          "focus-visible:outline-none focus-visible:ring-2",          error ? "border-red-500" : "border-gray-300",          className        )}        {...props}      />    );  });
Input.displayName = "Input";`,  },  {    path: "src/utils/helpers.ts",    name: "helpers.ts",    language: "typescript",    content: `export function cn(...classes: (string | boolean | undefined)[]) {  return classes.filter(Boolean).join(" ");}
interface FormData {  name: string;  email: string;}
interface ValidationResult {  isValid: boolean;  errors: string[];}
export function validateForm(data: FormData): ValidationResult {  const errors: string[] = [];
  if (!data.name.trim()) {    errors.push("Name is required");  }
  if (!data.email.trim()) {    errors.push("Email is required");  } else if (!isValidEmail(data.email)) {    errors.push("Invalid email format");  }
  return {    isValid: errors.length === 0,    errors,  };}
function isValidEmail(email: string): boolean {  return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);}`,  },  {    path: "package.json",    name: "package.json",    language: "json",    content: `{  "name": "my-app",  "version": "1.0.0",  "private": true,  "scripts": {    "dev": "vite",    "build": "tsc && vite build",    "test": "vitest",    "lint": "eslint src"  },  "dependencies": {    "react": "^18.2.0",    "react-dom": "^18.2.0"  },  "devDependencies": {    "@types/react": "^18.2.0",    "typescript": "^5.0.0",    "vite": "^5.0.0",    "vitest": "^1.0.0"  }}`,  },  {    path: "README.md",    name: "README.md",    language: "markdown",    content: `# My App
A simple React application with form validation.
## Getting Started
\`\`\`bashnpm installnpm run dev\`\`\`
## Features
- Contact form with validation- Reusable Button and Input components- TypeScript support`,  },];
// Mock tasksconst initialTasks: TaskItem[] = [  { id: "1", title: "Refactor Button component", status: "completed" },  { id: "2", title: "Add form validation", status: "in_progress" },  { id: "3", title: "Write unit tests", status: "pending" },];
// Mock chat messagesconst mockMessages: MessageType[] = [  {    key: nanoid(),    from: "user",    content: "Can you help me add email validation to the form?",  },  {    key: nanoid(),    from: "assistant",    content: `I can help you add email validation. Looking at your code in \`src/utils/helpers.ts\`, I see you already have a \`validateForm\` function.
Here's what I'll do:
1. Add an \`isValidEmail\` helper function2. Update \`validateForm\` to check email format3. Show validation errors in the UI
The email validation uses a regex pattern to check for valid email format. The form will now show "Invalid email format" if the user enters an incorrectly formatted email address.`,  },];
// Mock terminal outputconst mockTerminalLines = [  "\x1b[32m✓\x1b[0m Building application...",  "\x1b[36m  src/app.tsx\x1b[0m → \x1b[33mdist/app.js\x1b[0m",  "\x1b[36m  src/components/button.tsx\x1b[0m → \x1b[33mdist/button.js\x1b[0m",  "\x1b[36m  src/components/input.tsx\x1b[0m → \x1b[33mdist/input.js\x1b[0m",  "\x1b[36m  src/utils/helpers.ts\x1b[0m → \x1b[33mdist/helpers.js\x1b[0m",  "",  "\x1b[32m✓\x1b[0m Build completed in \x1b[33m1.2s\x1b[0m",  "",  "\x1b[34mRunning tests...\x1b[0m",  "",  " \x1b[32m✓\x1b[0m validateForm › returns errors for empty fields",  " \x1b[32m✓\x1b[0m validateForm › returns error for invalid email",  " \x1b[32m✓\x1b[0m validateForm › passes for valid input",  " \x1b[32m✓\x1b[0m Button › renders with correct variant",  " \x1b[32m✓\x1b[0m Input › shows error state",  "",  "\x1b[32mAll tests passed!\x1b[0m (5/5)",];
const Example = () => {  // File tree state  const [selectedPath, setSelectedPath] = useState<string>("src/app.tsx");  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(    new Set(["src", "src/components", "src/utils"])  );
  // Code editor state  const [currentFile, setCurrentFile] = useState<MockFile>(mockFiles[0]);
  // Terminal state  const [terminalOutput, setTerminalOutput] = useState<string>("");  const [isTerminalStreaming, setIsTerminalStreaming] =    useState<boolean>(false);
  // Chat state  const [messages, setMessages] = useState<MessageType[]>([]);  const [chatText, setChatText] = useState<string>("");  const [status, setStatus] = useState<"ready" | "streaming" | "submitted">(    "ready"  );
  // Tasks state  const [tasks, setTasks] = useState<TaskItem[]>(initialTasks);
  // Checkpoint state  const [showCheckpoint, setShowCheckpoint] = useState<boolean>(false);
  // Find file by path  const findFileByPath = (path: string): MockFile | undefined => {    return mockFiles.find((f) => f.path === path);  };
  // Handle file selection  const handleFileSelect = (path: string) => {    setSelectedPath(path);    const file = findFileByPath(path);    if (file) {      setCurrentFile(file);    }  };
  // Stream message content word by word  const streamContent = useCallback(    async (messageKey: string, content: string) => {      const words = content.split(" ");      let currentContent = "";
      for (let i = 0; i < words.length; i++) {        currentContent += (i > 0 ? " " : "") + words[i];        const finalContent = currentContent;
        setMessages((prev) =>          prev.map((msg) =>            msg.key === messageKey ? { ...msg, content: finalContent } : msg          )        );
        await new Promise((resolve) =>          setTimeout(resolve, Math.random() * 40 + 20)        );      }    },    []  );
  // Stream message  const streamMessage = useCallback(    async (message: MessageType) => {      const newKey = nanoid(); // Generate fresh key to avoid duplicates      if (message.from === "user") {        setMessages((prev) => [...prev, { ...message, key: newKey }]);        return;      }
      // Add empty assistant message      const newMessage = { ...message, key: newKey, content: "" };      setMessages((prev) => [...prev, newMessage]);
      setStatus("streaming");      await streamContent(message.key, message.content);      setStatus("ready");    },    [streamContent]  );
  // Stream terminal output line by line  const streamTerminal = useCallback(async () => {    setIsTerminalStreaming(true);    let output = "";
    for (const line of mockTerminalLines) {      output += `${line}\n`;      setTerminalOutput(output);      await new Promise((resolve) => setTimeout(resolve, 100));    }
    setIsTerminalStreaming(false);  }, []);
  // Animation sequence on mount  useEffect(() => {    const runAnimation = async () => {      // Wait a bit before starting      await new Promise((resolve) => setTimeout(resolve, 500));
      // Stream first message (user)      await streamMessage(mockMessages[0]);      await new Promise((resolve) => setTimeout(resolve, 800));
      // Stream second message (assistant)      await streamMessage(mockMessages[1]);      await new Promise((resolve) => setTimeout(resolve, 500));
      // Update task status      setTasks((prev) =>        prev.map((task) =>          task.id === "2" ? { ...task, status: "completed" as const } : task        )      );
      // Stream terminal output      await streamTerminal();
      // Show checkpoint      await new Promise((resolve) => setTimeout(resolve, 300));      setShowCheckpoint(true);    };
    runAnimation();  }, [streamMessage, streamTerminal]);
  // Handle chat submit  const handleSubmit = (message: PromptInputMessage) => {    if (!message.text.trim()) {      return;    }
    const userMessage: MessageType = {      key: nanoid(),      from: "user",      content: message.text,    };
    setMessages((prev) => [...prev, userMessage]);    setChatText("");    setStatus("submitted");
    // Simulate AI response    setTimeout(() => {      const assistantMessage: MessageType = {        key: nanoid(),        from: "assistant",        content:          "I'll look into that for you. Let me analyze the codebase and suggest some improvements.",      };      streamMessage(assistantMessage);    }, 500);  };
  const completedTasks = tasks.filter((t) => t.status === "completed");  const pendingTasks = tasks.filter((t) => t.status !== "completed");
  return (    <div className="flex h-full w-full bg-background">      {/* Left Sidebar - File Tree */}      <div className="flex w-64 flex-col border-r">        <div className="flex-1 overflow-auto p-1">          <FileTree            className="border-none"            expanded={expandedPaths}            onExpandedChange={setExpandedPaths}            onSelect={handleFileSelect}            selectedPath={selectedPath}          >            <FileTreeFolder name="src" path="src">              <FileTreeFolder name="components" path="src/components">                <FileTreeFile                  name="button.tsx"                  path="src/components/button.tsx"                />                <FileTreeFile                  name="input.tsx"                  path="src/components/input.tsx"                />              </FileTreeFolder>              <FileTreeFolder name="utils" path="src/utils">                <FileTreeFile name="helpers.ts" path="src/utils/helpers.ts" />              </FileTreeFolder>              <FileTreeFile name="app.tsx" path="src/app.tsx" />            </FileTreeFolder>            <FileTreeFile name="package.json" path="package.json" />            <FileTreeFile name="README.md" path="README.md" />          </FileTree>        </div>      </div>
      {/* Center Panel - Code + Terminal */}      <div className="flex flex-1 flex-col overflow-hidden">        {/* Code Block */}        <CodeBlock          className="rounded-none border-0"          code={currentFile.content}          language={currentFile.language}          showLineNumbers        />
        <Terminal          className="h-64 rounded-none border-0"          isStreaming={isTerminalStreaming}          output={terminalOutput}        >          <TerminalContent className="max-h-full" />        </Terminal>      </div>
      {/* Right Sidebar - AI Chat */}      <div className="flex w-80 flex-col border-l">        {/* Plan Section */}        <div className="border-b p-3">          <Plan defaultOpen>            <PlanHeader>              <div>                <PlanTitle>Implementation Plan</PlanTitle>                <PlanDescription>Adding form validation</PlanDescription>              </div>              <PlanAction>                <PlanTrigger />              </PlanAction>            </PlanHeader>            <PlanContent className="pt-0">              <Task defaultOpen>                <TaskTrigger title="Search for validation patterns" />                <TaskContent>                  <TaskItemFile>src/utils/helpers.ts</TaskItemFile>                  <TaskItemFile>src/app.tsx</TaskItemFile>                </TaskContent>              </Task>            </PlanContent>          </Plan>        </div>
        {/* Task Queue */}        <div className="border-b p-3">          <Queue>            <QueueSection defaultOpen>              <QueueSectionTrigger>                <QueueSectionLabel                  count={pendingTasks.length}                  icon={<ListTodoIcon className="size-4" />}                  label="Pending"                />              </QueueSectionTrigger>              <QueueSectionContent>                <QueueList>                  {pendingTasks.map((task) => (                    <QueueItem key={task.id}>                      <div className="flex items-center gap-2">                        <QueueItemIndicator />                        <QueueItemContent>{task.title}</QueueItemContent>                      </div>                    </QueueItem>                  ))}                </QueueList>              </QueueSectionContent>            </QueueSection>            <QueueSection defaultOpen={false}>              <QueueSectionTrigger>                <QueueSectionLabel                  count={completedTasks.length}                  icon={<CheckCircle2Icon className="size-4" />}                  label="Completed"                />              </QueueSectionTrigger>              <QueueSectionContent>                <QueueList>                  {completedTasks.map((task) => (                    <QueueItem key={task.id}>                      <div className="flex items-center gap-2">                        <QueueItemIndicator completed />                        <QueueItemContent completed>                          {task.title}                        </QueueItemContent>                      </div>                    </QueueItem>                  ))}                </QueueList>              </QueueSectionContent>            </QueueSection>          </Queue>        </div>
        {/* Chat Messages */}        <div className="flex flex-1 flex-col overflow-hidden">          <Conversation className="flex-1">            <ConversationContent className="gap-4 p-3">              {messages.map((message) => (                <Message from={message.from} key={message.key}>                  <MessageContent                    className={cn(                      message.from === "user"                        ? "rounded-lg bg-secondary px-3 py-2"                        : ""                    )}                  >                    {message.from === "assistant" ? (                      <MessageResponse>{message.content}</MessageResponse>                    ) : (                      message.content                    )}                  </MessageContent>                </Message>              ))}              {showCheckpoint && (                <Checkpoint>                  <CheckpointIcon />                  <CheckpointTrigger tooltip="Restore to this checkpoint">                    Checkpoint saved                  </CheckpointTrigger>                </Checkpoint>              )}            </ConversationContent>          </Conversation>
          {/* Chat Input */}          <div className="border-t p-3">            <PromptInput className="rounded-lg border" onSubmit={handleSubmit}>              <PromptInputTextarea                className="min-h-10"                onChange={(e) => setChatText(e.target.value)}                placeholder="Ask about the code..."                value={chatText}              />              <PromptInputFooter className="justify-end p-2">                <PromptInputSubmit                  disabled={status !== "ready" || !chatText.trim()}                  status={status === "streaming" ? "streaming" : undefined}                />              </PromptInputFooter>            </PromptInput>          </div>        </div>      </div>    </div>  );};
export default Example;

Workflow

Visualize and interact with your AI workflows.

"use client";
import { Canvas } from "@repo/elements/canvas";import { Connection } from "@repo/elements/connection";import { Edge } from "@repo/elements/edge";import {  Node,  NodeContent,  NodeDescription,  NodeFooter,  NodeHeader,  NodeTitle,} from "@repo/elements/node";
const nodeIds = {  start: "start",  process1: "process1",  process2: "process2",  decision: "decision",  output1: "output1",  output2: "output2",};
const nodes = [  {    id: nodeIds.start,    type: "workflow",    position: { x: 0, y: 0 },    data: {      label: "Start",      description: "Initialize workflow",      handles: { target: false, source: true },      content: "Triggered by user action at 09:30 AM",      footer: "Status: Ready",    },  },  {    id: nodeIds.process1,    type: "workflow",    position: { x: 500, y: 0 },    data: {      label: "Process Data",      description: "Transform input",      handles: { target: true, source: true },      content: "Validating 1,234 records and applying business rules",      footer: "Duration: ~2.5s",    },  },  {    id: nodeIds.decision,    type: "workflow",    position: { x: 1000, y: 0 },    data: {      label: "Decision Point",      description: "Route based on conditions",      handles: { target: true, source: true },      content: "Evaluating: data.status === 'valid' && data.score > 0.8",      footer: "Confidence: 94%",    },  },  {    id: nodeIds.output1,    type: "workflow",    position: { x: 1500, y: -300 },    data: {      label: "Success Path",      description: "Handle success case",      handles: { target: true, source: true },      content: "1,156 records passed validation (93.7%)",      footer: "Next: Send to production",    },  },  {    id: nodeIds.output2,    type: "workflow",    position: { x: 1500, y: 300 },    data: {      label: "Error Path",      description: "Handle error case",      handles: { target: true, source: true },      content: "78 records failed validation (6.3%)",      footer: "Next: Queue for review",    },  },  {    id: nodeIds.process2,    type: "workflow",    position: { x: 2000, y: 0 },    data: {      label: "Complete",      description: "Finalize workflow",      handles: { target: true, source: false },      content: "All records processed and routed successfully",      footer: "Total time: 4.2s",    },  },];
const edges = [  {    id: "edge1",    source: nodeIds.start,    target: nodeIds.process1,    type: "animated",  },  {    id: "edge2",    source: nodeIds.process1,    target: nodeIds.decision,    type: "animated",  },  {    id: "edge3",    source: nodeIds.decision,    target: nodeIds.output1,    type: "animated",  },  {    id: "edge4",    source: nodeIds.decision,    target: nodeIds.output2,    type: "temporary",  },  {    id: "edge5",    source: nodeIds.output1,    target: nodeIds.process2,    type: "animated",  },  {    id: "edge6",    source: nodeIds.output2,    target: nodeIds.process2,    type: "temporary",  },];
const nodeTypes = {  workflow: ({    data,  }: {    data: {      label: string;      description: string;      handles: { target: boolean; source: boolean };      content: string;      footer: string;    };  }) => (    <Node handles={data.handles}>      <NodeHeader>        <NodeTitle>{data.label}</NodeTitle>        <NodeDescription>{data.description}</NodeDescription>      </NodeHeader>      <NodeContent>        <p className="text-sm">{data.content}</p>      </NodeContent>      <NodeFooter>        <p className="text-muted-foreground text-xs">{data.footer}</p>      </NodeFooter>    </Node>  ),};
const edgeTypes = {  animated: Edge.Animated,  temporary: Edge.Temporary,};
const Example = () => (  <div style={{ height: "100%", width: "100%" }}>    <Canvas      connectionLineComponent={Connection}      edges={edges}      edgeTypes={edgeTypes}      fitView      nodes={nodes}      nodeTypes={nodeTypes}    />  </div>);
export default Example;

Start building AI interfaces today

Get started