IDE
An example of how to use the AI Elements to build an AI-powered IDE with file navigation, code display, terminal output, and an integrated chat assistant.
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> );}Search for validation patterns
Tutorial
Let's walk through how to build an AI-powered IDE using AI Elements. Our example will include a file tree, code block viewer, terminal output, task queue, and chat interface with streaming responses.
Setup
First, set up a new Next.js repo and cd into it by running the following command (make sure you choose to use Tailwind in the project setup):
npx create-next-app@latest ai-ide && cd ai-ideRun the following command to install AI Elements. This will also set up shadcn/ui if you haven't already configured it:
npx ai-elements@latestNow, install the required dependencies:
npm i nanoid shiki lucide-reactWe're now ready to start building our IDE!
Client
Let's build the IDE step by step. We'll create the component structure with a three-panel layout: file tree on the left, code and terminal in the center, and the AI chat on the right.
First, import the necessary AI Elements components in your app/page.tsx:
"use client"; import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import type { BundledLanguage } from "shiki"; import { Checkpoint, CheckpointIcon, CheckpointTrigger, } from "@/components/ai-elements/checkpoint"; import { CodeBlock } from "@/components/ai-elements/code-block"; import { Conversation, ConversationContent } from "@/components/ai-elements/conversation"; import { FileTree, FileTreeFile, FileTreeFolder, } from "@/components/ai-elements/file-tree"; import { Message, MessageContent, MessageResponse, } from "@/components/ai-elements/message"; import { Plan, PlanAction, PlanContent, PlanDescription, PlanHeader, PlanTitle, PlanTrigger, } from "@/components/ai-elements/plan"; import { PromptInput, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, } from "@/components/ai-elements/prompt-input"; import { Queue, QueueItem, QueueItemContent, QueueItemIndicator, QueueList, QueueSection, QueueSectionContent, QueueSectionLabel, QueueSectionTrigger, } from "@/components/ai-elements/queue"; import { Task, TaskContent, TaskItemFile, TaskTrigger, } from "@/components/ai-elements/task"; import { Terminal, TerminalContent } from "@/components/ai-elements/terminal"; import { cn } from "@/lib/utils"; import { CheckCircle2Icon, ListTodoIcon } from "lucide-react"; import { nanoid } from "nanoid"; import { useCallback, useEffect, useState } from "react"; // Types interface 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 contents const mockFiles: MockFile[] = [ { 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> ); }`, language: "tsx", name: "app.tsx", path: "src/app.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";`, language: "tsx", name: "button.tsx", path: "src/components/button.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";`, language: "tsx", name: "input.tsx", path: "src/components/input.tsx", }, { 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); }`, language: "typescript", name: "helpers.ts", path: "src/utils/helpers.ts", }, { 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" } }`, language: "json", name: "package.json", path: "package.json", }, { content: `# My App A simple React application with form validation. ## Getting Started \`\`\`bash npm install npm run dev \`\`\` ## Features - Contact form with validation - Reusable Button and Input components - TypeScript support `, language: "markdown", name: "README.md", path: "README.md", }, ]; // Mock tasks const initialTasks: TaskItem[] = [ { id: "1", status: "completed", title: "Refactor Button component" }, { id: "2", status: "in_progress", title: "Add form validation" }, { id: "3", status: "pending", title: "Write unit tests" }, ]; // Mock chat messages const mockMessages: MessageType[] = [ { content: "Can you help me add email validation to the form?", from: "user", key: nanoid(), }, { 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 function 2. Update \`validateForm\` to check email format 3. 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.`, from: "assistant", key: nanoid(), }, ]; // Mock terminal output const mockTerminalLines = [ "\u001B[32m✓\u001B[0m Building application...", "\u001B[36m src/app.tsx\u001B[0m → \u001B[33mdist/app.js\u001B[0m", "\u001B[36m src/components/button.tsx\u001B[0m → \u001B[33mdist/button.js\u001B[0m", "\u001B[36m src/components/input.tsx\u001B[0m → \u001B[33mdist/input.js\u001B[0m", "\u001B[36m src/utils/helpers.ts\u001B[0m → \u001B[33mdist/helpers.js\u001B[0m", "", "\u001B[32m✓\u001B[0m Build completed in \u001B[33m1.2s\u001B[0m", "", "\u001B[34mRunning tests...\u001B[0m", "", " \u001B[32m✓\u001B[0m validateForm › returns errors for empty fields", " \u001B[32m✓\u001B[0m validateForm › returns error for invalid email", " \u001B[32m✓\u001B[0m validateForm › passes for valid input", " \u001B[32m✓\u001B[0m Button › renders with correct variant", " \u001B[32m✓\u001B[0m Input › shows error state", "", "\u001B[32mAll tests passed!\u001B[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 => mockFiles.find((f) => f.path === path); // Handle file selection const handleFileSelect = useCallback((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 += 1) { currentContent += (i > 0 ? " " : "") + words[i]; const finalContent = currentContent; setMessages((prev) => prev.map((msg) => msg.key === messageKey ? { ...msg, content: finalContent } : msg ) ); // oxlint-disable-next-line eslint-plugin-promise(avoid-new) await new Promise((resolve) => { setTimeout(resolve, Math.random() * 40 + 20); }); } }, [] ); // Stream message const streamMessage = useCallback( async (message: MessageType) => { // Generate fresh key to avoid duplicates const newKey = nanoid(); if (message.from === "user") { setMessages((prev) => [...prev, { ...message, key: newKey }]); return; } // Add empty assistant message const newMessage = { ...message, content: "", key: newKey }; 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); // oxlint-disable-next-line eslint-plugin-promise(avoid-new) await new Promise((resolve) => { setTimeout(resolve, 100); }); } setIsTerminalStreaming(false); }, []); // Animation sequence on mount useEffect(() => { const runAnimation = async () => { // Wait a bit before starting // oxlint-disable-next-line eslint-plugin-promise(avoid-new) await new Promise((resolve) => { setTimeout(resolve, 500); }); // Stream first message (user) await streamMessage(mockMessages[0]); // oxlint-disable-next-line eslint-plugin-promise(avoid-new) await new Promise((resolve) => { setTimeout(resolve, 800); }); // Stream second message (assistant) await streamMessage(mockMessages[1]); // oxlint-disable-next-line eslint-plugin-promise(avoid-new) 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 // oxlint-disable-next-line eslint-plugin-promise(avoid-new) await new Promise((resolve) => { setTimeout(resolve, 300); }); setShowCheckpoint(true); }; runAnimation(); }, [streamMessage, streamTerminal]); const handleChatTextChange = useCallback( (e: React.ChangeEvent<HTMLTextAreaElement>) => setChatText(e.target.value), [] ); // Handle chat submit const handleSubmit = useCallback( (message: PromptInputMessage) => { if (!message.text.trim()) { return; } const userMessage: MessageType = { content: message.text, from: "user", key: nanoid(), }; setMessages((prev) => [...prev, userMessage]); setChatText(""); setStatus("submitted"); // Simulate AI response setTimeout(() => { const assistantMessage: MessageType = { content: "I'll look into that for you. Let me analyze the codebase and suggest some improvements.", from: "assistant", key: nanoid(), }; streamMessage(assistantMessage); }, 500); }, [streamMessage] ); 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={handleChatTextChange} 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;
Key Features
The IDE example demonstrates several powerful features:
- File Tree Navigation: The
FileTreecomponent displays a hierarchical file structure with expandable folders and file selection. - Code Display: The
CodeBlockcomponent renders syntax-highlighted code with line numbers and a copy button. - Terminal Output: The
Terminalcomponent shows streaming build output with ANSI color support. - Plan Component: The
Plandisplays the AI's implementation strategy with collapsible sections. - Task Queue: The
Queuecomponent organizes pending and completed tasks in separate sections. - Chat Interface: The
ConversationandMessagecomponents create a streaming chat experience. - Checkpoints: The
Checkpointcomponent allows users to mark and restore conversation states. - Streaming Support: All components support real-time streaming for a responsive user experience.
You now have a working AI-powered IDE interface! Feel free to extend it with additional features like file editing, multiple tabs, or connect it to a real AI backend using the AI SDK.