Full Code of neondatabase/yc-idea-matcher for AI

main 752dd374c8e7 cached
22 files
35.2 KB
10.9k tokens
21 symbols
1 requests
Download .txt
Repository: neondatabase/yc-idea-matcher
Branch: main
Commit: 752dd374c8e7
Files: 22
Total size: 35.2 KB

Directory structure:
gitextract_o4qu7qo7/

├── .eslintrc.json
├── .gitignore
├── README.md
├── generate-embeddings.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   └── idea/
│   │   │       └── route.ts
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components/
│   │   ├── badge.tsx
│   │   ├── company.tsx
│   │   ├── hero.tsx
│   │   ├── how-it-works.tsx
│   │   ├── providers.tsx
│   │   └── ui/
│   │       ├── button.tsx
│   │       └── input.tsx
│   ├── lib/
│   │   └── query-client.ts
│   ├── styles/
│   │   └── globals.css
│   └── utils/
│       └── index.ts
├── tailwind.config.js
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.json
================================================
{
  "extends": "next/core-web-vitals"
}


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: README.md
================================================
# YC idea matcher

![Screenshot of the app UI](ui.png)

This project allows you Submit your idea and get a list of similar ideas that YCombinator has invested in before.

The project is built using the following technologies:

- [Neon](https://neon.tech): Serverless Postgres
- [pgvector](https://github.com/pgvector/pgvector): open-source Postgres extension for vector storage and similarity search
- [Neon Serverless Driver](https://github.com/neondatabase/serverless)
- [Next.js](https://nextjs.org): Fullstack framework for React
- [Vercel](https://vercel.com): deployment platform
- [OpenAI API](https://openai.com): generating vector embeddings
- [TailwindCSS](https://tailwindcss.com): Utility-first CSS framework
- [Upstash Redis](https://upstash.com): serverless Redis for rate limiting
- [Zod](https://zod.dev): TypeScript-first schema validation
- [React Query](https://react-query.tanstack.com): data fetching and caching library
- [Vaul](https://vaul.emilkowal.ski/): Drawer component for React.

## How the app works

You 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:

1. It creates the database schema and installs the `pgvector` extension
2. It goes through the YCombinator API 'https://api.ycombinator.com/v0.1/companies?page=1' and gets all the companies
3. For each company it generates embeddings using the long description and then stores the company data in the database.
  
> 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[];`

The app itself is a Next.js app with an API route located at `/api/idea`. Whenever a user submits an idea, the following happens:

1. The idea is sent to the OpenAI API to generate an embedding
2. We then use pgvector to retrieve the top 3 most similar ideas


================================================
FILE: generate-embeddings.ts
================================================
import { Pool } from 'pg';
import axios from 'axios';

const DATABASE_URL = '';
const OPENAI_API_KEY = '';

const pool = new Pool({
  connectionString: DATABASE_URL,
});

async function createCompaniesTable() {
  const client = await pool.connect();
  try {
    await client.query(`
      CREATE EXTENSION IF NOT EXISTS vector;
      CREATE TABLE IF NOT EXISTS companies (
        id SERIAL PRIMARY KEY,
        name TEXT,
        slug TEXT,
        website TEXT,
        "smallLogoUrl" TEXT,
        "oneLiner" TEXT,
        "longDescription" TEXT,
        "teamSize" INTEGER,
        url TEXT,
        batch TEXT,
        tags TEXT[],
        status TEXT,
        industries TEXT[],
        regions TEXT[],
        locations TEXT[],
        badges TEXT[],
        embedding VECTOR(1536)
      );
    `);
    console.log('Companies table created successfully');
  } catch (error) {
    console.error('Error creating companies table:', error);
  } finally {
    client.release();
  }
}

async function scrapeCompanies(url: string) {
  try {
    const response = await axios.get(url);
    const { companies, nextPage } = response.data;

    for (const company of companies) {
      const { longDescription } = company;
      const embedding = await generateEmbedding(longDescription);
      await storeCompany(company, embedding);
    }

    if (nextPage) {
      await scrapeCompanies(nextPage);
    }
  } catch (error) {
    console.error('Error scraping companies:', error);
  }
}

async function generateEmbedding(text: string): Promise<number[]> {
  try {
    const response = await axios.post(
      'https://api.openai.com/v1/embeddings',
      {
        input: text,
        model: 'text-embedding-ada-002',
      },
      {
        headers: {
          Authorization: `Bearer ${OPENAI_API_KEY}`,
          'Content-Type': 'application/json',
        },
      }
    );
    const { data } = response.data;
    return data[0].embedding;
  } catch (error) {
    console.error('Error generating embedding:', error);
  }

  return [];
}

async function storeCompany(company: any, embedding: number[]) {
  const {
    name,
    slug,
    website,
    smallLogoUrl,
    oneLiner,
    longDescription,
    teamSize,
    url,
    batch,
    tags,
    status,
    industries,
    regions,
    locations,
    badges,
  } = company;
  const client = await pool.connect();

  try {
    await client.query(
      `
      INSERT INTO companies (
        name, slug, website, "smallLogoUrl", "oneLiner", "longDescription", "teamSize", url, batch, tags, status, industries, regions, locations, badges, embedding
      )
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
    `,
      [
        name,
        slug,
        website,
        smallLogoUrl,
        oneLiner,
        longDescription,
        teamSize,
        url,
        batch,
        tags,
        status,
        industries,
        regions,
        locations,
        badges,
        embedding,
      ]
    );

    console.log(`Company '${name}' stored successfully`);
  } catch (error) {
    console.error(`Error storing company '${name}':`, error);
  } finally {
    client.release();
  }
}

async function runScript() {
  await createCompaniesTable();
  await scrapeCompanies('https://api.ycombinator.com/v0.1/companies?page=1');
  await pool.end();
}

runScript();


================================================
FILE: next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = nextConfig


================================================
FILE: package.json
================================================
{
  "name": "yc-idea-matcher",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "generate-embeddings": "tsx generate-embeddings.ts"
  },
  "dependencies": {
    "@neondatabase/serverless": "^0.5.6",
    "@tanstack/react-query": "^4.32.1",
    "@upstash/ratelimit": "^0.4.3",
    "@upstash/redis": "^1.22.0",
    "@vercel/analytics": "^1.0.1",
    "clsx": "^2.0.0",
    "next": "13.4.12",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-hook-form": "^7.45.2",
    "tailwind-merge": "^1.14.0",
    "usehooks-ts": "^2.9.1",
    "vaul": "^0.1.1",
    "zod": "^3.21.4"
  },
  "devDependencies": {
    "@tailwindcss/forms": "^0.5.4",
    "@tailwindcss/typography": "^0.5.9",
    "@types/node": "20.4.5",
    "@types/pg": "^8.10.2",
    "@types/react": "18.2.18",
    "@types/react-dom": "18.2.7",
    "autoprefixer": "10.4.14",
    "axios": "^1.4.0",
    "eslint": "8.46.0",
    "eslint-config-next": "13.4.12",
    "pg": "^8.11.2",
    "postcss": "8.4.27",
    "tailwindcss": "3.3.3",
    "tsx": "^3.12.7",
    "typescript": "5.1.6"
  }
}


================================================
FILE: postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}


================================================
FILE: src/app/api/idea/route.ts
================================================
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { neon, neonConfig } from '@neondatabase/serverless';
import { NextRequest, NextResponse } from 'next/server';
import { generateEmbeddings } from '~/utils';
import { z } from 'zod';

neonConfig.fetchConnectionCache = true;

export const runtime = 'edge';
export const preferredRegion = 'iad1';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  analytics: true,
  limiter: Ratelimit.slidingWindow(2, '5s'),
});

const sql = neon(process.env.DATABASE_URL!);

export async function POST(request: NextRequest) {
  const id = request.ip ?? 'anonymous';
  const limit = await ratelimit.limit(id ?? 'anonymous');

  if (!limit.success) {
    return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
  }

  const body = await request.json();

  const schema = z.object({
    idea: z
      .string()
      .min(20, {
        message: 'Idea must be at least 20 characters.',
      })
      .max(140, {
        message: 'Idea should be at most 140 characters.',
      }),
  });

  const validated = schema.safeParse(body);

  if (!validated.success) {
    return NextResponse.json(
      { error: `Invalid request ${validated.error.message}` },
      { status: 400 }
    );
  }

  const { idea } = validated.data;

  try {
    const embedding = await generateEmbeddings(idea);

    const result = await sql(
      `SELECT id, name, "smallLogoUrl", website, "oneLiner", "longDescription", batch, url, status, industries FROM companies ORDER BY embedding::VECTOR <=> '[${embedding}]' LIMIT 5;`
    );

    return NextResponse.json({ data: result });
  } catch (error) {
    return NextResponse.json({ error: error }, { status: 500 });
  }
}


================================================
FILE: src/app/layout.tsx
================================================
import '../styles/globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import Link from 'next/link';
import Providers from '~/components/providers';
import { cn } from '~/utils';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'YC Idea Matcher',
  description:
    'Submit your idea and get a list of similar ideas that YCombinator has invested in in the past.',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html
      lang="en"
      className={cn(
        'bg-[#080808] text-gray-1100 selection:text-gray-1200 selection:bg-green-800 flex flex-col min-h-[95vh]',
        inter.className
      )}
    >
      <Providers>
        <body className="flex-grow">{children}</body>
        <footer className="my-5">
          <p className="text-center text-sm">
            Powered by{' '}
            <a
              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
            transition-colors outline-none rounded-md text-gray-1200 hover:underline"
              target="_blank"
              rel="noopener noreferrer"
              href="https://bit.ly/3OFaSTp"
            >
              Neon
            </a>{' '}
            and{' '}
            <a
              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
            transition-colors outline-none rounded-md text-gray-1200 hover:underline"
              target="_blank"
              rel="noopener noreferrer"
              href="https://github.com/neondatabase/yc-idea-matcher"
            >
              pgvector
            </a>
          </p>
        </footer>
      </Providers>
    </html>
  );
}


================================================
FILE: src/app/page.tsx
================================================
'use client';
import { useMutation } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { Company } from '~/components/company';
import { Hero } from '~/components/hero';
import { Button } from '~/components/ui/button';
import { Input } from '~/components/ui/input';
import va from '@vercel/analytics';

type FormValues = {
  idea: string;
};

export default function Home() {
  const {
    handleSubmit,
    register,
    formState: { errors },
  } = useForm<FormValues>();

  const { mutate, isLoading, data } = useMutation(
    async (values: FormValues) => {
      va.track('Submit');
      const res = await fetch('/api/idea', {
        method: 'POST',
        body: JSON.stringify(values),
      });

      if (!res.ok) {
        throw new Error('Something went wrong...');
      }

      const data = await res.json();

      return data;
    }
  );

  const onSubmit = (values: FormValues) => mutate(values);

  return (
    <main className="relative">
      <a
        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
            transition-colors outline-none rounded-md text-gray-1200 hover:underline p-1"
        target="_blank"
        rel="noopener noreferrer"
        href="https://bit.ly/3OFaSTp"
      >
        <svg
          className="w-7 h-7 absolute top-10 left-10  lg:left-40"
          width="57"
          height="56"
          viewBox="0 0 57 56"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            fill-rule="evenodd"
            clip-rule="evenodd"
            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"
            fill="#12FFF7"
          />
          <path
            fill-rule="evenodd"
            clip-rule="evenodd"
            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"
            fill="url(#paint0_linear_9_7220)"
          />
          <path
            fill-rule="evenodd"
            clip-rule="evenodd"
            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"
            fill="url(#paint1_linear_9_7220)"
          />
          <path
            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"
            fill="#B9FFB3"
          />
          <defs>
            <linearGradient
              id="paint0_linear_9_7220"
              x1="56.0253"
              y1="55.8857"
              x2="6.90024"
              y2="-0.1215"
              gradientUnits="userSpaceOnUse"
            >
              <stop stop-color="#B9FFB3" />
              <stop offset="1" stop-color="#B9FFB3" stop-opacity="0" />
            </linearGradient>
            <linearGradient
              id="paint1_linear_9_7220"
              x1="56.0253"
              y1="55.8857"
              x2="22.77"
              y2="42.9181"
              gradientUnits="userSpaceOnUse"
            >
              <stop stop-color="#1A1A1A" stop-opacity="0.9" />
              <stop offset="1" stop-color="#1A1A1A" stop-opacity="0" />
            </linearGradient>
          </defs>
        </svg>
      </a>{' '}
      <div className="px-6 my-36 max-w-6xl mx-auto">
        <Hero />
        <form
          className="flex md:flex-row flex-col space-y-5 md:space-y-0 md:items-center justify-center md:space-x-2"
          onSubmit={handleSubmit(onSubmit)}
        >
          <Input
            placeholder="Describe your next big idea"
            id="idea"
            {...register('idea', {
              required: true,
              maxLength: {
                message: 'Max length exceeded',
                value: 140,
              },
              minLength: {
                message: 'Your idea should be at least 20 characters long',
                value: 20,
              },
            })}
          />

          <Button
            className="justify-center "
            loading={isLoading}
            size="large"
            type="submit"
          >
            Submit
          </Button>
        </form>
        {errors && (
          <p className="text-red-1100 mt-2 text-sm">{errors.idea?.message}</p>
        )}
        <div className="mt-10 mb-16">
          <div className="grid grid-cols-1 gap-10">
            {isLoading ? (
              <>
                <div className="p-5 animate-pulse h-80 rounded-md bg-gray-500" />
                <div className="p-5 animate-pulse h-80 rounded-md bg-gray-500" />{' '}
                <div className="p-5 animate-pulse h-80 rounded-md bg-gray-500" />
              </>
            ) : (
              data &&
              data.data.map((company: Company) => (
                <Company key={company.id} {...company} />
              ))
            )}
          </div>
        </div>
      </div>
    </main>
  );
}


================================================
FILE: src/components/badge.tsx
================================================
import { cn } from '~/utils';

export type Status = 'Public' | 'Active' | 'Acquired' | 'Inactive';

type Props = {
  status: Status;
};
export const Badge = ({ status }: Props) => {
  const styles: Record<Status, { text: string; className: string }> = {
    Public: {
      text: 'Public',
      className: 'text-indigo-1100 border-indigo-700',
    },
    Acquired: {
      text: 'Acquired',
      className: 'border-orange-700 text-orange-1100',
    },
    Active: {
      text: 'Active',
      className: 'border-green-700 text-green-1100',
    },
    Inactive: {
      text: 'Inactive',
      className: 'border-gray-700 text-gray-1100',
    },
  };

  return (
    <span
      className={cn(
        'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium',
        styles[status].className
      )}
    >
      {styles[status].text}
      <svg
        className="-mr-0.5 ml-1.5 h-2 w-2 "
        fill="currentColor"
        viewBox="0 0 8 8"
      >
        <circle cx={4} cy={4} r={3} />
      </svg>
    </span>
  );
};


================================================
FILE: src/components/company.tsx
================================================
import { Badge, Status } from './badge';

export type Company = {
  id: number;
  name: string;
  slug: string;
  website: string;
  smallLogoUrl?: string;
  oneLiner: string;
  longDescription: string;
  teamSize: number;
  url: string;
  batch: string;
  tags: string[];
  status: Status;
  industries: string[];
  badges: string[];
};

export const Company = ({
  id,
  website,
  smallLogoUrl,
  name,
  oneLiner,
  longDescription,
  industries,
  batch,
  status,
}: Company) => {
  return (
    <a
      key={id}
      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"
      href={website}
    >
      <div className="flex flex-col md:flex-row gap-10">
        {smallLogoUrl ? (
          <img
            className="w-16 h-16 rounded-md"
            src={smallLogoUrl}
            alt="Company logo"
          />
        ) : (
          <img
            className="w-16 h-16 rounded-md"
            src={`https://placehold.co/300x300/3a3f42/FFF?text=404`}
            alt="Company logo"
          />
        )}

        <div className="space-y-3">
          <h2 className="text-xl md:text-2xl font-semibold text-gray-1200">
            {name}
          </h2>
          <h3 className="text-gray-1200">{oneLiner}</h3>
          <p>{longDescription}</p>
          <p className="text-sm">{batch} batch</p>
          <div className="flex flex-wrap gap-3 text-sm">
            {industries.map((industry) => (
              <p
                className="inline-flex items-center rounded-md border-gray-700 text-gray-1100 border px-2.5 py-1 text-xs font-medium"
                key={industry}
              >
                {industry}
              </p>
            ))}
          </div>
          <Badge status={status} />
        </div>
      </div>
    </a>
  );
};


================================================
FILE: src/components/hero.tsx
================================================
import React from 'react';
import { HowItWorks } from './how-it-works';

export const Hero = () => {
  return (
    <>
      {' '}
      <h1 className="text-3xl flex items-center justify-center md:text-5xl text-center font-semibold text-gray-1200 mb-5">
        YC Idea Matcher{' '}
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="#FFC000"
          className="w-10 h-10 ml-2"
        >
          <path
            fillRule="evenodd"
            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"
            clipRule="evenodd"
          />
        </svg>
      </h1>
      <p className="md:text-lg text-center mb-16">
        Submit your idea and get a list of similar ideas that YCombinator has
        invested in before.
        <HowItWorks />
      </p>
    </>
  );
};


================================================
FILE: src/components/how-it-works.tsx
================================================
'use client';
import { useLocalStorage } from 'usehooks-ts';
import { Drawer } from 'vaul';

export const HowItWorks = () => {
  const codeString =
    `SELECT * FROM companies ORDER BY embedding <=> '[0.3, 0.8, -0.9]' LIMIT 5;`;

  const [open, setOpen] = useLocalStorage('show banner', true);

  return (
    <Drawer.Root open={open} onOpenChange={setOpen} shouldScaleBackground>
      <Drawer.Trigger asChild>
        <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">
          How it works
        </button>
      </Drawer.Trigger>
      <Drawer.Portal>
        <Drawer.Overlay className="fixed inset-0 bg-black/40" />
        <Drawer.Content
          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
            transition-colors outline-none "
        >
          <div className="p-4 mb-5 bg-gray-300 rounded-t-[10px] flex-1 overflow-y-auto">
            <div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-gray-1100 mb-8" />

            <div className="mx-auto prose prose-invert">
              <Drawer.Title className="font-medium mb-10 text-lg text-center">
                How this app works
              </Drawer.Title>
              <p>
                This project uses semantic search, an advanced search technique
                that aims to understand the intent and context behind a search
                query instead of just matching keywords.
              </p>
              <p>
                The first step was to collect company data from the YCombinator
                public API:{' '}
                <a
                  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
            transition-colors outline-none rounded-md"
                  target="_blank"
                  rel="noopener noreferrer"
                  href="https://api.ycombinator.com/v0.1/companies"
                >
                  https://api.ycombinator.com/v0.1/companies
                </a>
              </p>
              <p>
                Next, for each company description we generated an embedding,
                which is a vector (list) of floating-point numbers. For example,
                the word “Car” can be represented using the following vector:
                [0.3, 0.8, -0.9].
              </p>
              <p>
                We then used{' '}
                <a
                  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
            transition-colors outline-none rounded-md"
                  target="_blank"
                  rel="noopener noreferrer"
                  href="https://bit.ly/3OFaSTp"
                >
                  Neon
                </a>{' '}
                with{' '}
                <a
                  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
            transition-colors outline-none rounded-md"
                  target="_blank"
                  rel="noopener noreferrer"
                  href="https://github.com/pgvector/pgvector"
                >
                  pgvector
                </a>
                , which makes it possible to store and retrieve vector
                embeddings in Postgres.
              </p>
              <p>
                When a user submits a query, we convert it into a vector
                embedding and find the 5 most similar results. Here is an
                example query:
                <pre className="font-mono text-[0.8rem]">
                  <code>{codeString}</code>
                </pre>
              </p>
              <p>
                You can find the{' '}
                <a
                  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
            transition-colors outline-none rounded-md"
                  target="_blank"
                  rel="noopener noreferrer"
                  href="https://github.com/neondatabase/yc-idea-matcher"
                >
                  source code on GitHub.
                </a>
              </p>
            </div>
          </div>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  );
};


================================================
FILE: src/components/providers.tsx
================================================
'use client';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '~/lib/query-client';
import { Analytics } from '@vercel/analytics/react';

type Props = {
  children: React.ReactNode;
};

export default function Providers({ children }: Props) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <Analytics />
    </QueryClientProvider>
  );
}


================================================
FILE: src/components/ui/button.tsx
================================================
'use client';
import * as React from 'react';
import type { HTMLAttributes } from 'react';
import { cn } from '~/utils';

export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
  loading?: boolean;
  disabled?: boolean;
  size?: ButtonSize;
  appearance?: ButtonAppearance;
  leadingIcon?: React.ReactNode;
  trailingIcon?: React.ReactNode;
  children?: React.ReactNode;
  type?: 'button' | 'submit' | 'reset';
  link?: string;
}

export type ButtonSize = 'small' | 'medium' | 'large' | 'xlarge';

export type ButtonAppearance = 'primary' | 'secondary' | 'outlined' | 'danger';

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  function Btn(
    {
      disabled = false,
      loading = false,
      size = 'medium',
      appearance = 'primary',
      children,
      className,
      leadingIcon,
      trailingIcon,
      type = 'button',
      ...props
    }: ButtonProps,
    ref
  ) {
    const computeStyles = (appearance: ButtonAppearance) => {
      switch (appearance) {
        case 'primary':
          return 'text-white  bg-green-900  hover:bg-green-1000 border-transparent border';
        case 'secondary':
          return 'text-green-1100  bg-green-400 hover:bg-green-500 border-2 border-transparent';
        case 'danger':
          return 'text-gray-1200 bg-red-900 hover:bg-red-1000 border-2 border-transparent';
        case 'outlined':
          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';
      }
    };

    return (
      <button
        type={type}
        ref={ref}
        disabled={disabled || loading}
        {...props}
        className={cn(
          computeStyles(appearance),
          size === 'small' && 'px-3 py-2 text-sm',
          size === 'medium' && 'px-4 py-2 text-sm ',
          size === 'large' && 'px-4 py-2.5 text-base',
          size === 'xlarge' && 'px-4 py-2.5 text-lg',
          'rounded-md disabled:cursor-progress disabled:opacity-50',
          'relative inline-flex select-none items-center  capitalize leading-4',
          'transition-colors duration-150 ease-in-out',
          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-700 focus-visible:ring-offset-2 ring-offset-gray-100',
          className
        )}
      >
        {leadingIcon}

        {loading ? 'Loading...' : children}
        {trailingIcon}
      </button>
    );
  }
);


================================================
FILE: src/components/ui/input.tsx
================================================
'use client';
import type { InputHTMLAttributes } from 'react';
import * as React from 'react';
import { cn } from '~/utils';

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  type?: 'text' | 'email' | 'password' | 'hidden' | 'number' | 'search';
  className?: string;
  id: string;
  name: string;
  placeholder?: string;
  value?: string;
  autoComplete?: string;
  error?: string;
}

export const Input = React.forwardRef<HTMLInputElement, InputProps>(
  function textInput(
    {
      type = 'text',
      id,
      name,
      className,
      placeholder,
      value,
      autoComplete = 'on',
      error,
      ...props
    }: InputProps,
    ref
  ) {
    return (
      <>
        <input
          ref={ref}
          id={id}
          name={name}
          type={type}
          value={value}
          autoComplete={autoComplete}
          className={cn(
            className,
            'block w-full rounded-md border-gray-700 bg-gray-200 text-gray-1200 shadow-sm placeholder:text-gray-1100 sm:text-sm',
            'disabled:cursor-not-allowed disabled:opacity-40',
            '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',
            error &&
              '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'
          )}
          placeholder={placeholder}
          {...props}
        />
      </>
    );
  }
);


================================================
FILE: src/lib/query-client.ts
================================================
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient();


================================================
FILE: src/styles/globals.css
================================================
@tailwind base;

/* Firefox */
* {
  scrollbar-width: thin;
  scrollbar-color: #2b2f31 #151718;
}

/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
  width: 15px;
}

*::-webkit-scrollbar-track {
  background: #151718;
  border-radius: 5px;
}

*::-webkit-scrollbar-thumb {
  background-color: #2b2f31;
  border-radius: 14px;
  border: 3px solid #151718;
}

@tailwind components;
@tailwind utilities;


================================================
FILE: src/utils/index.ts
================================================
import clsx, { type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

export const generateEmbeddings = async (prompt: string) => {
  const res = await fetch('https://api.openai.com/v1/embeddings', {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    method: 'POST',
    body: JSON.stringify({
      model: 'text-embedding-ada-002',
      input: prompt,
    }),
  });
  const embeddings = await res.json();

  return embeddings.data[0].embedding;
};


================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */

// radix-ui.com/colors
/**
| Step | Use Case                                |
| ---- | --------------------------------------- |
| 100  | App background                          |
| 200  | Subtle background                       |
| 300  | UI element background                   |
| 400  | Hovered UI element background           |
| 500  | Active / Selected UI element background |
| 600  | Subtle borders and separators           |
| 700  | UI element border and focus rings       |
| 800  | Hovered UI element border               |
| 900  | Solid backgrounds                       |
| 1000 | Hovered solid backgrounds               |
| 1100 | Low-contrast text                       |
| 1200 | High-contrast text                      |
*/

module.exports = {
  content: [
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        indigo: {
          100: '#131620',
          200: '#15192d',
          400: '#192140',
          400: '#1c274f',
          500: '#1f2c5c',
          600: '#22346e',
          700: '#273e89',
          800: '#2f4eb2',
          900: '#3e63dd',
          1000: '#5373e7',
          1100: '#849dff',
          1200: '#eef1fd',
        },
        orange: {
          100: '#1f1206',
          200: '#2b1400',
          300: '#391a03',
          400: '#441f04',
          500: '#4f2305',
          600: '#5f2a06',
          700: '#763205',
          800: '#943e00',
          900: '#f76808',
          1000: '#ff802b',
          1100: '#ff8b3e',
          1200: '#feeadd',
        },
        red: {
          100: '#1f1315',
          200: '#291415',
          300: '#3c181a',
          400: '#481a1d',
          500: '#541b1f',
          600: '#671e22',
          700: '#822025',
          800: '#aa2429',
          900: '#e5484d',
          1000: '#f2555a',
          1100: '#ff6369',
          1200: '#feecee',
        },
        gray: {
          100: '#151718',
          200: '#1a1d1e',
          300: '#202425',
          400: '#26292b',
          500: '#2b2f31',
          600: '#313538',
          700: '#3a3f42',
          800: '#4c5155',
          900: '#697177',
          1000: '#787f85',
          1100: '#9ba1a6',
          1200: '#ecedee',
        },
        green: {
          100: '#0d1912',
          200: '#0f1e13',
          300: '#132819',
          400: '#16301d',
          500: '#193921',
          600: '#1d4427',
          700: '#245530',
          800: '#2f6e3b',
          900: '#46a758',
          1000: '#55b467',
          1100: '#63c174',
          1200: '#e5fbeb',
        },
      },
    },
  },
  plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
};


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "~/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
Download .txt
gitextract_o4qu7qo7/

├── .eslintrc.json
├── .gitignore
├── README.md
├── generate-embeddings.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   └── idea/
│   │   │       └── route.ts
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components/
│   │   ├── badge.tsx
│   │   ├── company.tsx
│   │   ├── hero.tsx
│   │   ├── how-it-works.tsx
│   │   ├── providers.tsx
│   │   └── ui/
│   │       ├── button.tsx
│   │       └── input.tsx
│   ├── lib/
│   │   └── query-client.ts
│   ├── styles/
│   │   └── globals.css
│   └── utils/
│       └── index.ts
├── tailwind.config.js
└── tsconfig.json
Download .txt
SYMBOL INDEX (21 symbols across 10 files)

FILE: generate-embeddings.ts
  constant DATABASE_URL (line 4) | const DATABASE_URL = '';
  constant OPENAI_API_KEY (line 5) | const OPENAI_API_KEY = '';
  function createCompaniesTable (line 11) | async function createCompaniesTable() {
  function scrapeCompanies (line 44) | async function scrapeCompanies(url: string) {
  function generateEmbedding (line 63) | async function generateEmbedding(text: string): Promise<number[]> {
  function storeCompany (line 87) | async function storeCompany(company: any, embedding: number[]) {
  function runScript (line 143) | async function runScript() {

FILE: src/app/api/idea/route.ts
  function POST (line 21) | async function POST(request: NextRequest) {

FILE: src/app/layout.tsx
  function RootLayout (line 16) | function RootLayout({

FILE: src/app/page.tsx
  type FormValues (line 10) | type FormValues = {
  function Home (line 14) | function Home() {

FILE: src/components/badge.tsx
  type Status (line 3) | type Status = 'Public' | 'Active' | 'Acquired' | 'Inactive';
  type Props (line 5) | type Props = {

FILE: src/components/company.tsx
  type Company (line 3) | type Company = {

FILE: src/components/providers.tsx
  type Props (line 6) | type Props = {
  function Providers (line 10) | function Providers({ children }: Props) {

FILE: src/components/ui/button.tsx
  type ButtonProps (line 6) | interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
  type ButtonSize (line 18) | type ButtonSize = 'small' | 'medium' | 'large' | 'xlarge';
  type ButtonAppearance (line 20) | type ButtonAppearance = 'primary' | 'secondary' | 'outlined' | 'danger';

FILE: src/components/ui/input.tsx
  type InputProps (line 6) | interface InputProps extends InputHTMLAttributes<HTMLInputElement> {

FILE: src/utils/index.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (39K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 40,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".gitignore",
    "chars": 368,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "README.md",
    "chars": 1959,
    "preview": "# 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 simil"
  },
  {
    "path": "generate-embeddings.ts",
    "chars": 3361,
    "preview": "import { Pool } from 'pg';\nimport axios from 'axios';\n\nconst DATABASE_URL = '';\nconst OPENAI_API_KEY = '';\n\nconst pool ="
  },
  {
    "path": "next.config.js",
    "chars": 92,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "package.json",
    "chars": 1160,
    "preview": "{\n  \"name\": \"yc-idea-matcher\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build"
  },
  {
    "path": "postcss.config.js",
    "chars": 82,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "src/app/api/idea/route.ts",
    "chars": 1754,
    "preview": "import { Ratelimit } from '@upstash/ratelimit';\nimport { Redis } from '@upstash/redis';\nimport { neon, neonConfig } from"
  },
  {
    "path": "src/app/layout.tsx",
    "chars": 1923,
    "preview": "import '../styles/globals.css';\nimport type { Metadata } from 'next';\nimport { Inter } from 'next/font/google';\nimport L"
  },
  {
    "path": "src/app/page.tsx",
    "chars": 6710,
    "preview": "'use client';\nimport { useMutation } from '@tanstack/react-query';\nimport { useForm } from 'react-hook-form';\nimport { C"
  },
  {
    "path": "src/components/badge.tsx",
    "chars": 1051,
    "preview": "import { cn } from '~/utils';\n\nexport type Status = 'Public' | 'Active' | 'Acquired' | 'Inactive';\n\ntype Props = {\n  sta"
  },
  {
    "path": "src/components/company.tsx",
    "chars": 1978,
    "preview": "import { Badge, Status } from './badge';\n\nexport type Company = {\n  id: number;\n  name: string;\n  slug: string;\n  websit"
  },
  {
    "path": "src/components/hero.tsx",
    "chars": 1675,
    "preview": "import React from 'react';\nimport { HowItWorks } from './how-it-works';\n\nexport const Hero = () => {\n  return (\n    <>\n "
  },
  {
    "path": "src/components/how-it-works.tsx",
    "chars": 4837,
    "preview": "'use client';\nimport { useLocalStorage } from 'usehooks-ts';\nimport { Drawer } from 'vaul';\n\nexport const HowItWorks = ("
  },
  {
    "path": "src/components/providers.tsx",
    "chars": 413,
    "preview": "'use client';\nimport { QueryClientProvider } from '@tanstack/react-query';\nimport { queryClient } from '~/lib/query-clie"
  },
  {
    "path": "src/components/ui/button.tsx",
    "chars": 2472,
    "preview": "'use client';\nimport * as React from 'react';\nimport type { HTMLAttributes } from 'react';\nimport { cn } from '~/utils';"
  },
  {
    "path": "src/components/ui/input.tsx",
    "chars": 1621,
    "preview": "'use client';\nimport type { InputHTMLAttributes } from 'react';\nimport * as React from 'react';\nimport { cn } from '~/ut"
  },
  {
    "path": "src/lib/query-client.ts",
    "chars": 100,
    "preview": "import { QueryClient } from '@tanstack/react-query';\n\nexport const queryClient = new QueryClient();\n"
  },
  {
    "path": "src/styles/globals.css",
    "chars": 404,
    "preview": "@tailwind base;\n\n/* Firefox */\n* {\n  scrollbar-width: thin;\n  scrollbar-color: #2b2f31 #151718;\n}\n\n/* Chrome, Edge, and "
  },
  {
    "path": "src/utils/index.ts",
    "chars": 628,
    "preview": "import clsx, { type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: C"
  },
  {
    "path": "tailwind.config.js",
    "chars": 2777,
    "preview": "/** @type {import('tailwindcss').Config} */\n\n// radix-ui.com/colors\n/**\n| Step | Use Case                               "
  },
  {
    "path": "tsconfig.json",
    "chars": 645,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"sk"
  }
]

About this extraction

This page contains the full source code of the neondatabase/yc-idea-matcher GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (35.2 KB), approximately 10.9k tokens, and a symbol index with 21 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!