├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── app ├── (auth) │ ├── auth │ │ ├── callback │ │ │ └── route.ts │ │ ├── error │ │ │ └── page.tsx │ │ └── page.tsx │ └── layout.tsx ├── (chat) │ ├── c │ │ └── [id] │ │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── api │ └── chat │ │ ├── actions.ts │ │ ├── prompt.md │ │ ├── route.ts │ │ ├── schema.ts │ │ └── tools │ │ ├── code-generator.ts │ │ ├── generate-files-description.md │ │ └── generate-files-prompt.md ├── favicon.ico ├── globals.css └── layout.tsx ├── components.json ├── components ├── chat-input │ ├── chat-input.tsx │ └── model-selector.tsx ├── chat │ ├── app-preview.tsx │ ├── artifact │ │ ├── artifact-actions.tsx │ │ ├── artifact-header.tsx │ │ ├── code-editor.tsx │ │ ├── file-node.tsx │ │ ├── file-tree.tsx │ │ ├── folder-node.tsx │ │ ├── tree-node.tsx │ │ └── vertical-line.tsx │ ├── chat-view.tsx │ ├── code-preview-tabs.tsx │ └── code-view.tsx ├── header.tsx ├── login-form.tsx ├── messages │ ├── ai-message.tsx │ ├── regenerate-action.tsx │ └── user-message.tsx ├── providers │ ├── providers.tsx │ └── theme-provider.tsx ├── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── chart.tsx │ ├── chat-container.tsx │ ├── checkbox.tsx │ ├── code-block.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input-otp.tsx │ ├── input.tsx │ ├── label.tsx │ ├── loader.tsx │ ├── logo.tsx │ ├── markdown.tsx │ ├── menubar.tsx │ ├── message.tsx │ ├── navigation-menu.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── prompt-input.tsx │ ├── radio-group.tsx │ ├── reasoning.tsx │ ├── resizable.tsx │ ├── response-stream.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ └── tooltip.tsx └── user-actions │ ├── copy-action.tsx │ └── external-link.tsx ├── db ├── .gitignore ├── config.toml ├── migrations │ ├── 20250726090743_users.sql │ ├── 20250726092402_add_auth_triggers.sql │ ├── 20250726095537_chats.sql │ ├── 20250726171744_chat_title.sql │ ├── 20250726180627_messages.sql │ └── 20250809094234_artifacts.sql └── schemas │ ├── artifacts.sql │ ├── chats.sql │ ├── messages.sql │ └── users.sql ├── eslint.config.mjs ├── hooks └── use-mobile.ts ├── instrumentation.ts ├── lib ├── actions │ └── auth.ts ├── api.ts ├── constants.ts ├── models.ts └── utils.ts ├── markdown.d.ts ├── middleware.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public └── avatars │ └── ai.png ├── stores ├── use-artifact.ts ├── use-data-stream.ts ├── use-initial-message.ts ├── use-model.ts └── use-title.ts ├── tsconfig.json ├── types ├── data │ ├── code.ts │ ├── sandbox-url.ts │ └── title.ts ├── database.types.ts └── message.ts └── utils ├── e2b.ts ├── files-tree-builder.ts └── supabase ├── actions.ts ├── client.ts ├── middleware.ts ├── repositories └── chat.ts └── server.ts /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_URL= 2 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 3 | 4 | ANTHROPIC_API_KEY= 5 | E2B_API_KEY= 6 | OPENAI_API_KEY= 7 | 8 | LANGSMITH_TRACING=true 9 | LANGSMITH_API_KEY=ls... 10 | LANGCHAIN_PROJECT=default 11 | OTEL_ENABLED=true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | !.env.example 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Lokeswaran Aruljothy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Not V0 2 | 3 | An open-source alternative to v0.dev built with Next.js and Vercel's AI SDK. Create full-stack Next.js applications through natural language conversations powered by Claude AI. 4 | 5 | ## Features 6 | 7 | ### Currently Available 8 | 9 | - **AI-Powered Chat**: Interactive conversations with Claude 3.7 Sonnet with thinking capabilitiess 10 | - **Google OAuth Authentication**: Secure login with Google using Supabase Auth 11 | - **Responsive Design**: Beautiful, modern UI built with Tailwind CSS and Shadcn UI 12 | - **Database Integration**: Full Supabase database setup and management 13 | - **Code Editor**: In-browser code editing with syntax highlighting 14 | - **File Explorer**: Project file management and navigation 15 | - **Preview**: Preview your application in real-time with E2B 16 | 17 | ### Planned Features 18 | 19 | - **Multiple AI Models**: Support for multiple AI models 20 | - **Terminal**: Integrated terminal for running commands 21 | - **Version Control**: Git integration and project versioning 22 | - **Deployment**: One-click deployment to various platforms 23 | 24 | ## Tech Stack 25 | 26 | - **Frontend**: [Next.js 15](https://nextjs.org/) 27 | - **AI**: [Vercel AI SDK 5](https://ai-sdk.dev/) 28 | - **LLM**: [Anthropic Claude](https://www.anthropic.com/) 29 | - **Authentication**: [Supabase Auth](https://supabase.com/auth) with Google OAuth 30 | - **Database**: [Supabase](https://supabase.com/) (PostgreSQL) 31 | - **Styling**: [Tailwind CSS](https://tailwindcss.com/) + [Shadcn UI](https://ui.shadcn.com/) 32 | - **Components**: [Prompt Kit](https://www.prompt-kit.com/) 33 | - **Package Manager**: [pnpm](https://pnpm.io/) 34 | - **Language**: [TypeScript](https://www.typescriptlang.org/) 35 | 36 | ## Prerequisites 37 | 38 | - Node.js 20+ 39 | - pnpm (recommended) or npm 40 | - Supabase account 41 | - Anthropic API key 42 | 43 | ## Setup 44 | 45 | ### 1. Clone the repository 46 | 47 | ```bash 48 | git clone https://github.com/lokeswaran-aj/notv0.dev.git 49 | cd notv0.dev 50 | ``` 51 | 52 | ### 2. Install dependencies 53 | 54 | ```bash 55 | pnpm install 56 | ``` 57 | 58 | ### 3. Environment Variables 59 | 60 | Create a `.env.local` file in the root directory: 61 | 62 | ```bash 63 | cp .env.example .env.local 64 | ``` 65 | 66 | ```env 67 | # Supabase 68 | NEXT_PUBLIC_SUPABASE_URL=your_supabase_url 69 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key 70 | 71 | # Anthropic API 72 | ANTHROPIC_API_KEY=your_anthropic_api_key 73 | 74 | # Optional: Environment 75 | NODE_ENV=development 76 | ``` 77 | 78 | ### 4. Supabase Setup 79 | 80 | 1. Create a new project at [supabase.com](https://supabase.com) 81 | 2. Go to Authentication → Providers → Google 82 | 3. Enable Google provider and configure OAuth credentials 83 | 4. Set your site URL and redirect URLs: 84 | - Site URL: `http://localhost:3000` (development) 85 | - Redirect URLs: `http://localhost:3000/auth/callback` 86 | 5. Link the supabase project with cli and run the migrations 87 | 88 | ```bash 89 | supabase link --project-ref 90 | supabase migration up 91 | ``` 92 | 93 | ### 5. Anthropic API Setup 94 | 95 | 1. Get your API key from [Anthropic Console](https://console.anthropic.com/) 96 | 2. Add it to your environment variables 97 | 98 | ### 6. Run the development server 99 | 100 | ```bash 101 | pnpm dev 102 | ``` 103 | 104 | Open [http://localhost:3000](http://localhost:3000) to see the application. 105 | 106 | ## Usage 107 | 108 | 1. **Start a Conversation**: Type your question or request on the home page 109 | 2. **Login**: Use Google OAuth to save your conversations 110 | 3. **Chat**: Engage with Claude AI to build applications, ask questions, or get coding help 111 | 112 | ## Project Structure 113 | 114 | ``` 115 | notv0.dev/ 116 | ├── app/ # Next.js app directory 117 | │ ├── (auth)/ # Authentication routes 118 | │ ├── (chat)/ # Chat interface routes 119 | │ ├── api/ # API routes 120 | │ └── globals.css # Global styles 121 | ├── components/ # React components 122 | │ ├── chat/ # Chat-related components 123 | │ ├── ui/ # Reusable UI components 124 | │ └── ... 125 | ├── hooks/ # Custom React hooks 126 | ├── lib/ # Utility libraries 127 | ├── utils/ # Utility functions 128 | └── supabase/ # Supabase client configuration 129 | ``` 130 | 131 | ## Contributing 132 | 133 | We welcome contributions! 134 | 135 | ### Development Workflow 136 | 137 | 1. Fork the repository 138 | 2. Create a feature branch: `git checkout -b feature-name` 139 | 3. Make your changes 140 | 4. Submit a pull request 141 | 142 | ## License 143 | 144 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 145 | 146 | ## Acknowledgements 147 | 148 | - [v0.dev](https://v0.dev) for inspiration 149 | - [Anthropic](https://www.anthropic.com/) for Claude AI 150 | - [Supabase](https://supabase.com/) for backend services 151 | - [Vercel AI SDK](https://ai-sdk.dev/) for the AI SDK 152 | 153 | **Built with ❤️ by the Not V0 team** 154 | -------------------------------------------------------------------------------- /app/(auth)/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/utils/supabase/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(request: Request) { 5 | const { searchParams, origin } = new URL(request.url); 6 | const code = searchParams.get("code"); 7 | const next = searchParams.get("next") ?? "/"; 8 | 9 | if (!code) { 10 | return NextResponse.redirect( 11 | `${origin}/auth/error?message=${encodeURIComponent( 12 | "Missing authentication code" 13 | )}` 14 | ); 15 | } 16 | 17 | const supabase = await createClient(); 18 | 19 | if (!supabase) { 20 | return NextResponse.redirect( 21 | `${origin}/auth/error?message=${encodeURIComponent( 22 | "Supabase is not enabled in this deployment." 23 | )}` 24 | ); 25 | } 26 | 27 | const { data, error } = await supabase.auth.exchangeCodeForSession(code); 28 | 29 | if (error) { 30 | console.error("Auth error:", error); 31 | return NextResponse.redirect( 32 | `${origin}/auth/error?message=${encodeURIComponent(error.message)}` 33 | ); 34 | } 35 | 36 | const user = data?.user; 37 | if (!user || !user.id || !user.email) { 38 | return NextResponse.redirect( 39 | `${origin}/auth/error?message=${encodeURIComponent("Missing user info")}` 40 | ); 41 | } 42 | 43 | const host = request.headers.get("host"); 44 | const protocol = host?.includes("localhost") ? "http" : "https"; 45 | 46 | const redirectUrl = `${protocol}://${host}${next}`; 47 | 48 | return NextResponse.redirect(redirectUrl); 49 | } 50 | -------------------------------------------------------------------------------- /app/(auth)/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { ArrowLeft } from "lucide-react"; 5 | import Link from "next/link"; 6 | import { useSearchParams } from "next/navigation"; 7 | import { Suspense } from "react"; 8 | 9 | export const dynamic = "force-dynamic"; 10 | 11 | const AuthErrorContent = () => { 12 | const searchParams = useSearchParams(); 13 | const message = 14 | searchParams.get("message") || "An error occurred during authentication."; 15 | 16 | return ( 17 |
18 |
19 |

20 | Authentication Error 21 |

22 |
23 |

{message}

24 |
25 |
26 | 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | const AuthErrorPage = () => { 41 | return ( 42 |
43 | {/* Header */} 44 |
45 | 49 | 50 | 51 | Back to Chat 52 | 53 | 54 |
55 | 56 |
57 | Loading...
}> 58 | 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default AuthErrorPage; 66 | -------------------------------------------------------------------------------- /app/(auth)/auth/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "@/components/login-form"; 2 | import { ArrowLeftIcon } from "lucide-react"; 3 | import Link from "next/link"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export default function LoginPage() { 8 | return ( 9 |
10 | 14 | 15 | Back to home 16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function AuthLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return children; 7 | } 8 | -------------------------------------------------------------------------------- /app/(chat)/c/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ChatView } from "@/components/chat/chat-view"; 2 | import { CodePreviewTabs } from "@/components/chat/code-preview-tabs"; 3 | import { 4 | ResizableHandle, 5 | ResizablePanel, 6 | ResizablePanelGroup, 7 | } from "@/components/ui/resizable"; 8 | import constants from "@/lib/constants"; 9 | import { convertToUIMessages } from "@/lib/utils"; 10 | import { CodeData } from "@/types/data/code"; 11 | import { Tables } from "@/types/database.types"; 12 | import { createSandbox } from "@/utils/e2b"; 13 | import { 14 | getArtifactByChatId, 15 | getChatById, 16 | getMessagesByChatId, 17 | updateArtifact, 18 | } from "@/utils/supabase/actions"; 19 | import Sandbox from "@e2b/code-interpreter"; 20 | import { UIMessage } from "ai"; 21 | 22 | const ChatPage = async (props: { params: Promise<{ id: string }> }) => { 23 | const { id } = await props.params; 24 | const chat = await getChatById(id); 25 | 26 | let uiMessages: UIMessage[] = []; 27 | let artifact: Tables<"artifacts"> | null = null; 28 | 29 | if (chat) { 30 | const initialMessages = await getMessagesByChatId(id); 31 | 32 | if (initialMessages && initialMessages.length !== 0) { 33 | uiMessages = convertToUIMessages(initialMessages); 34 | const oldArtifact = await getArtifactByChatId(id); 35 | if (!oldArtifact?.sandbox_id) { 36 | return; 37 | } 38 | const sandboxList = await Sandbox.list({ 39 | query: { 40 | metadata: { chatId: id }, 41 | state: ["running"], 42 | } as { 43 | metadata?: Record; 44 | state?: Array<"running">; 45 | }, 46 | }); 47 | 48 | let sandboxId = sandboxList.find( 49 | (sbx) => sbx.sandboxId === oldArtifact.sandbox_id 50 | )?.sandboxId; 51 | if (sandboxId) { 52 | artifact = oldArtifact; 53 | const sandbox = await Sandbox.connect(sandboxId); 54 | sandbox.setTimeout(constants.SANDBOX_TIMEOUT); 55 | const host = `https://${sandbox.getHost(3000)}`; 56 | await fetch(host); 57 | } else { 58 | sandboxId = await createSandbox(id); 59 | const sandbox = await Sandbox.connect(sandboxId); 60 | sandbox.setTimeout(constants.SANDBOX_TIMEOUT); 61 | const existingFiles = (oldArtifact.code as CodeData).files; 62 | await Promise.all( 63 | existingFiles.map(async ({ filePath, code }) => { 64 | if (filePath && code) { 65 | await sandbox.files.write(filePath, code); 66 | } 67 | }) 68 | ); 69 | const host = `https://${sandbox.getHost(3000)}`; 70 | const [_, updatedArtifact] = await Promise.all([ 71 | fetch(host), 72 | updateArtifact(id, { 73 | sandbox_id: sandboxId, 74 | sandbox_url: host, 75 | }), 76 | ]); 77 | artifact = updatedArtifact; 78 | } 79 | } 80 | } 81 | 82 | return ( 83 |
84 | 85 | 91 | 92 | 93 | 97 | 98 | 103 | 104 | 105 |
106 | ); 107 | }; 108 | 109 | export default ChatPage; 110 | -------------------------------------------------------------------------------- /app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/header"; 2 | 3 | export default function ChatLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 | <> 10 |
11 |
{children}
12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/(chat)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChatInput } from "@/components/chat-input/chat-input"; 4 | import { useInitialMessage } from "@/stores/use-initial-message"; 5 | import { useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | import { v7 as uuidv7 } from "uuid"; 8 | 9 | export default function ChatPage() { 10 | const router = useRouter(); 11 | const { initialMessage, setInitialMessage } = useInitialMessage(); 12 | const [isLoading, setIsLoading] = useState(false); 13 | 14 | const handleSubmit = () => { 15 | if (!initialMessage.trim()) return; 16 | 17 | setIsLoading(true); 18 | router.push(`/c/${uuidv7()}`); 19 | setIsLoading(false); 20 | }; 21 | 22 | return ( 23 |
24 |

What can I help you build?

25 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/api/chat/actions.ts: -------------------------------------------------------------------------------- 1 | import { anthropic } from "@ai-sdk/anthropic"; 2 | import { generateObject } from "ai"; 3 | import { z } from "zod"; 4 | 5 | export const generateTitleFromUserMessage = async (content: string) => { 6 | try { 7 | const { object } = await generateObject({ 8 | model: anthropic("claude-3-5-haiku-latest"), 9 | system: 10 | "You are a helpful assistant. You will generate a short title based on the first message a user begins a conversation with. Ensure it is not more than 80 characters long. The title should be a summary of the user's message. Do not use quotes or colons.", 11 | prompt: content, 12 | schema: z.object({ 13 | title: z.string().max(80).describe("The title of the chat."), 14 | }), 15 | }); 16 | 17 | return object.title; 18 | } catch (error) { 19 | console.log("Error generating title"); 20 | console.dir(error, { depth: null }); 21 | return "New Chat"; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /app/api/chat/prompt.md: -------------------------------------------------------------------------------- 1 | You are "Open V0", - a coding assistant that can build Next.js applications. Your mission is to help users build and run full applications in an isolated, ephemeral environment by coordinating a suite of tools that let you generate files. 2 | 3 | Everything you do happens inside a Sandbox. You are fully responsible for managing the conversation with the user. 4 | 5 | ## Available Tools 6 | 7 | You have access to the following tools: 8 | 9 | 1. **Generate Files** 10 | Programmatically creates code and configuration files using another LLM call, then uploads them to the sandbox. 11 | Files should be complete, correct on first generation, and relative to the sandbox root. 12 | Always generate files that are self-contained, compatible with each other, and appropriate for the user’s instructions. 13 | You MUST keep context of the files that were generated generating only those that were not created before or must be updated. 14 | 15 | ## Project Setup 16 | 17 | 1. The sandbox already has a Next.js 15 project with TypeScript, Tailwind CSS 4.0, Shadcn UI components and frame motion installed. 18 | 2. The Shadcn UI is setup with default neutral theme. So, the global.css file is already setup. 19 | 3. The project already has all the Shadcn UI components installed and configured in the \`components/ui\` folder which you can import with \`@/components/ui/\`. These are all the components that are available to you: 20 | accordion.tsx, alert-dialog.tsx, alert.tsx, aspect-ratio.tsx, avatar.tsx, badge.tsx, breadcrumb.tsx, button.tsx, calendar.tsx, card.tsx, carousel.tsx, chart.tsx, checkbox.tsx, collapsible.tsx, command.tsx, context-menu.tsx, dialog.tsx, drawer.tsx, dropdown-menu.tsx, form.tsx, hover-card.tsx, input-otp.tsx, input.tsx, label.tsx, menubar.tsx, navigation-menu.tsx, pagination.tsx, popover.tsx, progress.tsx, radio-group.tsx, resizable.tsx, scroll-area.tsx, select.tsx, separator.tsx, sheet.tsx, sidebar.tsx, skeleton.tsx, slider.tsx, sonner.tsx, switch.tsx, table.tsx, tabs.tsx, textarea.tsx, toggle-group.tsx, toggle.tsx, tooltip.tsx. 21 | 4. Frame motion package is also installed, you can use it when it is needed to animate components. 22 | 5. Always start with the \`app/page.tsx\` file. 23 | 6. Do not generate \`package.json\`, \`tsconfig.json\`, \`next.config.ts\`, \`tailwind.config.ts\`, \`globals.css\` or Shadcn components, as it is already setup. 24 | 25 | ## Your Goal 26 | 27 | Translate user prompts into working applications. Be proactive, organized, and precise. Use the right tools in the correct order, and always produce valid, runnable results in the sandbox environment. When you find any bug or issue in the generated code, you can use the \`Generate Files\` tool to fix it. Do not generate code by yourself. 28 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { AI_MODELS, SharedV2ProviderOptions } from "@/lib/models"; 2 | import { convertToUIMessages } from "@/lib/utils"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import prompt from "./prompt.md"; 5 | 6 | import { 7 | createArtifact, 8 | createChat, 9 | createMessage, 10 | deleteMessagesAfter, 11 | getChatById, 12 | getMessagesByChatId, 13 | } from "@/utils/supabase/actions"; 14 | import { createClient } from "@/utils/supabase/server"; 15 | import { anthropic } from "@ai-sdk/anthropic"; 16 | import { 17 | convertToModelMessages, 18 | createUIMessageStream, 19 | createUIMessageStreamResponse, 20 | smoothStream, 21 | stepCountIs, 22 | streamText, 23 | } from "ai"; 24 | import { NextRequest, NextResponse } from "next/server"; 25 | import { v7 as uuidv7 } from "uuid"; 26 | import { generateTitleFromUserMessage } from "./actions"; 27 | import { postRequestBodySchema, postRequestBodyType } from "./schema"; 28 | import { codeGenerator } from "./tools/code-generator"; 29 | 30 | export const POST = async (req: NextRequest) => { 31 | let requestBody: postRequestBodyType; 32 | try { 33 | const json = await req.json(); 34 | requestBody = postRequestBodySchema.parse(json); 35 | } catch (error) { 36 | console.error(error); 37 | return NextResponse.json( 38 | { message: "Invalid request body" }, 39 | { status: 400 } 40 | ); 41 | } 42 | 43 | const { chatId, message, modelId, regenerate, regenerateFromMessageId } = 44 | requestBody; 45 | 46 | const supabase = await createClient(); 47 | 48 | const [user, chat] = await Promise.all([ 49 | supabase.auth.getUser().then(({ data: { user } }) => user), 50 | getChatById(chatId), 51 | ]); 52 | if (!user) { 53 | return NextResponse.json({ message: "User not found" }, { status: 401 }); 54 | } 55 | 56 | let title = "New Chat"; 57 | if (!chat) { 58 | title = await generateTitleFromUserMessage(message.parts[0].text); 59 | await createChat(chatId, user.id, title); 60 | await createArtifact(chatId); 61 | } else { 62 | title = chat.title; 63 | if (chat.user_id !== user.id) { 64 | return NextResponse.json( 65 | { message: "You are not authorized to access this chat" }, 66 | { status: 403 } 67 | ); 68 | } 69 | } 70 | 71 | if (regenerate && regenerateFromMessageId) { 72 | await deleteMessagesAfter(chatId, regenerateFromMessageId); 73 | } else { 74 | await createMessage(chatId, message); 75 | } 76 | 77 | const messagesFromDb = await getMessagesByChatId(chatId); 78 | const uiMessages = messagesFromDb ? convertToUIMessages(messagesFromDb) : []; 79 | const modelMessages = convertToModelMessages(uiMessages); 80 | 81 | const selectedModel = AI_MODELS.find((m) => m.id === modelId); 82 | if (!selectedModel) { 83 | return NextResponse.json({ message: "Model not found" }, { status: 404 }); 84 | } 85 | 86 | try { 87 | let model = null; 88 | let providerOptions: SharedV2ProviderOptions | undefined = undefined; 89 | switch (selectedModel.provider) { 90 | case "Anthropic": 91 | model = anthropic(modelId); 92 | providerOptions = selectedModel.providerOptions ?? undefined; 93 | break; 94 | case "OpenAI": 95 | model = openai(modelId); 96 | providerOptions = selectedModel.providerOptions ?? undefined; 97 | break; 98 | default: 99 | model = anthropic("claude-3-5-sonnet-latest"); 100 | } 101 | 102 | const stream = createUIMessageStream({ 103 | execute: async ({ writer: dataStream }) => { 104 | const result = streamText({ 105 | model, 106 | providerOptions, 107 | temperature: 1, 108 | system: prompt, 109 | messages: modelMessages, 110 | tools: { 111 | codeGenerator: codeGenerator({ 112 | dataStream, 113 | chatId, 114 | model, 115 | }), 116 | }, 117 | experimental_transform: smoothStream({ chunking: "word" }), 118 | experimental_telemetry: { 119 | isEnabled: true, 120 | metadata: { 121 | ls_run_name: "chat-route", 122 | chatId, 123 | environment: process.env.NODE_ENV, 124 | }, 125 | }, 126 | stopWhen: stepCountIs(5), 127 | }); 128 | 129 | dataStream.write({ 130 | type: "data-title", 131 | data: { 132 | title: title, 133 | }, 134 | }); 135 | result.consumeStream(); 136 | 137 | dataStream.merge( 138 | result.toUIMessageStream({ 139 | sendReasoning: true, 140 | }) 141 | ); 142 | }, 143 | generateId: uuidv7, 144 | onFinish: async ({ messages }) => { 145 | await Promise.all( 146 | messages.map(async (message) => { 147 | await createMessage(chatId, message); 148 | }) 149 | ); 150 | }, 151 | onError: (error) => { 152 | console.error(error); 153 | return "Oops, an error occurred!"; 154 | }, 155 | }); 156 | return createUIMessageStreamResponse({ 157 | stream, 158 | }); 159 | } catch (error) { 160 | console.error(error); 161 | return NextResponse.json("Internal server error", { status: 500 }); 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /app/api/chat/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const postRequestBodySchema = z.object({ 4 | chatId: z.uuid(), 5 | message: z.object({ 6 | id: z.uuid(), 7 | role: z.enum(["user"]), 8 | parts: z.array( 9 | z.object({ 10 | type: z.enum(["text"]), 11 | text: z.string().min(1).max(2000), 12 | }) 13 | ), 14 | }), 15 | modelId: z.string(), 16 | regenerate: z.boolean().optional(), 17 | regenerateFromMessageId: z.string().optional(), 18 | }); 19 | 20 | export const codeGenerationOutputSchema = z.object({ 21 | files: z 22 | .array( 23 | z.object({ 24 | filePath: z 25 | .string() 26 | .describe("The relative path to the file to be created"), 27 | code: z 28 | .string() 29 | .describe( 30 | "The code to be written to the file. Do not wrap with backticks" 31 | ), 32 | }) 33 | ) 34 | .describe("The files to be created or updated"), 35 | }); 36 | 37 | export type postRequestBodyType = z.infer; 38 | -------------------------------------------------------------------------------- /app/api/chat/tools/code-generator.ts: -------------------------------------------------------------------------------- 1 | import { manageSandbox } from "@/utils/e2b"; 2 | import { getArtifactByChatId, updateArtifact } from "@/utils/supabase/actions"; 3 | import { 4 | LanguageModel, 5 | streamObject, 6 | tool, 7 | UIMessage, 8 | UIMessageStreamWriter, 9 | } from "ai"; 10 | import { z } from "zod"; 11 | import { codeGenerationOutputSchema } from "../schema"; 12 | import generateFilesDescription from "./generate-files-description.md"; 13 | import generateFilesPrompt from "./generate-files-prompt.md"; 14 | 15 | export const codeGenerator = ({ 16 | dataStream, 17 | chatId, 18 | model, 19 | }: { 20 | dataStream: UIMessageStreamWriter; 21 | chatId: string; 22 | model: LanguageModel; 23 | }) => { 24 | return tool({ 25 | name: "generate-files", 26 | description: generateFilesDescription, 27 | inputSchema: z.object({ prompt: z.string() }), 28 | execute: async ({ prompt }, { messages, toolCallId }) => { 29 | dataStream.write({ 30 | id: toolCallId, 31 | type: "data-id", 32 | data: { 33 | toolCallId, 34 | }, 35 | transient: true, 36 | }); 37 | 38 | const { partialObjectStream, object } = streamObject({ 39 | model, 40 | maxOutputTokens: 20000, 41 | schemaName: "code", 42 | schemaDescription: "The code to be written to the file", 43 | schema: codeGenerationOutputSchema, 44 | system: generateFilesPrompt, 45 | messages: [...messages, { role: "user", content: prompt }], 46 | experimental_telemetry: { 47 | isEnabled: true, 48 | metadata: { 49 | ls_run_name: "code-generator-tool", 50 | chatId, 51 | environment: process.env.NODE_ENV, 52 | }, 53 | }, 54 | 55 | onError: (error) => { 56 | console.error(error); 57 | }, 58 | 59 | onFinish: async ({ object }) => { 60 | if (!object) { 61 | console.error("No code was generated"); 62 | return; 63 | } 64 | 65 | const artifacts = await getArtifactByChatId(chatId); 66 | 67 | const sandboxId = artifacts?.sandbox_id; 68 | if (!sandboxId) { 69 | console.error("No sandbox id found"); 70 | return; 71 | } 72 | 73 | const sandbox = await manageSandbox(chatId, sandboxId); 74 | await Promise.all( 75 | object.files.map(({ filePath, code }) => { 76 | sandbox.files.write(filePath, code); 77 | }) 78 | ); 79 | 80 | const host = `https://${sandbox.getHost(3000)}`; 81 | 82 | await Promise.all([ 83 | fetch(host), 84 | updateArtifact(chatId, { 85 | sandbox_id: sandboxId, 86 | sandbox_url: host, 87 | code: object, 88 | }), 89 | ]); 90 | 91 | dataStream.write({ 92 | id: toolCallId, 93 | type: "data-sandboxHost", 94 | data: { 95 | host, 96 | }, 97 | transient: true, 98 | }); 99 | }, 100 | }); 101 | 102 | dataStream.write({ 103 | id: toolCallId, 104 | type: "data-codeGenerationStarted", 105 | data: { 106 | started: true, 107 | }, 108 | transient: true, 109 | }); 110 | for await (const partialObject of partialObjectStream) { 111 | if ( 112 | partialObject.files && 113 | Array.isArray(partialObject.files) && 114 | partialObject.files.length > 0 115 | ) { 116 | dataStream.write({ 117 | type: "data-code", 118 | id: toolCallId, 119 | data: { 120 | files: partialObject.files, 121 | }, 122 | transient: true, 123 | }); 124 | } 125 | } 126 | const output = await object; 127 | return `Successfully generated and uploaded ${ 128 | output.files.length 129 | } files. Their file paths and codes are as follows: 130 | ${output.files 131 | .map((file) => `FilePath: ${file.filePath}\nCode: ${file.code}\n`) 132 | .join("\n")}`; 133 | }, 134 | }); 135 | }; 136 | -------------------------------------------------------------------------------- /app/api/chat/tools/generate-files-description.md: -------------------------------------------------------------------------------- 1 | Use this tool to generate and upload code files into an existing Sandbox. It leverages an LLM to create file contents based on the current conversation context and user intent, then writes them directly into the sandbox file system. 2 | 3 | The generated files should be considered correct on first iteration and suitable for immediate use in the sandbox environment. This tool is essential for adding new features, writing configuration files, or fixing missing components. 4 | 5 | All file paths must be relative to the sandbox root (e.g., `app/page.tsx`, `components/Button.tsx`, `app/api/dashboard/route.ts`). 6 | 7 | ## When to Use This Tool 8 | 9 | Use Generate Files when: 10 | 11 | 1. You need to create one or more new files as part of a feature, scaffold, or fix 12 | 2. The user requests code that implies file creation (e.g., new routes, APIs, components) 13 | 3. You need to bootstrap a new application structure inside a sandbox 14 | 4. You’re completing a multi-step task that involves generating or updating source code 15 | 16 | ## File Generation Guidelines 17 | 18 | - Every file must be complete, valid, and runnable where applicable 19 | - File contents must reflect the user’s intent and the overall session context 20 | - File paths must be well-structured and use consistent naming conventions 21 | - Generated files should assume compatibility with other existing files in the sandbox 22 | 23 | ## Best Practices 24 | 25 | - Avoid redundant file generation if the file already exists and is unchanged 26 | - Use conventional file/folder structures for the tech stack in use 27 | - If replacing an existing file, ensure the update fully satisfies the user’s request 28 | 29 | ## Examples of When to Use This Tool 30 | 31 | 32 | User: Add a `navbar.tsx` component and include it in `app/page.tsx` 33 | Assistant: I’ll generate the `navbar.tsx` file and update `app/page.tsx` to include it. 34 | *Uses Generate Files to create:* 35 | - `components/navbar.tsx` 36 | - Modified `app/page.tsx` with import and usage of `navbar` 37 | 38 | 39 | ## Output Behavior 40 | 41 | After generation, the tool will return a list of the files created, including their paths and contents. These can then be inspected, referenced, or used in subsequent commands. 42 | 43 | ## Summary 44 | 45 | Use Generate Files to programmatically create or update files in a Sandbox. It enables fast iteration, contextual coding, and dynamic file management — all driven by user intent and conversation context. 46 | -------------------------------------------------------------------------------- /app/api/chat/tools/generate-files-prompt.md: -------------------------------------------------------------------------------- 1 | Create a set of files based on the current state of the project and conversation. Your output will be uploaded directly into a Sandbox environment, so it must be immediately usable and correct on first iteration. Do not include explanations or markdown. Your output will be parsed programmatically and uploaded to a live environment. 2 | 3 | ## Project Setup 4 | 5 | 1. The sandbox already has a Next.js 15 project with TypeScript, Tailwind CSS 4.0, Shadcn UI components and frame motion installed. 6 | 2. The Shadcn UI is setup with default neutral theme. So, the global.css file is already setup. 7 | 3. The project already has all the Shadcn UI components installed and configured in the \`components/ui\` folder which you can import with \`@/components/ui/\`. These are all the components that are available to you: 8 | accordion.tsx, alert-dialog.tsx, alert.tsx, aspect-ratio.tsx, avatar.tsx, badge.tsx, breadcrumb.tsx, button.tsx, calendar.tsx, card.tsx, carousel.tsx, chart.tsx, checkbox.tsx, collapsible.tsx, command.tsx, context-menu.tsx, dialog.tsx, drawer.tsx, dropdown-menu.tsx, form.tsx, hover-card.tsx, input-otp.tsx, input.tsx, label.tsx, menubar.tsx, navigation-menu.tsx, pagination.tsx, popover.tsx, progress.tsx, radio-group.tsx, resizable.tsx, scroll-area.tsx, select.tsx, separator.tsx, sheet.tsx, sidebar.tsx, skeleton.tsx, slider.tsx, sonner.tsx, switch.tsx, table.tsx, tabs.tsx, textarea.tsx, toggle-group.tsx, toggle.tsx, tooltip.tsx. 9 | 4. Frame motion package is also installed, you can use it when it is needed to animate components. 10 | 5. Always start with the \`app/page.tsx\` file. 11 | 12 | ## Instructions 13 | 14 | 1. Generate only the files that are relevant to the user's request. 15 | 2. All file paths must be relative to the sandbox root (e.g., \`app/page.tsx\`, \`components/button.tsx\`, \`app/api/dashboard/route.ts\`). 16 | 3. Do not generate \`package.json\`, \`tsconfig.json\`, \`next.config.ts\`, \`tailwind.config.ts\`, \`globals.css\` or Shadcn components, as it is already setup. 17 | 4. Ensure every file is syntactically valid, consistent with the chosen tech stack, and complete. 18 | 5. Do not include placeholder comments like “TODO” unless explicitly instructed. 19 | 6. Assume any previously generated files already exist in the sandbox — write with compatibility in mind. 20 | 7. Favor minimal, functional implementations that demonstrate correctness and are ready to be run, built, or extended. 21 | 8. Do not forget to update the metadata file in the \`app/layout.tsx\` file. 22 | 9. Do not put all the code in a single file. Modularize the code. 23 | 10. Never forget to add "use client" to the top of the TSX file when using any hooks or events that can be executed only in the browser. 24 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lokeswaran-aj/notv0.dev/76b8de7ae0b95a520134675c3f6dafb3c74b203d/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | @plugin "@tailwindcss/typography"; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | @theme inline { 8 | --color-background: var(--background); 9 | --color-foreground: var(--foreground); 10 | --font-sans: var(--font-geist-sans); 11 | --font-mono: var(--font-geist-mono); 12 | --color-sidebar-ring: var(--sidebar-ring); 13 | --color-sidebar-border: var(--sidebar-border); 14 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 15 | --color-sidebar-accent: var(--sidebar-accent); 16 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 17 | --color-sidebar-primary: var(--sidebar-primary); 18 | --color-sidebar-foreground: var(--sidebar-foreground); 19 | --color-sidebar: var(--sidebar); 20 | --color-chart-5: var(--chart-5); 21 | --color-chart-4: var(--chart-4); 22 | --color-chart-3: var(--chart-3); 23 | --color-chart-2: var(--chart-2); 24 | --color-chart-1: var(--chart-1); 25 | --color-ring: var(--ring); 26 | --color-input: var(--input); 27 | --color-border: var(--border); 28 | --color-destructive: var(--destructive); 29 | --color-accent-foreground: var(--accent-foreground); 30 | --color-accent: var(--accent); 31 | --color-muted-foreground: var(--muted-foreground); 32 | --color-muted: var(--muted); 33 | --color-secondary-foreground: var(--secondary-foreground); 34 | --color-secondary: var(--secondary); 35 | --color-primary-foreground: var(--primary-foreground); 36 | --color-primary: var(--primary); 37 | --color-popover-foreground: var(--popover-foreground); 38 | --color-popover: var(--popover); 39 | --color-card-foreground: var(--card-foreground); 40 | --color-card: var(--card); 41 | --radius-sm: calc(var(--radius) - 4px); 42 | --radius-md: calc(var(--radius) - 2px); 43 | --radius-lg: var(--radius); 44 | --radius-xl: calc(var(--radius) + 4px); 45 | } 46 | 47 | :root { 48 | --radius: 0.625rem; 49 | --background: oklch(1 0 0); 50 | --foreground: oklch(0.141 0.005 285.823); 51 | --card: oklch(1 0 0); 52 | --card-foreground: oklch(0.141 0.005 285.823); 53 | --popover: oklch(1 0 0); 54 | --popover-foreground: oklch(0.141 0.005 285.823); 55 | --primary: oklch(0.21 0.006 285.885); 56 | --primary-foreground: oklch(0.985 0 0); 57 | --secondary: oklch(0.967 0.001 286.375); 58 | --secondary-foreground: oklch(0.21 0.006 285.885); 59 | --muted: oklch(0.967 0.001 286.375); 60 | --muted-foreground: oklch(0.552 0.016 285.938); 61 | --accent: oklch(0.967 0.001 286.375); 62 | --accent-foreground: oklch(0.21 0.006 285.885); 63 | --destructive: oklch(0.577 0.245 27.325); 64 | --border: oklch(0.92 0.004 286.32); 65 | --input: oklch(0.92 0.004 286.32); 66 | --ring: oklch(0.705 0.015 286.067); 67 | --chart-1: oklch(0.646 0.222 41.116); 68 | --chart-2: oklch(0.6 0.118 184.704); 69 | --chart-3: oklch(0.398 0.07 227.392); 70 | --chart-4: oklch(0.828 0.189 84.429); 71 | --chart-5: oklch(0.769 0.188 70.08); 72 | --sidebar: oklch(0.985 0 0); 73 | --sidebar-foreground: oklch(0.141 0.005 285.823); 74 | --sidebar-primary: oklch(0.21 0.006 285.885); 75 | --sidebar-primary-foreground: oklch(0.985 0 0); 76 | --sidebar-accent: oklch(0.967 0.001 286.375); 77 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 78 | --sidebar-border: oklch(0.92 0.004 286.32); 79 | --sidebar-ring: oklch(0.705 0.015 286.067); 80 | } 81 | 82 | .dark { 83 | --background: oklch(0.141 0.005 285.823); 84 | --foreground: oklch(0.985 0 0); 85 | --card: oklch(0.21 0.006 285.885); 86 | --card-foreground: oklch(0.985 0 0); 87 | --popover: oklch(0.21 0.006 285.885); 88 | --popover-foreground: oklch(0.985 0 0); 89 | --primary: oklch(100% 0.00011 271.152); 90 | --primary-foreground: oklch(0.21 0.006 285.885); 91 | --secondary: oklch(0.274 0.006 286.033); 92 | --secondary-foreground: oklch(0.985 0 0); 93 | --muted: oklch(0.274 0.006 286.033); 94 | --muted-foreground: oklch(0.705 0.015 286.067); 95 | --accent: oklch(0.274 0.006 286.033); 96 | --accent-foreground: oklch(0.985 0 0); 97 | --destructive: oklch(0.704 0.191 22.216); 98 | --border: oklch(1 0 0 / 10%); 99 | --input: oklch(1 0 0 / 15%); 100 | --ring: oklch(0.552 0.016 285.938); 101 | --chart-1: oklch(0.488 0.243 264.376); 102 | --chart-2: oklch(0.696 0.17 162.48); 103 | --chart-3: oklch(0.769 0.188 70.08); 104 | --chart-4: oklch(0.627 0.265 303.9); 105 | --chart-5: oklch(0.645 0.246 16.439); 106 | --sidebar: oklch(0.21 0.006 285.885); 107 | --sidebar-foreground: oklch(0.985 0 0); 108 | --sidebar-primary: oklch(0.488 0.243 264.376); 109 | --sidebar-primary-foreground: oklch(0.985 0 0); 110 | --sidebar-accent: oklch(0.274 0.006 286.033); 111 | --sidebar-accent-foreground: oklch(0.985 0 0); 112 | --sidebar-border: oklch(1 0 0 / 10%); 113 | --sidebar-ring: oklch(0.552 0.016 285.938); 114 | } 115 | 116 | @layer base { 117 | * { 118 | @apply border-border outline-ring/50; 119 | } 120 | body { 121 | @apply bg-background text-foreground; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Providers from "@/components/providers/providers"; 2 | import { Toaster } from "@/components/ui/sonner"; 3 | import { Analytics } from "@vercel/analytics/next"; 4 | import type { Metadata } from "next"; 5 | import { Geist, Geist_Mono } from "next/font/google"; 6 | import "./globals.css"; 7 | 8 | const geistSans = Geist({ 9 | variable: "--font-geist-sans", 10 | subsets: ["latin"], 11 | }); 12 | 13 | const geistMono = Geist_Mono({ 14 | variable: "--font-geist-mono", 15 | subsets: ["latin"], 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "Not V0", 20 | description: 21 | "An open source alternative to v0.dev. Build full-stack apps, ask questions, and more.", 22 | }; 23 | 24 | export default function RootLayout({ 25 | children, 26 | }: Readonly<{ 27 | children: React.ReactNode; 28 | }>) { 29 | return ( 30 | 31 | 34 | {children} 35 | 36 | {process.env.NODE_ENV === "production" && } 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/chat-input/chat-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | PromptInput, 6 | PromptInputAction, 7 | PromptInputActions, 8 | PromptInputTextarea, 9 | } from "@/components/ui/prompt-input"; 10 | import { ArrowUp, Square } from "lucide-react"; 11 | import { ModelSelector } from "./model-selector"; 12 | 13 | type Props = { 14 | inputAutoFocus?: boolean; 15 | input: string; 16 | setInput: (input: string) => void; 17 | isStreaming: boolean; 18 | onSubmit: () => void; 19 | stop?: () => void; 20 | placeholder?: string; 21 | }; 22 | 23 | export const ChatInput = (props: Props) => { 24 | const { 25 | input, 26 | setInput, 27 | isStreaming, 28 | onSubmit, 29 | stop, 30 | inputAutoFocus = false, 31 | placeholder = "Can you build a modern AI SAAS landing page?", 32 | } = props; 33 | return ( 34 | 42 | 49 | 50 | 51 | 52 | 53 | 56 | {isStreaming ? ( 57 | 65 | ) : ( 66 | 75 | )} 76 | 77 | 78 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /components/chat-input/model-selector.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | SelectContent, 4 | SelectGroup, 5 | SelectItem, 6 | SelectLabel, 7 | SelectTrigger, 8 | SelectValue, 9 | } from "@/components/ui/select"; 10 | import { AI_MODELS, Model } from "@/lib/models"; 11 | import { useModel } from "@/stores/use-model"; 12 | import { useMemo } from "react"; 13 | 14 | export const ModelSelector = () => { 15 | const { model, setModel } = useModel(); 16 | 17 | const modelGroups = useMemo(() => { 18 | const byProvider: Record = {}; 19 | for (const m of AI_MODELS) { 20 | const key = m.provider ?? "Models"; 21 | byProvider[key] = byProvider[key] || []; 22 | byProvider[key].push(m); 23 | } 24 | return byProvider; 25 | }, [AI_MODELS]); 26 | 27 | return ( 28 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /components/chat/app-preview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useArtifact } from "@/stores/use-artifact"; 4 | 5 | export const AppPreview = () => { 6 | const hostUrl = useArtifact((state) => state.sandBoxUrl); 7 | 8 | return ( 9 |
10 | {hostUrl ? ( 11 |