├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── components.json ├── drizzle.config.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── dots-background.png ├── file.svg ├── globe.svg ├── logo.svg ├── model-background.png ├── models │ ├── gemma.png │ ├── llama.png │ ├── mixtral.png │ ├── nemotron.png │ ├── noushermes.webp │ ├── qwen.png │ └── wizardlm.png ├── next.svg ├── og-image.png ├── vercel.svg └── window.svg ├── src ├── app │ ├── actions.ts │ ├── api │ │ └── generate-app │ │ │ └── route.ts │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── models.tsx │ ├── page.tsx │ └── top-models │ │ ├── loading.tsx │ │ └── page.tsx ├── components │ ├── icons │ │ ├── arrows.tsx │ │ ├── github.tsx │ │ ├── ribbon.tsx │ │ ├── swords.tsx │ │ ├── three-arrows.tsx │ │ ├── up-right-arrow.tsx │ │ └── x.tsx │ ├── logo.tsx │ ├── spinner.tsx │ └── ui │ │ ├── button.tsx │ │ ├── table.tsx │ │ └── tabs.tsx ├── db.ts ├── lib │ └── utils.ts └── schema.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | force=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | CodeArena 3 |

CodeArena

4 |
5 | 6 |

7 | Watch LLMs battle to build the same app and see a live leaderboard of the best OSS coding LLMs. 8 |

9 | 10 | ## Tech stack 11 | 12 | - LLMs on [Together AI](https://dub.sh/together-ai) on Together AI to generate code 13 | - [Sandpack](https://sandpack.codesandbox.io/) for rendering the UI code 14 | - [Next.js](https://nextjs.org/) with TypeScript for the app framework 15 | - [Shadcn](https://ui.shadcn.com/) for UI components & [Tailwind](https://tailwindcss.com/) for styling 16 | - [Plausible](https://plausible.io/) & [Helicone](https://helicone.ai/) for analytics & observability 17 | 18 | ## Cloning & running 19 | 20 | 1. Clone the repo: `git clone https://github.com/Nutlope/codearena` 21 | 2. Create a `.env` file and add your [Together AI API key](https://api.together.xyz/settings/api-keys): `TOGETHER_API_KEY=` 22 | 3. Create a postgres DB (I recommend [Neon](https://neon.tech/)) and add connection details to `.env`: `DATABASE_URL=` 23 | 4. Run `npm install` and `npm run dev` to install dependencies and run locally. 24 | 25 | ## Future tasks 26 | 27 | - [ ] Add two chained LLM calls generations for better results (also do a DB migration to tag each app/battle with single/multi calls) 28 | - [ ] Add Elo scores 29 | - [ ] Add the abilty to generate + run scripts 30 | -------------------------------------------------------------------------------- /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": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 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 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | config({ path: ".env.local" }); 5 | 6 | if (typeof process.env.DATABASE_URL !== "string") { 7 | throw new Error("DATABASE_URL is not set"); 8 | } 9 | 10 | export default defineConfig({ 11 | schema: "./src/schema.ts", 12 | dialect: "postgresql", 13 | dbCredentials: { 14 | url: process.env.DATABASE_URL, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-arena", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:push": "drizzle-kit push", 11 | "db:pull": "drizzle-kit pull", 12 | "db:studio": "drizzle-kit studio" 13 | }, 14 | "dependencies": { 15 | "@codesandbox/sandpack-react": "^2.19.10", 16 | "@codesandbox/sandpack-themes": "^2.0.21", 17 | "@neondatabase/serverless": "^0.10.4", 18 | "@radix-ui/react-slot": "^1.1.0", 19 | "@radix-ui/react-tabs": "^1.1.1", 20 | "class-variance-authority": "^0.7.1", 21 | "clsx": "^2.1.1", 22 | "dedent": "^1.5.3", 23 | "dotenv": "^16.4.5", 24 | "drizzle-orm": "^0.36.4", 25 | "lucide-react": "^0.462.0", 26 | "next": "15.0.3", 27 | "next-plausible": "^3.12.4", 28 | "party-js": "^2.2.0", 29 | "postgres": "^3.4.5", 30 | "react": "19.0.0-rc-66855b96-20241106", 31 | "react-dom": "19.0.0-rc-66855b96-20241106", 32 | "react-use-measure": "^2.1.1", 33 | "tailwind-merge": "^2.5.5", 34 | "tailwindcss-animate": "^1.0.7", 35 | "together-ai": "^0.9.0", 36 | "zod": "^3.23.8" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^20", 40 | "@types/react": "^18", 41 | "@types/react-dom": "^18", 42 | "drizzle-kit": "^0.28.1", 43 | "eslint": "^8", 44 | "eslint-config-next": "15.0.3", 45 | "postcss": "^8", 46 | "prettier": "^3.3.3", 47 | "prettier-plugin-tailwindcss": "^0.6.9", 48 | "tailwindcss": "^3.4.1", 49 | "typescript": "^5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/dots-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/dots-background.png -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/model-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/model-background.png -------------------------------------------------------------------------------- /public/models/gemma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/models/gemma.png -------------------------------------------------------------------------------- /public/models/llama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/models/llama.png -------------------------------------------------------------------------------- /public/models/mixtral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/models/mixtral.png -------------------------------------------------------------------------------- /public/models/nemotron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/models/nemotron.png -------------------------------------------------------------------------------- /public/models/noushermes.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/models/noushermes.webp -------------------------------------------------------------------------------- /public/models/qwen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/models/qwen.png -------------------------------------------------------------------------------- /public/models/wizardlm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/models/wizardlm.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/public/og-image.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "@/db"; 4 | import { apps, battles } from "@/schema"; 5 | import { cookies } from "next/headers"; 6 | 7 | type App = { 8 | model: { label: string; apiName: string }; 9 | code: string; 10 | trimmedCode: string; 11 | completionTokens: number; 12 | totalTime: number; 13 | }; 14 | 15 | export default async function saveBattle({ 16 | prompt, 17 | winners, 18 | losers, 19 | }: { 20 | prompt: string; 21 | winners: App[]; 22 | losers: App[]; 23 | }) { 24 | const creatorCookie = await findOrCreateCreatorCookie(); 25 | 26 | const result = await db 27 | .insert(battles) 28 | .values({ 29 | prompt, 30 | creatorCookie, 31 | }) 32 | .returning(); 33 | 34 | const battle = result[0]; 35 | 36 | for (const winner of winners) { 37 | await db.insert(apps).values({ 38 | battleId: battle.id, 39 | model: winner.model.apiName, 40 | code: winner.code, 41 | trimmedCode: winner.trimmedCode, 42 | completionTokens: winner.completionTokens, 43 | totalTime: winner.totalTime, 44 | didWin: true, 45 | }); 46 | } 47 | 48 | for (const loser of losers) { 49 | await db.insert(apps).values({ 50 | battleId: battle.id, 51 | model: loser.model.apiName, 52 | code: loser.code, 53 | trimmedCode: loser.trimmedCode, 54 | completionTokens: loser.completionTokens, 55 | totalTime: loser.totalTime, 56 | didWin: false, 57 | }); 58 | } 59 | 60 | return battle; 61 | } 62 | 63 | async function findOrCreateCreatorCookie() { 64 | const cookieStore = await cookies(); 65 | let creatorId = cookieStore.get("creatorCookie")?.value; 66 | if (!creatorId) { 67 | creatorId = crypto.randomUUID(); 68 | cookieStore.set("creatorCookie", creatorId, { maxAge: 60 * 60 * 24 * 365 }); 69 | } 70 | return creatorId; 71 | } 72 | -------------------------------------------------------------------------------- /src/app/api/generate-app/route.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import Together from "together-ai"; 3 | 4 | const options: ConstructorParameters[0] = {}; 5 | if (process.env.HELICONE_API_KEY) { 6 | options.baseURL = "https://together.helicone.ai/v1"; 7 | options.defaultHeaders = { 8 | "Helicone-Auth": `Bearer ${process.env.HELICONE_API_KEY}`, 9 | "Helicone-Property-appname": "codearena", 10 | }; 11 | } 12 | 13 | const together = new Together(options); 14 | 15 | export async function POST(request: Request) { 16 | const { prompt, model } = await request.json(); 17 | 18 | try { 19 | const res = await together.chat.completions.create({ 20 | model, 21 | messages: [ 22 | { 23 | role: "system", 24 | content: dedent` 25 | You are an expert frontend React engineer who is also a great UI/UX designer. Follow the instructions carefully, I will tip you $1 million if you do a good job: 26 | 27 | - Think carefully step by step. 28 | - Create a React component for whatever the user asked you to create and make sure it can run by itself by using a default export 29 | - Make sure the React app is interactive and functional by creating state when needed and having no required props 30 | - If you use any imports from React like useState or useEffect, make sure to import them directly 31 | - Use TypeScript as the language for the React component 32 | - Use Tailwind classes for styling. DO NOT USE ARBITRARY VALUES (e.g. \`h-[600px]\`). Make sure to use a consistent color palette. 33 | - NEVER import any CSS files like ./App.css 34 | - Use Tailwind margin and padding classes to style the components and ensure the components are spaced out nicely 35 | - Please ONLY return the full React code starting with the imports, nothing else. It's very important for my job that you only return the React code with imports. DO NOT START WITH \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`. 36 | - Do not import any libraries or dependencies other than React 37 | - NO OTHER LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED. 38 | `, 39 | }, 40 | { 41 | role: "user", 42 | content: dedent` 43 | ${prompt} 44 | 45 | Please ONLY return code, NO backticks or language names.`, 46 | }, 47 | ], 48 | temperature: 0.2, 49 | stream: true, 50 | }); 51 | 52 | return new Response(res.toReadableStream()); 53 | } catch (error) { 54 | if (error instanceof Error) { 55 | return new Response(error.message, { status: 500 }); 56 | } else { 57 | return new Response("Unknown error", { status: 500 }); 58 | } 59 | } 60 | } 61 | 62 | export const runtime = "edge"; 63 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nutlope/codearena/46ed246bed4288e3ed4e96a221271cef7a00aa47/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @layer base { 5 | :root { 6 | --background: 0 0% 100%; 7 | --foreground: 224 71.4% 4.1%; 8 | --card: 0 0% 100%; 9 | --card-foreground: 224 71.4% 4.1%; 10 | --popover: 0 0% 100%; 11 | --popover-foreground: 224 71.4% 4.1%; 12 | --primary: 241 99% 63%; 13 | --primary-foreground: 0 0% 100%; 14 | --secondary: 36 33% 97%; 15 | --secondary-foreground: 0 0% 54%; 16 | --muted: 220 14.3% 95.9%; 17 | --muted-foreground: 220 8.9% 46.1%; 18 | --accent: 220 14.3% 95.9%; 19 | --accent-foreground: 220.9 39.3% 11%; 20 | --destructive: 0 84.2% 60.2%; 21 | --destructive-foreground: 210 20% 98%; 22 | --border: 220 13% 91%; 23 | --input: 220 13% 91%; 24 | --ring: 224 71.4% 4.1%; 25 | --chart-1: 12 76% 61%; 26 | --chart-2: 173 58% 39%; 27 | --chart-3: 197 37% 24%; 28 | --chart-4: 43 74% 66%; 29 | --chart-5: 27 87% 67%; 30 | --radius: 0rem; 31 | } 32 | } 33 | @layer base { 34 | * { 35 | @apply min-w-0 border-border; 36 | } 37 | body { 38 | @apply bg-background text-foreground; 39 | } 40 | } 41 | 42 | @keyframes spinner { 43 | from { 44 | opacity: 1; 45 | } 46 | to { 47 | opacity: 0.25; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Logo from "@/components/logo"; 2 | import type { Metadata } from "next"; 3 | import { Tektur, Plus_Jakarta_Sans } from "next/font/google"; 4 | import "./globals.css"; 5 | import { Button } from "@/components/ui/button"; 6 | import Link from "next/link"; 7 | import GithubIcon from "@/components/icons/github"; 8 | import XIcon from "@/components/icons/x"; 9 | import RibbonIcon from "@/components/icons/ribbon"; 10 | import DotsBackground from "@/public/dots-background.png"; 11 | import PlausibleProvider from "next-plausible"; 12 | 13 | const tektur = Tektur({ 14 | weight: ["400", "500", "600", "700", "800", "900"], 15 | subsets: ["latin"], 16 | variable: "--font-tektur", 17 | }); 18 | const plusJakartaSans = Plus_Jakarta_Sans({ 19 | subsets: ["latin"], 20 | display: "swap", 21 | variable: "--font-plus-jakarta-sans", 22 | }); 23 | 24 | const title = "CodeArena – Which LLM codes best?"; 25 | const description = 26 | "Watch AI models compete in real-time & vote on the best one"; 27 | const url = "https://llmcodearena.com/"; 28 | const ogimage = "https://llmcodearena.com/og-image.png"; 29 | const sitename = "llmcodearena.com"; 30 | 31 | export const metadata: Metadata = { 32 | metadataBase: new URL(url), 33 | title, 34 | description, 35 | icons: { 36 | icon: "/favicon.ico", 37 | }, 38 | openGraph: { 39 | images: [ogimage], 40 | title, 41 | description, 42 | url: url, 43 | siteName: sitename, 44 | locale: "en_US", 45 | type: "website", 46 | }, 47 | twitter: { 48 | card: "summary_large_image", 49 | images: [ogimage], 50 | title, 51 | description, 52 | }, 53 | }; 54 | 55 | export default function RootLayout({ 56 | children, 57 | }: Readonly<{ 58 | children: React.ReactNode; 59 | }>) { 60 | return ( 61 | 65 | 66 | 67 | 68 | 69 |
75 | 76 |
77 |
78 | 79 | 80 | 81 | 82 | 91 |
92 |
93 | 94 |
{children}
95 | 96 | 140 | 141 | 142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /src/app/models.tsx: -------------------------------------------------------------------------------- 1 | import LlamaLogo from "@/public/models/llama.png"; 2 | import MixtralLogo from "@/public/models/mixtral.png"; 3 | import QwenLogo from "@/public/models/qwen.png"; 4 | import GemmaLogo from "@/public/models/gemma.png"; 5 | import NemotronLogo from "@/public/models/nemotron.png"; 6 | import NousHermesLogo from "@/public/models/noushermes.webp"; 7 | 8 | export const models = [ 9 | { 10 | label: "Llama 3.3 70B Instruct Turbo", 11 | shortLabel: "Llama 3.3 70B", 12 | organization: "Meta", 13 | logo: LlamaLogo, 14 | apiName: "meta-llama/Llama-3.3-70B-Instruct-Turbo", 15 | }, 16 | { 17 | label: "Llama 3.1 405B Instruct Turbo", 18 | shortLabel: "Llama 3.1 405B", 19 | organization: "Meta", 20 | logo: LlamaLogo, 21 | apiName: "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", 22 | }, 23 | { 24 | label: "Llama 3.1 8B Instruct Turbo", 25 | shortLabel: "Llama 3.1 8B", 26 | organization: "Meta", 27 | logo: LlamaLogo, 28 | apiName: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", 29 | }, 30 | { 31 | label: "Llama 3.1 70B Instruct Turbo", 32 | shortLabel: "Llama 3.1 70B", 33 | organization: "Meta", 34 | logo: LlamaLogo, 35 | apiName: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", 36 | }, 37 | 38 | { 39 | label: "Gemma 2 9B", 40 | shortLabel: "Gemma 2 9B", 41 | organization: "Google", 42 | logo: GemmaLogo, 43 | apiName: "google/gemma-2-9b-it", 44 | }, 45 | { 46 | label: "Gemma 2 27B", 47 | shortLabel: "Gemma 2 27B", 48 | organization: "Google", 49 | logo: GemmaLogo, 50 | apiName: "google/gemma-2-27b-it", 51 | }, 52 | { 53 | label: "Mixtral-8x22B Instruct (141B)", 54 | shortLabel: "Mixtral-8x22B", 55 | organization: "Mistral AI", 56 | logo: MixtralLogo, 57 | apiName: "mistralai/Mixtral-8x22B-Instruct-v0.1", 58 | }, 59 | { 60 | label: "Qwen 2.5 Coder 32B Instruct", 61 | shortLabel: "Qwen 2.5 Coder 32B", 62 | organization: "Qwen", 63 | logo: QwenLogo, 64 | apiName: "Qwen/Qwen2.5-Coder-32B-Instruct", 65 | }, 66 | { 67 | label: "Qwen 2.5 72B Instruct Turbo", 68 | shortLabel: "Qwen 2.5 72B", 69 | organization: "Qwen", 70 | logo: QwenLogo, 71 | apiName: "Qwen/Qwen2.5-72B-Instruct-Turbo", 72 | }, 73 | { 74 | label: "Llama 3.1 Nemotron 70B", 75 | shortLabel: "Nemotron 70B", 76 | organization: "NVIDIA", 77 | logo: NemotronLogo, 78 | apiName: "nvidia/Llama-3.1-Nemotron-70B-Instruct-HF", 79 | }, 80 | { 81 | label: "Nous Hermes 2 Mixtral 8x7B-DPO (46.7B)", 82 | shortLabel: "Nous Hermes 2 – Mixtral 8x7B-DPO", 83 | organization: "Nous Research", 84 | logo: NousHermesLogo, 85 | apiName: "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", 86 | }, 87 | ]; 88 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import RibbonIcon from "@/components/icons/ribbon"; 4 | import SwordsIcon from "@/components/icons/swords"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 7 | import modelBackgroundImage from "@/public/model-background.png"; 8 | import { Battle } from "@/schema"; 9 | import { 10 | SandpackCodeEditor, 11 | SandpackPreview, 12 | SandpackProvider, 13 | } from "@codesandbox/sandpack-react"; 14 | import { dracula } from "@codesandbox/sandpack-themes"; 15 | import assert from "assert"; 16 | import Image from "next/image"; 17 | import Link from "next/link"; 18 | import party from "party-js"; 19 | import { 20 | FormEvent, 21 | ReactNode, 22 | Ref, 23 | useActionState, 24 | useEffect, 25 | useRef, 26 | useState, 27 | } from "react"; 28 | import { ChatCompletionStream } from "together-ai/lib/ChatCompletionStream.mjs"; 29 | import { z } from "zod"; 30 | import saveBattle from "./actions"; 31 | import { models } from "./models"; 32 | 33 | type App = { 34 | clientId: string; 35 | model: (typeof models)[number]; 36 | isLoading: boolean; 37 | code: string; 38 | trimmedCode: string; 39 | status: "idle" | "generating" | "complete"; 40 | completionTokens?: number; 41 | totalTime?: number; 42 | }; 43 | 44 | export default function Home() { 45 | const [status, setStatus] = useState<"idle" | "generating" | "complete">( 46 | "idle", 47 | ); 48 | const [prompt, setPrompt] = useState(""); 49 | const [submittedPrompt, setSubmittedPrompt] = useState(""); 50 | const [appA, setAppA] = useState(); 51 | const [appB, setAppB] = useState(); 52 | const [selectedTabA, setSelectedTabA] = useState("code"); 53 | const [selectedTabB, setSelectedTabB] = useState("code"); 54 | 55 | async function handleSubmit(event: FormEvent) { 56 | event.preventDefault(); 57 | 58 | setSelectedTabA("code"); 59 | setSelectedTabB("code"); 60 | 61 | const formData = new FormData(event.currentTarget); 62 | const prompt = formData.get("prompt"); 63 | const testModel = formData.get("testModel"); 64 | 65 | assert.ok(typeof prompt === "string"); 66 | 67 | setStatus("generating"); 68 | setSubmittedPrompt(prompt); 69 | 70 | let modelA, modelB; 71 | 72 | if (testModel) { 73 | const model = models.find((m) => m.apiName === testModel); 74 | if (!model) { 75 | throw new Error("Test model not found"); 76 | } 77 | modelA = model; 78 | modelB = model; 79 | } else { 80 | [modelA, modelB] = getRandomModels(); 81 | } 82 | 83 | setAppA({ 84 | clientId: crypto.randomUUID(), 85 | code: "", 86 | trimmedCode: "", 87 | model: modelA, 88 | isLoading: true, 89 | status: "generating", 90 | }); 91 | setAppB({ 92 | clientId: crypto.randomUUID(), 93 | code: "", 94 | trimmedCode: "", 95 | model: modelB, 96 | isLoading: true, 97 | status: "generating", 98 | }); 99 | 100 | // create a stream for each model 101 | const startTime = new Date(); 102 | 103 | const [resA, resB] = await Promise.all([ 104 | fetch("/api/generate-app", { 105 | method: "POST", 106 | body: JSON.stringify({ 107 | prompt, 108 | model: modelA.apiName, 109 | }), 110 | }), 111 | fetch("/api/generate-app", { 112 | method: "POST", 113 | body: JSON.stringify({ 114 | prompt, 115 | model: modelB.apiName, 116 | }), 117 | }), 118 | ]); 119 | 120 | if (!resA.body || !resB.body) return; 121 | 122 | let generatingCount = 2; 123 | ChatCompletionStream.fromReadableStream(resA.body) 124 | .on("content", (delta) => 125 | setAppA((app) => { 126 | if (!app) { 127 | console.log("?"); 128 | return undefined; 129 | } 130 | 131 | const code = app.code + delta; 132 | const trimmedCode = trimCode(code); 133 | 134 | return { ...app, code, trimmedCode }; 135 | }), 136 | ) 137 | .on("totalUsage", (usage) => { 138 | setAppA((app) => 139 | app 140 | ? { 141 | ...app, 142 | completionTokens: usage.completion_tokens, 143 | } 144 | : undefined, 145 | ); 146 | }) 147 | .on("end", () => { 148 | setTimeout(() => { 149 | setAppA((app) => 150 | app 151 | ? { 152 | ...app, 153 | status: "complete", 154 | totalTime: new Date().getTime() - startTime.getTime(), 155 | } 156 | : undefined, 157 | ); 158 | setSelectedTabA("preview"); 159 | generatingCount--; 160 | if (generatingCount === 0) { 161 | setStatus("complete"); 162 | } 163 | }, 500); 164 | }); 165 | ChatCompletionStream.fromReadableStream(resB.body) 166 | .on("content", (delta) => 167 | setAppB((app) => { 168 | if (!app) { 169 | console.log("?"); 170 | return undefined; 171 | } 172 | 173 | const code = app.code + delta; 174 | const trimmedCode = trimCode(code); 175 | 176 | return { ...app, code, trimmedCode }; 177 | }), 178 | ) 179 | .on("totalUsage", (usage) => { 180 | setAppB((app) => 181 | app 182 | ? { 183 | ...app, 184 | completionTokens: usage.completion_tokens, 185 | } 186 | : undefined, 187 | ); 188 | }) 189 | .on("end", () => { 190 | setTimeout(() => { 191 | setAppB((app) => 192 | app 193 | ? { 194 | ...app, 195 | status: "complete", 196 | totalTime: new Date().getTime() - startTime.getTime(), 197 | } 198 | : undefined, 199 | ); 200 | setSelectedTabB("preview"); 201 | generatingCount--; 202 | if (generatingCount === 0) { 203 | setStatus("complete"); 204 | } 205 | }, 500); 206 | }); 207 | } 208 | 209 | return ( 210 |
211 |
212 |

213 | Which LLM Codes the Best? 214 |

215 |

216 | Watch AI models compete in real-time, and see who emerges victorious. 217 |

218 |
219 | 220 |
221 |
222 | {/*
223 | 234 |
*/} 235 | 236 |
237 | setPrompt(e.target.value)} 244 | /> 245 | 246 | {/*
247 | 253 |
*/} 254 |
255 | 256 |
257 | {[ 258 | "Quiz app about llamas", 259 | "Hacker news clone", 260 | "Personal finance dashboard", 261 | "Budgeting app tracker", 262 | ].map((example) => ( 263 | 272 | ))} 273 |
274 | 275 |
276 | 283 |
284 |
285 |
286 |
287 |
291 | 297 | 303 |
304 |
305 | 306 | {status === "complete" && appA && appB && ( 307 | { 311 | setStatus("idle"); 312 | setAppA(undefined); 313 | setAppB(undefined); 314 | }} 315 | /> 316 | )} 317 |
318 | ); 319 | } 320 | 321 | function Result({ 322 | app, 323 | selectedTab, 324 | onTabSelect, 325 | placeholder, 326 | }: { 327 | app: App | undefined; 328 | selectedTab: string; 329 | onTabSelect: (v: string) => void; 330 | placeholder: string; 331 | }) { 332 | if (!app) { 333 | return ( 334 |
335 | 341 |
342 |

{placeholder}

343 |
344 |
345 | ); 346 | } 347 | 348 | return ( 349 |
350 | 351 |
352 |

353 | {placeholder} 354 |

355 | 356 | 361 | Preview 362 | 363 | 367 | Code 368 | 369 | 370 |
371 |
372 | 382 | {app.status === "complete" && ( 383 | 388 | 397 | 398 | )} 399 | 404 | 408 | 409 | 410 |
411 |
412 |
413 | ); 414 | } 415 | 416 | const savableAppSchema = z.object({ 417 | model: z.object({ 418 | label: z.string(), 419 | apiName: z.string(), 420 | }), 421 | code: z.string(), 422 | trimmedCode: z.string(), 423 | totalTime: z.number(), 424 | completionTokens: z.number(), 425 | }); 426 | 427 | const saveBattleSchema = z.object({ 428 | prompt: z.string(), 429 | winners: z.array(savableAppSchema), 430 | losers: z.array(savableAppSchema), 431 | }); 432 | 433 | type State = { 434 | didVote: boolean; 435 | battle?: Battle; 436 | winners?: App[]; 437 | }; 438 | 439 | function Vote({ 440 | prompt, 441 | apps, 442 | onLaunchNextBattle, 443 | }: { 444 | prompt: string; 445 | apps: [App, App]; 446 | onLaunchNextBattle: () => void; 447 | }) { 448 | const [appA, appB] = apps; 449 | 450 | const [state, dispatch, isPending] = useActionState< 451 | State, 452 | { winners: App[] } 453 | >( 454 | async (previous, payload) => { 455 | if (previous.didVote) return previous; 456 | 457 | const winners = payload.winners; 458 | const losers = apps.filter( 459 | (app) => !winners.some((winner) => winner.clientId === app.clientId), 460 | ); 461 | 462 | const data = saveBattleSchema.parse({ prompt, winners, losers }); 463 | const battle = await saveBattle(data); 464 | 465 | return { 466 | battle, 467 | didVote: true, 468 | winners, 469 | }; 470 | }, 471 | { didVote: false }, 472 | ); 473 | 474 | return ( 475 |
476 | {!state.didVote ? ( 477 |
478 |

479 | Which one did better? 480 |

481 | 482 |
483 |
484 |
485 | 494 | 503 |
504 | 505 |
506 | 515 | 524 |
525 |
526 |
527 |
528 | ) : ( 529 |
530 |
531 |

532 | app.model.apiName) 536 | .includes(appA.model.apiName) 537 | ? "font-bold text-gray-900" 538 | : "" 539 | } 540 | > 541 | Model A: 542 | {" "} 543 | 544 | {appA.model.shortLabel} 545 | 546 |

547 |

548 | app.model.apiName) 552 | .includes(appB.model.apiName) 553 | ? "font-bold text-gray-900" 554 | : "" 555 | } 556 | > 557 | Model B: 558 | {" "} 559 | 560 | {appB.model.shortLabel} 561 | 562 |

563 |
564 | 565 |
566 |
567 | {(ref) =>
} 568 |
569 | 570 |

571 | {state.winners?.length === 1 ? ( 572 | <>{state.winners[0].model.shortLabel} wins! 573 | ) : state.winners?.length === 2 ? ( 574 | <>Both models won! 575 | ) : ( 576 | <>Neither model won. 577 | )} 578 |

579 | 580 |

Thanks for voting!

581 |

582 | Check out the{" "} 583 | 587 | leaderboard 588 | {" "} 589 | or try again. 590 |

591 |
592 | 593 |
594 | 605 | 613 |
614 |
615 | )} 616 |
617 | ); 618 | } 619 | 620 | function getRandomModels() { 621 | const shuffled = models.sort(() => 0.5 - Math.random()); 622 | return [shuffled[0], shuffled[1]]; 623 | } 624 | 625 | function trimCode(code: string) { 626 | let trimmedCode = code.trim(); 627 | trimmedCode = trimmedCode.split("\n")[0]?.startsWith("```") 628 | ? trimmedCode.split("\n").slice(1).join("\n") 629 | : trimmedCode; 630 | trimmedCode = trimmedCode.split("\n").at(-1)?.startsWith("```") 631 | ? trimmedCode.split("\n").slice(0, -1).join("\n") 632 | : trimmedCode; 633 | 634 | return trimmedCode; 635 | } 636 | 637 | function Confetti({ 638 | children, 639 | }: { 640 | children: (ref: Ref) => ReactNode; 641 | }) { 642 | const ref = useRef(null); 643 | const hasRun = useRef(false); 644 | 645 | useEffect(() => { 646 | if (!ref.current || hasRun.current) return; 647 | 648 | party.confetti(ref.current, { count: 100 }); 649 | hasRun.current = true; 650 | }, []); 651 | 652 | return children(ref); 653 | } 654 | -------------------------------------------------------------------------------- /src/app/top-models/loading.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from "@/components/spinner"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/top-models/page.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/db"; 2 | import { apps, battles } from "@/schema"; 3 | import { count, countDistinct, desc, sql, sum } from "drizzle-orm"; 4 | import { 5 | Table, 6 | TableBody, 7 | TableCell, 8 | TableHead, 9 | TableHeader, 10 | TableRow, 11 | } from "@/components/ui/table"; 12 | import { models } from "../models"; 13 | import Image from "next/image"; 14 | import Link from "next/link"; 15 | import { Button } from "@/components/ui/button"; 16 | import UpRightArrow from "@/components/icons/up-right-arrow"; 17 | 18 | export default async function Page() { 19 | const [{ totalUsers }] = await db 20 | .select({ totalUsers: countDistinct(battles.creatorCookie) }) 21 | .from(battles); 22 | 23 | const battlesCount = await db.$count(battles); 24 | 25 | const results = await db 26 | .select({ 27 | model: apps.model, 28 | wins: sum(sql`CASE WHEN ${apps.didWin} = true THEN 1 ELSE 0 END`).mapWith( 29 | Number, 30 | ), 31 | losses: sum( 32 | sql`CASE WHEN ${apps.didWin} = false THEN 1 ELSE 0 END`, 33 | ).mapWith(Number), 34 | games: count(), 35 | winPercentage: 36 | sql`ROUND(SUM(CASE WHEN ${apps.didWin} = true THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 0)` 37 | .mapWith(Number) 38 | .as("win_percentage"), 39 | }) 40 | .from(apps) 41 | .groupBy(apps.model) 42 | .orderBy(desc(sql`win_percentage`)); 43 | 44 | return ( 45 |
46 |

47 | Leaderboard: Top OSS Coding{" "} 48 | models 49 |

50 | 51 |
52 |
53 |
54 | {results.length} 55 |
56 |
models
57 |
58 | {/*
59 |
60 | {results.reduce((total, row) => total + row.wins, 0)} 61 |
62 |
# of votes
63 |
*/} 64 |
65 |
{battlesCount}
66 |
games/votes
67 |
68 |
69 |
{totalUsers}
70 |
unique users
71 |
72 |
73 | 74 |
75 | {results.map((result, index) => ( 76 | 77 | ))} 78 |
79 | 80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | Model 88 | 89 | 90 | Organization 91 | 92 | 93 | Total games 94 | 95 | 96 | Win % 97 | 98 | 99 | Playground 100 | 101 | 102 | 103 | 104 | {results.map((result, index) => ( 105 | 106 | ))} 107 | 108 |
109 |
110 |
111 |
112 | ); 113 | } 114 | 115 | function ResultRow({ 116 | result, 117 | index, 118 | }: { 119 | result: { 120 | model: string; 121 | wins: number; 122 | losses: number; 123 | games: number; 124 | winPercentage: number; 125 | }; 126 | index: number; 127 | }) { 128 | const model = models.find((m) => m.apiName === result.model); 129 | if (!model) return; 130 | 131 | return ( 132 | 133 | 134 | {index + 1}. 135 | 136 | 137 | 142 | {model.shortLabel} 143 | 144 | 145 | {models.find((m) => m.apiName === result.model)?.organization} 146 | 147 | {result.games} 148 | 149 | {result.winPercentage}% 150 | 151 | 152 | 160 | 161 | 162 | ); 163 | } 164 | 165 | function ResultCard({ 166 | result, 167 | index, 168 | }: { 169 | result: { 170 | model: string; 171 | wins: number; 172 | losses: number; 173 | games: number; 174 | winPercentage: number; 175 | }; 176 | index: number; 177 | }) { 178 | const model = models.find((m) => m.apiName === result.model); 179 | if (!model) return; 180 | 181 | return ( 182 |
183 |
184 | {index + 1}. 185 |
186 |
187 |
188 | 193 |

194 | {model.shortLabel} 195 |

196 |
197 | 198 |
199 |
200 |

Organization:

201 |

{model.organization}

202 |
203 |
204 |

Total Games:

205 |

{result.games}

206 |
207 |
208 |

Win %:

209 |

210 | {result.winPercentage}% 211 |

212 |
213 | 214 |
215 | 224 |
225 |
226 |
227 |
228 | ); 229 | } 230 | 231 | export const dynamic = "force-dynamic"; 232 | -------------------------------------------------------------------------------- /src/components/icons/arrows.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from "react"; 2 | 3 | export default function ArrowsIcon(props: ComponentProps<"svg">) { 4 | return ( 5 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 29 | 30 | 31 | 37 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/icons/github.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from "react"; 2 | 3 | export default function GithubIcon(props: ComponentProps<"svg">) { 4 | return ( 5 | 13 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/icons/ribbon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from "react"; 2 | 3 | export default function RibbonIcon(props: ComponentProps<"svg">) { 4 | return ( 5 | 13 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/icons/swords.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from "react"; 2 | 3 | export default function SwordsIcon(props: ComponentProps<"svg">) { 4 | return ( 5 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 31 | 32 | 33 | 39 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/icons/three-arrows.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from "react"; 2 | 3 | export default function ThreeArrowsIcon(props: ComponentProps<"svg">) { 4 | return ( 5 | 14 | 15 | 16 | 22 | 26 | 27 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/icons/up-right-arrow.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from "react"; 2 | 3 | export default function UpRightArrow(props: ComponentProps<"svg">) { 4 | return ( 5 | 11 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/icons/x.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from "react"; 2 | 3 | export default function XIcon(props: ComponentProps<"svg">) { 4 | return ( 5 | 13 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/spinner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | 5 | export default function Spinner({ 6 | loading = true, 7 | children, 8 | className = "size-4", 9 | }: { 10 | loading?: boolean; 11 | children?: ReactNode; 12 | className?: string; 13 | }) { 14 | if (!loading) return children; 15 | 16 | const spinner = ( 17 | <> 18 | 19 | {Array.from(Array(8).keys()).map((i) => ( 20 | 28 | ))} 29 | 30 | 31 | ); 32 | 33 | if (!children) return spinner; 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | 40 | {spinner} 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )); 17 | Table.displayName = "Table"; 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | TableHeader.displayName = "TableHeader"; 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 32 | )); 33 | TableBody.displayName = "TableBody"; 34 | 35 | const TableFooter = React.forwardRef< 36 | HTMLTableSectionElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 | 40 | )); 41 | TableFooter.displayName = "TableFooter"; 42 | 43 | const TableRow = React.forwardRef< 44 | HTMLTableRowElement, 45 | React.HTMLAttributes 46 | >(({ className, ...props }, ref) => ( 47 | 48 | )); 49 | TableRow.displayName = "TableRow"; 50 | 51 | const TableHead = React.forwardRef< 52 | HTMLTableCellElement, 53 | React.ThHTMLAttributes 54 | >(({ className, ...props }, ref) => ( 55 |
[role=checkbox]]:translate-y-[2px]", 59 | className, 60 | )} 61 | {...props} 62 | /> 63 | )); 64 | TableHead.displayName = "TableHead"; 65 | 66 | const TableCell = React.forwardRef< 67 | HTMLTableCellElement, 68 | React.TdHTMLAttributes 69 | >(({ className, ...props }, ref) => ( 70 | [role=checkbox]]:translate-y-[2px]", 74 | className, 75 | )} 76 | {...props} 77 | /> 78 | )); 79 | TableCell.displayName = "TableCell"; 80 | 81 | const TableCaption = React.forwardRef< 82 | HTMLTableCaptionElement, 83 | React.HTMLAttributes 84 | >(({ className, ...props }, ref) => ( 85 |
90 | )); 91 | TableCaption.displayName = "TableCaption"; 92 | 93 | export { 94 | Table, 95 | TableHeader, 96 | TableBody, 97 | TableFooter, 98 | TableHead, 99 | TableRow, 100 | TableCell, 101 | TableCaption, 102 | }; 103 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/neon-http"; 2 | import { neon } from "@neondatabase/serverless"; 3 | 4 | if (!process.env.DATABASE_URL || typeof process.env.DATABASE_URL !== "string") { 5 | throw new Error("DATABASE_URL is not set"); 6 | } 7 | 8 | const sql = neon(process.env.DATABASE_URL); 9 | 10 | export const db = drizzle({ client: sql }); 11 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/schema.ts: -------------------------------------------------------------------------------- 1 | import { relations } from "drizzle-orm"; 2 | import { 3 | integer, 4 | pgTable, 5 | serial, 6 | text, 7 | boolean, 8 | timestamp, 9 | index, 10 | } from "drizzle-orm/pg-core"; 11 | 12 | const timestamps = { 13 | createdAt: timestamp("created_at").notNull().defaultNow(), 14 | updatedAt: timestamp("updated_at") 15 | .notNull() 16 | .$onUpdate(() => new Date()), 17 | }; 18 | 19 | export const battles = pgTable( 20 | "battles", 21 | { 22 | id: serial("id").primaryKey(), 23 | prompt: text("prompt").notNull(), 24 | creatorCookie: text("creator_cookie_id").notNull(), 25 | ...timestamps, 26 | }, 27 | (table) => { 28 | return { 29 | creatorCookieIdx: index("creator_cookie_idx").on(table.creatorCookie), 30 | }; 31 | }, 32 | ); 33 | 34 | export type Battle = typeof battles.$inferSelect; 35 | 36 | export const battlesRelations = relations(battles, ({ many }) => ({ 37 | apps: many(apps), 38 | })); 39 | 40 | export const apps = pgTable( 41 | "apps", 42 | { 43 | id: serial("id").primaryKey(), 44 | battleId: integer("battle_id") 45 | .notNull() 46 | .references(() => battles.id), 47 | model: text("model").notNull(), 48 | didWin: boolean("did_win").notNull(), 49 | code: text("code").notNull(), 50 | trimmedCode: text("trimmed_code").notNull(), 51 | totalTime: integer("total_time"), 52 | completionTokens: integer("completion_tokens"), 53 | ...timestamps, 54 | }, 55 | (table) => { 56 | return { 57 | modelIdx: index("model_idx").on(table.model), 58 | didWinIdx: index("did_win_idx").on(table.didWin), 59 | }; 60 | }, 61 | ); 62 | 63 | export type App = typeof apps.$inferSelect; 64 | 65 | export const appsRelations = relations(apps, ({ one }) => ({ 66 | battle: one(battles, { 67 | fields: [apps.battleId], 68 | references: [battles.id], 69 | }), 70 | })); 71 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import colors from "tailwindcss/colors"; 3 | import tailwindcssAnimate from "tailwindcss-animate"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | const { gray, blue, ...rest } = colors; 7 | 8 | export default { 9 | darkMode: ["class"], 10 | content: [ 11 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 12 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 13 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 14 | ], 15 | theme: { 16 | colors: { 17 | ...rest, 18 | gray: { 19 | "200": "#FAF8F5", 20 | "300": "#F6F2EC", 21 | "400": "#D9D9D9", 22 | "500": "#898989", 23 | "900": "#181818", 24 | }, 25 | blue: { 26 | "500": "#4845FE", 27 | }, 28 | }, 29 | extend: { 30 | screens: { 31 | "2xl": "1440px", 32 | }, 33 | fontFamily: { 34 | title: ["var(--font-tektur)"], 35 | sans: ["var(--font-plus-jakarta-sans)"], 36 | }, 37 | borderRadius: { 38 | lg: "var(--radius)", 39 | md: "calc(var(--radius) - 2px)", 40 | sm: "calc(var(--radius) - 4px)", 41 | }, 42 | colors: { 43 | background: "hsl(var(--background))", 44 | foreground: "hsl(var(--foreground))", 45 | card: { 46 | DEFAULT: "hsl(var(--card))", 47 | foreground: "hsl(var(--card-foreground))", 48 | }, 49 | popover: { 50 | DEFAULT: "hsl(var(--popover))", 51 | foreground: "hsl(var(--popover-foreground))", 52 | }, 53 | primary: { 54 | DEFAULT: "hsl(var(--primary))", 55 | foreground: "hsl(var(--primary-foreground))", 56 | }, 57 | secondary: { 58 | DEFAULT: "hsl(var(--secondary))", 59 | foreground: "hsl(var(--secondary-foreground))", 60 | }, 61 | muted: { 62 | DEFAULT: "hsl(var(--muted))", 63 | foreground: "hsl(var(--muted-foreground))", 64 | }, 65 | accent: { 66 | DEFAULT: "hsl(var(--accent))", 67 | foreground: "hsl(var(--accent-foreground))", 68 | }, 69 | destructive: { 70 | DEFAULT: "hsl(var(--destructive))", 71 | foreground: "hsl(var(--destructive-foreground))", 72 | }, 73 | border: "hsl(var(--border))", 74 | input: "hsl(var(--input))", 75 | ring: "hsl(var(--ring))", 76 | chart: { 77 | "1": "hsl(var(--chart-1))", 78 | "2": "hsl(var(--chart-2))", 79 | "3": "hsl(var(--chart-3))", 80 | "4": "hsl(var(--chart-4))", 81 | "5": "hsl(var(--chart-5))", 82 | }, 83 | }, 84 | }, 85 | }, 86 | plugins: [tailwindcssAnimate], 87 | } satisfies Config; 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"], 23 | "@/public/*": ["./public/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------