├── src ├── components │ ├── battle-results-table-skeleton.tsx │ ├── provider-badge.tsx │ ├── ui │ │ ├── simple-tooltip.tsx │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── tooltip.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── command.tsx │ │ ├── select.tsx │ │ └── dropdown-menu.tsx │ ├── details │ │ ├── query-item.tsx │ │ ├── header.tsx │ │ ├── query-list.tsx │ │ ├── index.tsx │ │ ├── query-list-sort-select.tsx │ │ ├── query-details.tsx │ │ └── skeleton.tsx │ ├── battle-results.tsx │ ├── database-combobox.tsx │ ├── database-list.tsx │ ├── battle-setup-modal.tsx │ └── database-modal.tsx ├── app │ ├── favicon.ico │ ├── battle │ │ └── [battleId] │ │ │ └── page.tsx │ ├── page.tsx │ ├── api │ │ └── trpc │ │ │ └── [trpc] │ │ │ └── route.ts │ ├── layout.tsx │ ├── providers.tsx │ ├── wrapper.tsx │ └── globals.css ├── api │ ├── services │ │ ├── index.ts │ │ ├── database.ts │ │ └── llm.ts │ ├── trpc │ │ ├── index.ts │ │ ├── routers │ │ │ ├── auth.ts │ │ │ ├── index.ts │ │ │ ├── battle.ts │ │ │ └── database.ts │ │ ├── client.ts │ │ ├── types.ts │ │ ├── context.ts │ │ └── trpc.ts │ ├── db │ │ ├── index.ts │ │ └── schema.ts │ ├── providers │ │ ├── types.ts │ │ ├── algolia.ts │ │ ├── upstash.ts │ │ └── index.ts │ └── schema.sql ├── lib │ ├── dev.ts │ ├── utils.ts │ ├── providers.ts │ └── session-middleware.ts └── hooks │ ├── use-is-admin.ts │ └── use-query-state.tsx ├── migrations ├── 0005_glossy_toxin.sql ├── 0010_plain_nemesis.sql ├── 0004_bouncy_chamber.sql ├── 0011_glorious_gargoyle.sql ├── 0003_thankful_manta.sql ├── 0002_quiet_solo.sql ├── 0009_fresh_slayback.sql ├── 0008_colorful_layla_miller.sql ├── 0007_puzzling_boom_boom.sql ├── 0006_graceful_nightmare.sql ├── 0001_nasty_marvel_boy.sql ├── meta │ ├── _journal.json │ ├── 0002_snapshot.json │ ├── 0004_snapshot.json │ └── 0003_snapshot.json └── 0000_remarkable_jazinda.sql ├── postcss.config.mjs ├── next.config.ts ├── drizzle.config.ts ├── eslint.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── plan.md ├── README.md ├── package.json └── scripts └── migrate-algolia-to-upstash.mts /src/components/battle-results-table-skeleton.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /migrations/0005_glossy_toxin.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "battles" ADD COLUMN "error" text; -------------------------------------------------------------------------------- /migrations/0010_plain_nemesis.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "battle_queries" ADD COLUMN "error" text; -------------------------------------------------------------------------------- /migrations/0004_bouncy_chamber.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "battles" ALTER COLUMN "queries" DROP DEFAULT; -------------------------------------------------------------------------------- /migrations/0011_glorious_gargoyle.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "search_results" ADD COLUMN "metadata" jsonb; -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/search-arena/main/src/app/favicon.ico -------------------------------------------------------------------------------- /migrations/0003_thankful_manta.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "battles" ADD COLUMN "queries" text DEFAULT '' NOT NULL; -------------------------------------------------------------------------------- /src/api/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./battle"; 2 | export * from "./database"; 3 | export * from "./llm"; 4 | -------------------------------------------------------------------------------- /src/lib/dev.ts: -------------------------------------------------------------------------------- 1 | export const isDev = 2 | process.env.IS_DEV === "true" || process.env.NODE_ENV === "development"; 3 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /src/api/trpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./context"; 2 | export * from "./trpc"; 3 | export * from "./routers"; 4 | export * from "./types"; 5 | -------------------------------------------------------------------------------- /migrations/0002_quiet_solo.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "database_credentials" CASCADE;--> statement-breakpoint 2 | ALTER TABLE "databases" ADD COLUMN "credentials" text NOT NULL; -------------------------------------------------------------------------------- /migrations/0009_fresh_slayback.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "battles" ADD COLUMN "is_demo" boolean DEFAULT false;--> statement-breakpoint 2 | CREATE INDEX "is_demo_idx" ON "battles" USING btree ("is_demo"); -------------------------------------------------------------------------------- /migrations/0008_colorful_layla_miller.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "battles" ADD COLUMN "session_id" varchar(255);--> statement-breakpoint 2 | CREATE INDEX "session_id_idx" ON "battles" USING btree ("session_id"); -------------------------------------------------------------------------------- /migrations/0007_puzzling_boom_boom.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "search_results" ADD COLUMN "search_duration" numeric(8, 2);--> statement-breakpoint 2 | ALTER TABLE "search_results" ADD COLUMN "llm_duration" numeric(8, 2); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | experimental: { 5 | viewTransition: true, 6 | }, 7 | }; 8 | 9 | export default nextConfig; 10 | -------------------------------------------------------------------------------- /src/api/trpc/routers/auth.ts: -------------------------------------------------------------------------------- 1 | import { publicProcedure, router } from "../trpc"; 2 | 3 | export const authRouter = router({ 4 | isAdmin: publicProcedure.query(async ({ ctx }) => { 5 | return ctx.isAdmin; 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /src/hooks/use-is-admin.ts: -------------------------------------------------------------------------------- 1 | import { trpc } from "@/api/trpc/client"; 2 | 3 | export const useIsAdmin = () => { 4 | const { data } = trpc.auth.isAdmin.useQuery(); 5 | 6 | return { 7 | isAdmin: data, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/trpc/client.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from "@trpc/react-query"; 2 | import { type AppRouter } from "./routers"; 3 | 4 | /** 5 | * Create a tRPC client for React components 6 | */ 7 | export const trpc = createTRPCReact(); 8 | -------------------------------------------------------------------------------- /src/app/battle/[battleId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useParams } from "next/navigation"; 4 | import { BattleDetails } from "@/components/details"; 5 | 6 | export default function BattlePage() { 7 | const { battleId } = useParams(); 8 | 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | config({ path: ".env" }); 5 | export default defineConfig({ 6 | schema: "./src/api/db/schema.ts", 7 | out: "./migrations", 8 | dialect: "postgresql", 9 | dbCredentials: { 10 | url: process.env.DATABASE_URL!, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /migrations/0006_graceful_nightmare.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."battle_status" AS ENUM('pending', 'in_progress', 'completed', 'failed');--> statement-breakpoint 2 | ALTER TABLE "battles" ALTER COLUMN "status" SET DEFAULT 'pending'::"public"."battle_status";--> statement-breakpoint 3 | ALTER TABLE "battles" ALTER COLUMN "status" SET DATA TYPE "public"."battle_status" USING "status"::"public"."battle_status"; -------------------------------------------------------------------------------- /src/api/trpc/routers/index.ts: -------------------------------------------------------------------------------- 1 | import { router } from "../trpc"; 2 | import { databaseRouter } from "./database"; 3 | import { battleRouter } from "./battle"; 4 | import { authRouter } from "./auth"; 5 | 6 | // Root router 7 | export const appRouter = router({ 8 | database: databaseRouter, 9 | battle: battleRouter, 10 | auth: authRouter, 11 | }); 12 | 13 | // Export type definition of API 14 | export type AppRouter = typeof appRouter; 15 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /src/components/provider-badge.tsx: -------------------------------------------------------------------------------- 1 | import { Provider, PROVIDERS } from "@/lib/providers"; 2 | import { Badge } from "./ui/badge"; 3 | 4 | export const ProviderBadge = ({ provider }: { provider?: Provider }) => { 5 | if (!provider) { 6 | return null; 7 | } 8 | 9 | const { name, color } = PROVIDERS[provider]; 10 | 11 | return ( 12 | 17 | {name} 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DatabaseList } from "@/components/database-list"; 4 | import { BattleResults } from "@/components/battle-results"; 5 | import { useIsAdmin } from "@/hooks/use-is-admin"; 6 | 7 | export default function Page() { 8 | const { isAdmin } = useIsAdmin(); 9 | return ( 10 |
11 | {isAdmin && } 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /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": "src/app/globals.css", 9 | "baseColor": "neutral", 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 | } -------------------------------------------------------------------------------- /src/components/ui/simple-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Tooltip, 3 | TooltipContent, 4 | TooltipTrigger, 5 | } from "@/components/ui/tooltip"; 6 | 7 | export function SimpleTooltip({ 8 | children, 9 | content, 10 | }: { 11 | children: React.ReactNode; 12 | content?: React.ReactNode; 13 | }) { 14 | if (!content) return children; 15 | 16 | return ( 17 | 18 | {children} 19 | 20 |

{content}

21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /migrations/0001_nasty_marvel_boy.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "database_credentials" ADD COLUMN "env_file" text NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "database_credentials" DROP COLUMN "algolia_application_id";--> statement-breakpoint 3 | ALTER TABLE "database_credentials" DROP COLUMN "algolia_api_key";--> statement-breakpoint 4 | ALTER TABLE "database_credentials" DROP COLUMN "algolia_index";--> statement-breakpoint 5 | ALTER TABLE "database_credentials" DROP COLUMN "upstash_url";--> statement-breakpoint 6 | ALTER TABLE "database_credentials" DROP COLUMN "upstash_token";--> statement-breakpoint 7 | ALTER TABLE "database_credentials" DROP COLUMN "upstash_index"; -------------------------------------------------------------------------------- /.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 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /src/api/trpc/types.ts: -------------------------------------------------------------------------------- 1 | import { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 2 | import { AppRouter } from "."; 3 | 4 | export type RouterInput = inferRouterInputs; 5 | export type RouterOutput = inferRouterOutputs; 6 | 7 | export type Database = RouterOutput["database"]["getAll"][number]; 8 | export type NewDatabase = RouterInput["database"]["create"]; 9 | 10 | export type NewBattle = RouterInput["battle"]["create"]; 11 | export type BattleResult = RouterOutput["battle"]["getAll"][number]; 12 | 13 | export type BattleDetails = RouterOutput["battle"]["getById"]; 14 | 15 | export type BattleQuery = BattleDetails["queries"][number]; 16 | -------------------------------------------------------------------------------- /src/api/trpc/context.ts: -------------------------------------------------------------------------------- 1 | import { BattleService, DatabaseService } from "../services"; 2 | import { getOrCreateSessionId } from "../../lib/session-middleware"; 3 | import { isDev } from "@/lib/dev"; 4 | import { ipAddress } from "@vercel/functions"; 5 | 6 | /** 7 | * Create context for the tRPC API 8 | */ 9 | export async function createTRPCContext(req: Request) { 10 | const sessionId = await getOrCreateSessionId(); 11 | 12 | return { 13 | databaseService: new DatabaseService(), 14 | battleService: new BattleService(), 15 | sessionId, 16 | isAdmin: isDev, 17 | ip: ipAddress(req), 18 | }; 19 | } 20 | 21 | export type TRPCContext = Awaited>; 22 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner, ToasterProps } from "sonner"; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme(); 8 | 9 | return ( 10 | 22 | ); 23 | }; 24 | 25 | export { Toaster }; 26 | -------------------------------------------------------------------------------- /src/lib/providers.ts: -------------------------------------------------------------------------------- 1 | import colors from "tailwindcss/colors"; 2 | 3 | export type Provider = keyof typeof PROVIDERS; 4 | 5 | export const PROVIDERS = { 6 | upstash_search: { 7 | name: "Upstash", 8 | color: colors.emerald, 9 | env: ` 10 | UPSTASH_URL=https://your-database.upstash.io 11 | UPSTASH_TOKEN=your-rest-token 12 | UPSTASH_INDEX=your-index-name 13 | 14 | UPSTASH_RERANKING=true 15 | UPSTASH_INPUT_ENRICHMENT=true 16 | UPSTASH_TOPK=10 17 | UPSTASH_SEMANTIC_WEIGHT=0.75 18 | `, 19 | }, 20 | algolia: { 21 | name: "Algolia", 22 | color: colors.blue, 23 | env: ` 24 | ALGOLIA_APPLICATION_ID=your-app-id 25 | ALGOLIA_API_KEY=your-api-key 26 | ALGOLIA_INDEX=your-index-name 27 | `, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/session-middleware.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | 4 | export const SESSION_ID_COOKIE = "search-arena-session-id"; 5 | 6 | /** 7 | * Gets the current session ID from cookies or creates a new one 8 | */ 9 | export async function getOrCreateSessionId(): Promise { 10 | const cookieStore = await cookies(); 11 | const sessionCookie = cookieStore.get(SESSION_ID_COOKIE); 12 | 13 | if (sessionCookie?.value) return sessionCookie.value; 14 | 15 | const sessionId = uuidv4(); 16 | 17 | cookieStore.set(SESSION_ID_COOKIE, sessionId, { 18 | httpOnly: true, 19 | sameSite: "strict", 20 | path: "/", 21 | }); 22 | 23 | return sessionId; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /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 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "scripts/migrate-algolia-to-upstash.mts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/api/db/index.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless"; 2 | import { drizzle } from "drizzle-orm/neon-http"; 3 | import * as schema from "./schema"; 4 | 5 | // Get the database URL from environment variables 6 | const databaseUrl = process.env.DATABASE_URL; 7 | 8 | // Create a database connection 9 | export const createConnection = () => { 10 | if (!databaseUrl) { 11 | throw new Error("DATABASE_URL environment variable is not set"); 12 | } 13 | 14 | const sql = neon(databaseUrl); 15 | // Include schema and relations for proper type inference 16 | return drizzle(sql, { schema }); 17 | }; 18 | 19 | // Export a singleton instance for use throughout the application 20 | export const db = createConnection(); 21 | 22 | // Export schema for use in other files 23 | export { schema }; 24 | -------------------------------------------------------------------------------- /src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | import { appRouter, createTRPCContext } from "@/api/trpc"; 3 | 4 | export const runtime = "edge"; 5 | 6 | /** 7 | * Handle tRPC requests 8 | */ 9 | async function handler(req: Request) { 10 | return fetchRequestHandler({ 11 | endpoint: "/api/trpc", 12 | req, 13 | router: appRouter, 14 | createContext: async () => await createTRPCContext(req), 15 | onError: 16 | process.env.NODE_ENV === "development" 17 | ? ({ path, error }) => { 18 | console.error( 19 | `❌ tRPC error on ${path ?? ""}: ${error.message}` 20 | ); 21 | } 22 | : undefined, 23 | }); 24 | } 25 | 26 | export { handler as GET, handler as POST }; 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |