[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "README.md",
    "content": "# YC idea matcher\n\n![Screenshot of the app UI](ui.png)\n\nThis project allows you Submit your idea and get a list of similar ideas that YCombinator has invested in before.\n\nThe project is built using the following technologies:\n\n- [Neon](https://neon.tech): Serverless Postgres\n- [pgvector](https://github.com/pgvector/pgvector): open-source Postgres extension for vector storage and similarity search\n- [Neon Serverless Driver](https://github.com/neondatabase/serverless)\n- [Next.js](https://nextjs.org): Fullstack framework for React\n- [Vercel](https://vercel.com): deployment platform\n- [OpenAI API](https://openai.com): generating vector embeddings\n- [TailwindCSS](https://tailwindcss.com): Utility-first CSS framework\n- [Upstash Redis](https://upstash.com): serverless Redis for rate limiting\n- [Zod](https://zod.dev): TypeScript-first schema validation\n- [React Query](https://react-query.tanstack.com): data fetching and caching library\n- [Vaul](https://vaul.emilkowal.ski/): Drawer component for React.\n\n## How the app works\n\nYou will find a script called `generate-embeddings.ts` located in the root directory of this project. After running `npm run generate-embeddings`, the script does the following:\n\n1. It creates the database schema and installs the `pgvector` extension\n2. It goes through the YCombinator API 'https://api.ycombinator.com/v0.1/companies?page=1' and gets all the companies\n3. For each company it generates embeddings using the long description and then stores the company data in the database.\n  \n> Some companies don't have a long description, so we needed to manually remove those from the database by running `delete from companies WHERE embedding = ARRAY[]::real[];`\n\nThe app itself is a Next.js app with an API route located at `/api/idea`. Whenever a user submits an idea, the following happens:\n\n1. The idea is sent to the OpenAI API to generate an embedding\n2. We then use pgvector to retrieve the top 3 most similar ideas\n"
  },
  {
    "path": "generate-embeddings.ts",
    "content": "import { Pool } from 'pg';\nimport axios from 'axios';\n\nconst DATABASE_URL = '';\nconst OPENAI_API_KEY = '';\n\nconst pool = new Pool({\n  connectionString: DATABASE_URL,\n});\n\nasync function createCompaniesTable() {\n  const client = await pool.connect();\n  try {\n    await client.query(`\n      CREATE EXTENSION IF NOT EXISTS vector;\n      CREATE TABLE IF NOT EXISTS companies (\n        id SERIAL PRIMARY KEY,\n        name TEXT,\n        slug TEXT,\n        website TEXT,\n        \"smallLogoUrl\" TEXT,\n        \"oneLiner\" TEXT,\n        \"longDescription\" TEXT,\n        \"teamSize\" INTEGER,\n        url TEXT,\n        batch TEXT,\n        tags TEXT[],\n        status TEXT,\n        industries TEXT[],\n        regions TEXT[],\n        locations TEXT[],\n        badges TEXT[],\n        embedding VECTOR(1536)\n      );\n    `);\n    console.log('Companies table created successfully');\n  } catch (error) {\n    console.error('Error creating companies table:', error);\n  } finally {\n    client.release();\n  }\n}\n\nasync function scrapeCompanies(url: string) {\n  try {\n    const response = await axios.get(url);\n    const { companies, nextPage } = response.data;\n\n    for (const company of companies) {\n      const { longDescription } = company;\n      const embedding = await generateEmbedding(longDescription);\n      await storeCompany(company, embedding);\n    }\n\n    if (nextPage) {\n      await scrapeCompanies(nextPage);\n    }\n  } catch (error) {\n    console.error('Error scraping companies:', error);\n  }\n}\n\nasync function generateEmbedding(text: string): Promise<number[]> {\n  try {\n    const response = await axios.post(\n      'https://api.openai.com/v1/embeddings',\n      {\n        input: text,\n        model: 'text-embedding-ada-002',\n      },\n      {\n        headers: {\n          Authorization: `Bearer ${OPENAI_API_KEY}`,\n          'Content-Type': 'application/json',\n        },\n      }\n    );\n    const { data } = response.data;\n    return data[0].embedding;\n  } catch (error) {\n    console.error('Error generating embedding:', error);\n  }\n\n  return [];\n}\n\nasync function storeCompany(company: any, embedding: number[]) {\n  const {\n    name,\n    slug,\n    website,\n    smallLogoUrl,\n    oneLiner,\n    longDescription,\n    teamSize,\n    url,\n    batch,\n    tags,\n    status,\n    industries,\n    regions,\n    locations,\n    badges,\n  } = company;\n  const client = await pool.connect();\n\n  try {\n    await client.query(\n      `\n      INSERT INTO companies (\n        name, slug, website, \"smallLogoUrl\", \"oneLiner\", \"longDescription\", \"teamSize\", url, batch, tags, status, industries, regions, locations, badges, embedding\n      )\n      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n    `,\n      [\n        name,\n        slug,\n        website,\n        smallLogoUrl,\n        oneLiner,\n        longDescription,\n        teamSize,\n        url,\n        batch,\n        tags,\n        status,\n        industries,\n        regions,\n        locations,\n        badges,\n        embedding,\n      ]\n    );\n\n    console.log(`Company '${name}' stored successfully`);\n  } catch (error) {\n    console.error(`Error storing company '${name}':`, error);\n  } finally {\n    client.release();\n  }\n}\n\nasync function runScript() {\n  await createCompaniesTable();\n  await scrapeCompanies('https://api.ycombinator.com/v0.1/companies?page=1');\n  await pool.end();\n}\n\nrunScript();\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"yc-idea-matcher\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"generate-embeddings\": \"tsx generate-embeddings.ts\"\n  },\n  \"dependencies\": {\n    \"@neondatabase/serverless\": \"^0.5.6\",\n    \"@tanstack/react-query\": \"^4.32.1\",\n    \"@upstash/ratelimit\": \"^0.4.3\",\n    \"@upstash/redis\": \"^1.22.0\",\n    \"@vercel/analytics\": \"^1.0.1\",\n    \"clsx\": \"^2.0.0\",\n    \"next\": \"13.4.12\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-hook-form\": \"^7.45.2\",\n    \"tailwind-merge\": \"^1.14.0\",\n    \"usehooks-ts\": \"^2.9.1\",\n    \"vaul\": \"^0.1.1\",\n    \"zod\": \"^3.21.4\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/forms\": \"^0.5.4\",\n    \"@tailwindcss/typography\": \"^0.5.9\",\n    \"@types/node\": \"20.4.5\",\n    \"@types/pg\": \"^8.10.2\",\n    \"@types/react\": \"18.2.18\",\n    \"@types/react-dom\": \"18.2.7\",\n    \"autoprefixer\": \"10.4.14\",\n    \"axios\": \"^1.4.0\",\n    \"eslint\": \"8.46.0\",\n    \"eslint-config-next\": \"13.4.12\",\n    \"pg\": \"^8.11.2\",\n    \"postcss\": \"8.4.27\",\n    \"tailwindcss\": \"3.3.3\",\n    \"tsx\": \"^3.12.7\",\n    \"typescript\": \"5.1.6\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "src/app/api/idea/route.ts",
    "content": "import { Ratelimit } from '@upstash/ratelimit';\nimport { Redis } from '@upstash/redis';\nimport { neon, neonConfig } from '@neondatabase/serverless';\nimport { NextRequest, NextResponse } from 'next/server';\nimport { generateEmbeddings } from '~/utils';\nimport { z } from 'zod';\n\nneonConfig.fetchConnectionCache = true;\n\nexport const runtime = 'edge';\nexport const preferredRegion = 'iad1';\n\nconst ratelimit = new Ratelimit({\n  redis: Redis.fromEnv(),\n  analytics: true,\n  limiter: Ratelimit.slidingWindow(2, '5s'),\n});\n\nconst sql = neon(process.env.DATABASE_URL!);\n\nexport async function POST(request: NextRequest) {\n  const id = request.ip ?? 'anonymous';\n  const limit = await ratelimit.limit(id ?? 'anonymous');\n\n  if (!limit.success) {\n    return NextResponse.json({ error: 'Too many requests' }, { status: 429 });\n  }\n\n  const body = await request.json();\n\n  const schema = z.object({\n    idea: z\n      .string()\n      .min(20, {\n        message: 'Idea must be at least 20 characters.',\n      })\n      .max(140, {\n        message: 'Idea should be at most 140 characters.',\n      }),\n  });\n\n  const validated = schema.safeParse(body);\n\n  if (!validated.success) {\n    return NextResponse.json(\n      { error: `Invalid request ${validated.error.message}` },\n      { status: 400 }\n    );\n  }\n\n  const { idea } = validated.data;\n\n  try {\n    const embedding = await generateEmbeddings(idea);\n\n    const result = await sql(\n      `SELECT id, name, \"smallLogoUrl\", website, \"oneLiner\", \"longDescription\", batch, url, status, industries FROM companies ORDER BY embedding::VECTOR <=> '[${embedding}]' LIMIT 5;`\n    );\n\n    return NextResponse.json({ data: result });\n  } catch (error) {\n    return NextResponse.json({ error: error }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import '../styles/globals.css';\nimport type { Metadata } from 'next';\nimport { Inter } from 'next/font/google';\nimport Link from 'next/link';\nimport Providers from '~/components/providers';\nimport { cn } from '~/utils';\n\nconst inter = Inter({ subsets: ['latin'] });\n\nexport const metadata: Metadata = {\n  title: 'YC Idea Matcher',\n  description:\n    'Submit your idea and get a list of similar ideas that YCombinator has invested in in the past.',\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html\n      lang=\"en\"\n      className={cn(\n        'bg-[#080808] text-gray-1100 selection:text-gray-1200 selection:bg-green-800 flex flex-col min-h-[95vh]',\n        inter.className\n      )}\n    >\n      <Providers>\n        <body className=\"flex-grow\">{children}</body>\n        <footer className=\"my-5\">\n          <p className=\"text-center text-sm\">\n            Powered by{' '}\n            <a\n              className=\"focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\n            transition-colors outline-none rounded-md text-gray-1200 hover:underline\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              href=\"https://bit.ly/3OFaSTp\"\n            >\n              Neon\n            </a>{' '}\n            and{' '}\n            <a\n              className=\"focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\n            transition-colors outline-none rounded-md text-gray-1200 hover:underline\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              href=\"https://github.com/neondatabase/yc-idea-matcher\"\n            >\n              pgvector\n            </a>\n          </p>\n        </footer>\n      </Providers>\n    </html>\n  );\n}\n"
  },
  {
    "path": "src/app/page.tsx",
    "content": "'use client';\nimport { useMutation } from '@tanstack/react-query';\nimport { useForm } from 'react-hook-form';\nimport { Company } from '~/components/company';\nimport { Hero } from '~/components/hero';\nimport { Button } from '~/components/ui/button';\nimport { Input } from '~/components/ui/input';\nimport va from '@vercel/analytics';\n\ntype FormValues = {\n  idea: string;\n};\n\nexport default function Home() {\n  const {\n    handleSubmit,\n    register,\n    formState: { errors },\n  } = useForm<FormValues>();\n\n  const { mutate, isLoading, data } = useMutation(\n    async (values: FormValues) => {\n      va.track('Submit');\n      const res = await fetch('/api/idea', {\n        method: 'POST',\n        body: JSON.stringify(values),\n      });\n\n      if (!res.ok) {\n        throw new Error('Something went wrong...');\n      }\n\n      const data = await res.json();\n\n      return data;\n    }\n  );\n\n  const onSubmit = (values: FormValues) => mutate(values);\n\n  return (\n    <main className=\"relative\">\n      <a\n        className=\"focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\n            transition-colors outline-none rounded-md text-gray-1200 hover:underline p-1\"\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        href=\"https://bit.ly/3OFaSTp\"\n      >\n        <svg\n          className=\"w-7 h-7 absolute top-10 left-10  lg:left-40\"\n          width=\"57\"\n          height=\"56\"\n          viewBox=\"0 0 57 56\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <path\n            fill-rule=\"evenodd\"\n            clip-rule=\"evenodd\"\n            d=\"M0 9.63547C0 4.31395 4.32472 0 9.65953 0H46.3657C51.7006 0 56.0253 4.31395 56.0253 9.63547V40.7763C56.0253 46.2817 49.0411 48.671 45.6538 44.3245L35.0641 30.7359V47.2138C35.0641 52.0032 31.1719 55.8857 26.3705 55.8857H9.65953C4.32472 55.8857 0 51.5718 0 46.2502V9.63547ZM9.65953 7.70837C8.59257 7.70837 7.72762 8.57116 7.72762 9.63547V46.2502C7.72762 47.3145 8.59257 48.1773 9.65953 48.1773H26.6603C27.1938 48.1773 27.3365 47.7459 27.3365 47.2138V25.117C27.3365 19.6117 34.3206 17.2223 37.708 21.5688L48.2976 35.1574V9.63547C48.2976 8.57116 48.3987 7.70837 47.3317 7.70837H9.65953Z\"\n            fill=\"#12FFF7\"\n          />\n          <path\n            fill-rule=\"evenodd\"\n            clip-rule=\"evenodd\"\n            d=\"M0 9.63547C0 4.31395 4.32472 0 9.65953 0H46.3657C51.7006 0 56.0253 4.31395 56.0253 9.63547V40.7763C56.0253 46.2817 49.0411 48.671 45.6538 44.3245L35.0641 30.7359V47.2138C35.0641 52.0032 31.1719 55.8857 26.3705 55.8857H9.65953C4.32472 55.8857 0 51.5718 0 46.2502V9.63547ZM9.65953 7.70837C8.59257 7.70837 7.72762 8.57116 7.72762 9.63547V46.2502C7.72762 47.3145 8.59257 48.1773 9.65953 48.1773H26.6603C27.1938 48.1773 27.3365 47.7459 27.3365 47.2138V25.117C27.3365 19.6117 34.3206 17.2223 37.708 21.5688L48.2976 35.1574V9.63547C48.2976 8.57116 48.3987 7.70837 47.3317 7.70837H9.65953Z\"\n            fill=\"url(#paint0_linear_9_7220)\"\n          />\n          <path\n            fill-rule=\"evenodd\"\n            clip-rule=\"evenodd\"\n            d=\"M0 9.63547C0 4.31395 4.32472 0 9.65953 0H46.3657C51.7006 0 56.0253 4.31395 56.0253 9.63547V40.7763C56.0253 46.2817 49.0411 48.671 45.6538 44.3245L35.0641 30.7359V47.2138C35.0641 52.0032 31.1719 55.8857 26.3705 55.8857H9.65953C4.32472 55.8857 0 51.5718 0 46.2502V9.63547ZM9.65953 7.70837C8.59257 7.70837 7.72762 8.57116 7.72762 9.63547V46.2502C7.72762 47.3145 8.59257 48.1773 9.65953 48.1773H26.6603C27.1938 48.1773 27.3365 47.7459 27.3365 47.2138V25.117C27.3365 19.6117 34.3206 17.2223 37.708 21.5688L48.2976 35.1574V9.63547C48.2976 8.57116 48.3987 7.70837 47.3317 7.70837H9.65953Z\"\n            fill=\"url(#paint1_linear_9_7220)\"\n          />\n          <path\n            d=\"M46.3661 0C51.7009 0 56.0256 4.31395 56.0256 9.63547V40.7763C56.0256 46.2817 49.0414 48.671 45.6541 44.3245L35.0644 30.736V47.2138C35.0644 52.0032 31.1722 55.8857 26.3708 55.8857C26.9043 55.8857 27.3368 55.4543 27.3368 54.9222V25.117C27.3368 19.6117 34.3209 17.2223 37.7083 21.5688L48.298 35.1574V1.92709C48.298 0.862789 47.433 0 46.3661 0Z\"\n            fill=\"#B9FFB3\"\n          />\n          <defs>\n            <linearGradient\n              id=\"paint0_linear_9_7220\"\n              x1=\"56.0253\"\n              y1=\"55.8857\"\n              x2=\"6.90024\"\n              y2=\"-0.1215\"\n              gradientUnits=\"userSpaceOnUse\"\n            >\n              <stop stop-color=\"#B9FFB3\" />\n              <stop offset=\"1\" stop-color=\"#B9FFB3\" stop-opacity=\"0\" />\n            </linearGradient>\n            <linearGradient\n              id=\"paint1_linear_9_7220\"\n              x1=\"56.0253\"\n              y1=\"55.8857\"\n              x2=\"22.77\"\n              y2=\"42.9181\"\n              gradientUnits=\"userSpaceOnUse\"\n            >\n              <stop stop-color=\"#1A1A1A\" stop-opacity=\"0.9\" />\n              <stop offset=\"1\" stop-color=\"#1A1A1A\" stop-opacity=\"0\" />\n            </linearGradient>\n          </defs>\n        </svg>\n      </a>{' '}\n      <div className=\"px-6 my-36 max-w-6xl mx-auto\">\n        <Hero />\n        <form\n          className=\"flex md:flex-row flex-col space-y-5 md:space-y-0 md:items-center justify-center md:space-x-2\"\n          onSubmit={handleSubmit(onSubmit)}\n        >\n          <Input\n            placeholder=\"Describe your next big idea\"\n            id=\"idea\"\n            {...register('idea', {\n              required: true,\n              maxLength: {\n                message: 'Max length exceeded',\n                value: 140,\n              },\n              minLength: {\n                message: 'Your idea should be at least 20 characters long',\n                value: 20,\n              },\n            })}\n          />\n\n          <Button\n            className=\"justify-center \"\n            loading={isLoading}\n            size=\"large\"\n            type=\"submit\"\n          >\n            Submit\n          </Button>\n        </form>\n        {errors && (\n          <p className=\"text-red-1100 mt-2 text-sm\">{errors.idea?.message}</p>\n        )}\n        <div className=\"mt-10 mb-16\">\n          <div className=\"grid grid-cols-1 gap-10\">\n            {isLoading ? (\n              <>\n                <div className=\"p-5 animate-pulse h-80 rounded-md bg-gray-500\" />\n                <div className=\"p-5 animate-pulse h-80 rounded-md bg-gray-500\" />{' '}\n                <div className=\"p-5 animate-pulse h-80 rounded-md bg-gray-500\" />\n              </>\n            ) : (\n              data &&\n              data.data.map((company: Company) => (\n                <Company key={company.id} {...company} />\n              ))\n            )}\n          </div>\n        </div>\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "src/components/badge.tsx",
    "content": "import { cn } from '~/utils';\n\nexport type Status = 'Public' | 'Active' | 'Acquired' | 'Inactive';\n\ntype Props = {\n  status: Status;\n};\nexport const Badge = ({ status }: Props) => {\n  const styles: Record<Status, { text: string; className: string }> = {\n    Public: {\n      text: 'Public',\n      className: 'text-indigo-1100 border-indigo-700',\n    },\n    Acquired: {\n      text: 'Acquired',\n      className: 'border-orange-700 text-orange-1100',\n    },\n    Active: {\n      text: 'Active',\n      className: 'border-green-700 text-green-1100',\n    },\n    Inactive: {\n      text: 'Inactive',\n      className: 'border-gray-700 text-gray-1100',\n    },\n  };\n\n  return (\n    <span\n      className={cn(\n        'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium',\n        styles[status].className\n      )}\n    >\n      {styles[status].text}\n      <svg\n        className=\"-mr-0.5 ml-1.5 h-2 w-2 \"\n        fill=\"currentColor\"\n        viewBox=\"0 0 8 8\"\n      >\n        <circle cx={4} cy={4} r={3} />\n      </svg>\n    </span>\n  );\n};\n"
  },
  {
    "path": "src/components/company.tsx",
    "content": "import { Badge, Status } from './badge';\n\nexport type Company = {\n  id: number;\n  name: string;\n  slug: string;\n  website: string;\n  smallLogoUrl?: string;\n  oneLiner: string;\n  longDescription: string;\n  teamSize: number;\n  url: string;\n  batch: string;\n  tags: string[];\n  status: Status;\n  industries: string[];\n  badges: string[];\n};\n\nexport const Company = ({\n  id,\n  website,\n  smallLogoUrl,\n  name,\n  oneLiner,\n  longDescription,\n  industries,\n  batch,\n  status,\n}: Company) => {\n  return (\n    <a\n      key={id}\n      className=\"outline-none p-5 md:p-8 hover:bg-gray-400 rounded-md space-y-3 focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100 transition-colors hover:border-green-800\"\n      href={website}\n    >\n      <div className=\"flex flex-col md:flex-row gap-10\">\n        {smallLogoUrl ? (\n          <img\n            className=\"w-16 h-16 rounded-md\"\n            src={smallLogoUrl}\n            alt=\"Company logo\"\n          />\n        ) : (\n          <img\n            className=\"w-16 h-16 rounded-md\"\n            src={`https://placehold.co/300x300/3a3f42/FFF?text=404`}\n            alt=\"Company logo\"\n          />\n        )}\n\n        <div className=\"space-y-3\">\n          <h2 className=\"text-xl md:text-2xl font-semibold text-gray-1200\">\n            {name}\n          </h2>\n          <h3 className=\"text-gray-1200\">{oneLiner}</h3>\n          <p>{longDescription}</p>\n          <p className=\"text-sm\">{batch} batch</p>\n          <div className=\"flex flex-wrap gap-3 text-sm\">\n            {industries.map((industry) => (\n              <p\n                className=\"inline-flex items-center rounded-md border-gray-700 text-gray-1100 border px-2.5 py-1 text-xs font-medium\"\n                key={industry}\n              >\n                {industry}\n              </p>\n            ))}\n          </div>\n          <Badge status={status} />\n        </div>\n      </div>\n    </a>\n  );\n};\n"
  },
  {
    "path": "src/components/hero.tsx",
    "content": "import React from 'react';\nimport { HowItWorks } from './how-it-works';\n\nexport const Hero = () => {\n  return (\n    <>\n      {' '}\n      <h1 className=\"text-3xl flex items-center justify-center md:text-5xl text-center font-semibold text-gray-1200 mb-5\">\n        YC Idea Matcher{' '}\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          viewBox=\"0 0 24 24\"\n          fill=\"#FFC000\"\n          className=\"w-10 h-10 ml-2\"\n        >\n          <path\n            fillRule=\"evenodd\"\n            d=\"M9 4.5a.75.75 0 01.721.544l.813 2.846a3.75 3.75 0 002.576 2.576l2.846.813a.75.75 0 010 1.442l-2.846.813a3.75 3.75 0 00-2.576 2.576l-.813 2.846a.75.75 0 01-1.442 0l-.813-2.846a3.75 3.75 0 00-2.576-2.576l-2.846-.813a.75.75 0 010-1.442l2.846-.813A3.75 3.75 0 007.466 7.89l.813-2.846A.75.75 0 019 4.5zM18 1.5a.75.75 0 01.728.568l.258 1.036c.236.94.97 1.674 1.91 1.91l1.036.258a.75.75 0 010 1.456l-1.036.258c-.94.236-1.674.97-1.91 1.91l-.258 1.036a.75.75 0 01-1.456 0l-.258-1.036a2.625 2.625 0 00-1.91-1.91l-1.036-.258a.75.75 0 010-1.456l1.036-.258a2.625 2.625 0 001.91-1.91l.258-1.036A.75.75 0 0118 1.5zM16.5 15a.75.75 0 01.712.513l.394 1.183c.15.447.5.799.948.948l1.183.395a.75.75 0 010 1.422l-1.183.395c-.447.15-.799.5-.948.948l-.395 1.183a.75.75 0 01-1.422 0l-.395-1.183a1.5 1.5 0 00-.948-.948l-1.183-.395a.75.75 0 010-1.422l1.183-.395c.447-.15.799-.5.948-.948l.395-1.183A.75.75 0 0116.5 15z\"\n            clipRule=\"evenodd\"\n          />\n        </svg>\n      </h1>\n      <p className=\"md:text-lg text-center mb-16\">\n        Submit your idea and get a list of similar ideas that YCombinator has\n        invested in before.\n        <HowItWorks />\n      </p>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/components/how-it-works.tsx",
    "content": "'use client';\nimport { useLocalStorage } from 'usehooks-ts';\nimport { Drawer } from 'vaul';\n\nexport const HowItWorks = () => {\n  const codeString =\n    `SELECT * FROM companies ORDER BY embedding <=> '[0.3, 0.8, -0.9]' LIMIT 5;`;\n\n  const [open, setOpen] = useLocalStorage('show banner', true);\n\n  return (\n    <Drawer.Root open={open} onOpenChange={setOpen} shouldScaleBackground>\n      <Drawer.Trigger asChild>\n        <button className=\"underline ml-2 focus-visible:border-gray-700 text-gray-1200 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100 transition-colors outline-none rounded-md\">\n          How it works\n        </button>\n      </Drawer.Trigger>\n      <Drawer.Portal>\n        <Drawer.Overlay className=\"fixed inset-0 bg-black/40\" />\n        <Drawer.Content\n          className=\"z-10 bg-gray-300 flex flex-col rounded-t-[10px] h-[70%] mt-24 fixed bottom-0 left-0 right-0 focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\n            transition-colors outline-none \"\n        >\n          <div className=\"p-4 mb-5 bg-gray-300 rounded-t-[10px] flex-1 overflow-y-auto\">\n            <div className=\"mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-gray-1100 mb-8\" />\n\n            <div className=\"mx-auto prose prose-invert\">\n              <Drawer.Title className=\"font-medium mb-10 text-lg text-center\">\n                How this app works\n              </Drawer.Title>\n              <p>\n                This project uses semantic search, an advanced search technique\n                that aims to understand the intent and context behind a search\n                query instead of just matching keywords.\n              </p>\n              <p>\n                The first step was to collect company data from the YCombinator\n                public API:{' '}\n                <a\n                  className=\"focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\n            transition-colors outline-none rounded-md\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  href=\"https://api.ycombinator.com/v0.1/companies\"\n                >\n                  https://api.ycombinator.com/v0.1/companies\n                </a>\n              </p>\n              <p>\n                Next, for each company description we generated an embedding,\n                which is a vector (list) of floating-point numbers. For example,\n                the word “Car” can be represented using the following vector:\n                [0.3, 0.8, -0.9].\n              </p>\n              <p>\n                We then used{' '}\n                <a\n                  className=\"focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\n            transition-colors outline-none rounded-md\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  href=\"https://bit.ly/3OFaSTp\"\n                >\n                  Neon\n                </a>{' '}\n                with{' '}\n                <a\n                  className=\"focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\n            transition-colors outline-none rounded-md\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  href=\"https://github.com/pgvector/pgvector\"\n                >\n                  pgvector\n                </a>\n                , which makes it possible to store and retrieve vector\n                embeddings in Postgres.\n              </p>\n              <p>\n                When a user submits a query, we convert it into a vector\n                embedding and find the 5 most similar results. Here is an\n                example query:\n                <pre className=\"font-mono text-[0.8rem]\">\n                  <code>{codeString}</code>\n                </pre>\n              </p>\n              <p>\n                You can find the{' '}\n                <a\n                  className=\"focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\n            transition-colors outline-none rounded-md\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  href=\"https://github.com/neondatabase/yc-idea-matcher\"\n                >\n                  source code on GitHub.\n                </a>\n              </p>\n            </div>\n          </div>\n        </Drawer.Content>\n      </Drawer.Portal>\n    </Drawer.Root>\n  );\n};\n"
  },
  {
    "path": "src/components/providers.tsx",
    "content": "'use client';\nimport { QueryClientProvider } from '@tanstack/react-query';\nimport { queryClient } from '~/lib/query-client';\nimport { Analytics } from '@vercel/analytics/react';\n\ntype Props = {\n  children: React.ReactNode;\n};\n\nexport default function Providers({ children }: Props) {\n  return (\n    <QueryClientProvider client={queryClient}>\n      {children}\n      <Analytics />\n    </QueryClientProvider>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/button.tsx",
    "content": "'use client';\nimport * as React from 'react';\nimport type { HTMLAttributes } from 'react';\nimport { cn } from '~/utils';\n\nexport interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {\n  loading?: boolean;\n  disabled?: boolean;\n  size?: ButtonSize;\n  appearance?: ButtonAppearance;\n  leadingIcon?: React.ReactNode;\n  trailingIcon?: React.ReactNode;\n  children?: React.ReactNode;\n  type?: 'button' | 'submit' | 'reset';\n  link?: string;\n}\n\nexport type ButtonSize = 'small' | 'medium' | 'large' | 'xlarge';\n\nexport type ButtonAppearance = 'primary' | 'secondary' | 'outlined' | 'danger';\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  function Btn(\n    {\n      disabled = false,\n      loading = false,\n      size = 'medium',\n      appearance = 'primary',\n      children,\n      className,\n      leadingIcon,\n      trailingIcon,\n      type = 'button',\n      ...props\n    }: ButtonProps,\n    ref\n  ) {\n    const computeStyles = (appearance: ButtonAppearance) => {\n      switch (appearance) {\n        case 'primary':\n          return 'text-white  bg-green-900  hover:bg-green-1000 border-transparent border';\n        case 'secondary':\n          return 'text-green-1100  bg-green-400 hover:bg-green-500 border-2 border-transparent';\n        case 'danger':\n          return 'text-gray-1200 bg-red-900 hover:bg-red-1000 border-2 border-transparent';\n        case 'outlined':\n          return 'bg-gray-200  border border-gray-700 text-gray-1100 hover:bg-gray-300 hover:text-gray-1200  hover:border-gray-800  transition-colors';\n      }\n    };\n\n    return (\n      <button\n        type={type}\n        ref={ref}\n        disabled={disabled || loading}\n        {...props}\n        className={cn(\n          computeStyles(appearance),\n          size === 'small' && 'px-3 py-2 text-sm',\n          size === 'medium' && 'px-4 py-2 text-sm ',\n          size === 'large' && 'px-4 py-2.5 text-base',\n          size === 'xlarge' && 'px-4 py-2.5 text-lg',\n          'rounded-md disabled:cursor-progress disabled:opacity-50',\n          'relative inline-flex select-none items-center  capitalize leading-4',\n          'transition-colors duration-150 ease-in-out',\n          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 ring-offset-gray-100',\n          className\n        )}\n      >\n        {leadingIcon}\n\n        {loading ? 'Loading...' : children}\n        {trailingIcon}\n      </button>\n    );\n  }\n);\n"
  },
  {
    "path": "src/components/ui/input.tsx",
    "content": "'use client';\nimport type { InputHTMLAttributes } from 'react';\nimport * as React from 'react';\nimport { cn } from '~/utils';\n\ninterface InputProps extends InputHTMLAttributes<HTMLInputElement> {\n  type?: 'text' | 'email' | 'password' | 'hidden' | 'number' | 'search';\n  className?: string;\n  id: string;\n  name: string;\n  placeholder?: string;\n  value?: string;\n  autoComplete?: string;\n  error?: string;\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n  function textInput(\n    {\n      type = 'text',\n      id,\n      name,\n      className,\n      placeholder,\n      value,\n      autoComplete = 'on',\n      error,\n      ...props\n    }: InputProps,\n    ref\n  ) {\n    return (\n      <>\n        <input\n          ref={ref}\n          id={id}\n          name={name}\n          type={type}\n          value={value}\n          autoComplete={autoComplete}\n          className={cn(\n            className,\n            'block w-full rounded-md border-gray-700 bg-gray-200 text-gray-1200 shadow-sm placeholder:text-gray-1100 sm:text-sm',\n            'disabled:cursor-not-allowed disabled:opacity-40',\n            'focus-visible:border-gray-700 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100',\n            'transition-colors hover:border-green-800',\n            error &&\n              'border-red-800 focus-visible:border-red-800 focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100'\n          )}\n          placeholder={placeholder}\n          {...props}\n        />\n      </>\n    );\n  }\n);\n"
  },
  {
    "path": "src/lib/query-client.ts",
    "content": "import { QueryClient } from '@tanstack/react-query';\n\nexport const queryClient = new QueryClient();\n"
  },
  {
    "path": "src/styles/globals.css",
    "content": "@tailwind base;\n\n/* Firefox */\n* {\n  scrollbar-width: thin;\n  scrollbar-color: #2b2f31 #151718;\n}\n\n/* Chrome, Edge, and Safari */\n*::-webkit-scrollbar {\n  width: 15px;\n}\n\n*::-webkit-scrollbar-track {\n  background: #151718;\n  border-radius: 5px;\n}\n\n*::-webkit-scrollbar-thumb {\n  background-color: #2b2f31;\n  border-radius: 14px;\n  border: 3px solid #151718;\n}\n\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "import clsx, { type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport const generateEmbeddings = async (prompt: string) => {\n  const res = await fetch('https://api.openai.com/v1/embeddings', {\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,\n    },\n    method: 'POST',\n    body: JSON.stringify({\n      model: 'text-embedding-ada-002',\n      input: prompt,\n    }),\n  });\n  const embeddings = await res.json();\n\n  return embeddings.data[0].embedding;\n};\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\n\n// radix-ui.com/colors\n/**\n| Step | Use Case                                |\n| ---- | --------------------------------------- |\n| 100  | App background                          |\n| 200  | Subtle background                       |\n| 300  | UI element background                   |\n| 400  | Hovered UI element background           |\n| 500  | Active / Selected UI element background |\n| 600  | Subtle borders and separators           |\n| 700  | UI element border and focus rings       |\n| 800  | Hovered UI element border               |\n| 900  | Solid backgrounds                       |\n| 1000 | Hovered solid backgrounds               |\n| 1100 | Low-contrast text                       |\n| 1200 | High-contrast text                      |\n*/\n\nmodule.exports = {\n  content: [\n    './src/components/**/*.{js,ts,jsx,tsx,mdx}',\n    './src/app/**/*.{js,ts,jsx,tsx,mdx}',\n  ],\n  theme: {\n    extend: {\n      colors: {\n        indigo: {\n          100: '#131620',\n          200: '#15192d',\n          400: '#192140',\n          400: '#1c274f',\n          500: '#1f2c5c',\n          600: '#22346e',\n          700: '#273e89',\n          800: '#2f4eb2',\n          900: '#3e63dd',\n          1000: '#5373e7',\n          1100: '#849dff',\n          1200: '#eef1fd',\n        },\n        orange: {\n          100: '#1f1206',\n          200: '#2b1400',\n          300: '#391a03',\n          400: '#441f04',\n          500: '#4f2305',\n          600: '#5f2a06',\n          700: '#763205',\n          800: '#943e00',\n          900: '#f76808',\n          1000: '#ff802b',\n          1100: '#ff8b3e',\n          1200: '#feeadd',\n        },\n        red: {\n          100: '#1f1315',\n          200: '#291415',\n          300: '#3c181a',\n          400: '#481a1d',\n          500: '#541b1f',\n          600: '#671e22',\n          700: '#822025',\n          800: '#aa2429',\n          900: '#e5484d',\n          1000: '#f2555a',\n          1100: '#ff6369',\n          1200: '#feecee',\n        },\n        gray: {\n          100: '#151718',\n          200: '#1a1d1e',\n          300: '#202425',\n          400: '#26292b',\n          500: '#2b2f31',\n          600: '#313538',\n          700: '#3a3f42',\n          800: '#4c5155',\n          900: '#697177',\n          1000: '#787f85',\n          1100: '#9ba1a6',\n          1200: '#ecedee',\n        },\n        green: {\n          100: '#0d1912',\n          200: '#0f1e13',\n          300: '#132819',\n          400: '#16301d',\n          500: '#193921',\n          600: '#1d4427',\n          700: '#245530',\n          800: '#2f6e3b',\n          900: '#46a758',\n          1000: '#55b467',\n          1100: '#63c174',\n          1200: '#e5fbeb',\n        },\n      },\n    },\n  },\n  plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"~/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]