[
  {
    "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.yarn/install-state.gz\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.env\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n/images-to-index\nimages-with-metadata.json\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2024 Vercel, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."
  },
  {
    "path": "README.md",
    "content": "<a href=\"https://semantic-search.vercel.app\">\n  <img alt=\"Next.js 14 and App Router Semantic Search.\" src=\"https://semantic-image-search.vercel.app/opengraph-image.png\">\n  <h1 align=\"center\">Semantic Image Search</h1>\n</a>\n\n<p align=\"center\">\n  An open-source AI semantic image search app template built with Next.js, the Vercel AI SDK, OpenAI, Vercel Postgres, Vercel Blob and Vercel KV.\n</p>\n\n<p align=\"center\">\n  <a href=\"#features\"><strong>Features</strong></a> ·\n  <a href=\"#model-providers\"><strong>Model Providers</strong></a> ·\n  <a href=\"#deploy-your-own\"><strong>Deploy Your Own</strong></a> ·\n  <a href=\"#running-locally\"><strong>Running locally</strong></a> ·\n  <a href=\"#authors\"><strong>Authors</strong></a>\n</p>\n<br/>\n\n## Features\n\n- [Next.js](https://nextjs.org) App Router\n- React Server Components (RSCs), Suspense, and Server Actions\n- [Vercel AI SDK](https://sdk.vercel.ai/docs) for multimodal prompting, generating & embedding image metadata, and streaming images from Server to Client\n- Support for OpenAI (default), Gemini, Anthropic, Cohere, or custom AI chat models\n- [shadcn/ui](https://ui.shadcn.com)\n  - Styling with [Tailwind CSS](https://tailwindcss.com)\n  - [Radix UI](https://radix-ui.com) for headless component primitives\n- Query caching with [Vercel KV](https://vercel.com/storage/kv)\n- Embeddings powered by [Vercel Postgres](https://vercel.com/storage/kv), [pgvector](https://github.com/pgvector/pgvector-node#drizzle-orm), and [Drizzle ORM](https://orm.drizzle.team/)\n- File (image) storage with [Vercel Blob](https://vercel.com/storage/blob)\n\n## Model Providers\n\nThis template ships with OpenAI `GPT-4o` as the default. However, thanks to the [Vercel AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [Gemini](https://gemini.google.com/), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [more](https://sdk.vercel.ai/providers/ai-sdk-providers) with just a few lines of code.\n\n## Deploy Your Own\n\nYou can deploy your own version of the Semantic Image Search App to Vercel with one click:\n\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fsemantic-image-search&env=OPENAI_API_KEY&envDescription=OpenAI%20key%20needed&envLink=https%3A%2F%2Fplatform.openai.com%2Fdocs%2Foverview)\n\n## Setup\n### Creating a KV Database Instance\n\nFollow the steps outlined in the [quick start guide](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) provided by Vercel. This guide will assist you in creating and configuring your KV database instance on Vercel, enabling your application to interact with it.\n\nRemember to update your environment variables (`KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN`) in the `.env` file with the appropriate credentials provided during the KV database setup.\n\n### Creating a Postgres Database Instance\n\nFollow the steps outlined in the [quick start guide](https://vercel.com/docs/storage/vercel-postgres/quickstart) provided by Vercel. This guide will assist you in creating and configuring your Postgres database instance on Vercel, enabling your application to interact with it.\n\nOnce you have instantiated your Vercel Postgres instance, run the following code to enable `pgvector`:\n```bash\nCREATE EXTENSION vector;\n```\n\nRemember to update your environment variables (`POSTGRES_URL`, `POSTGRES_PRISMA_URL`, `POSTGRES_URL_NON_POOLING`, `POSTGRES_USER`, `POSTGRES_HOST`, `POSTGRES_PASSWORD`, `POSTGRES_DATABASE`) in the `.env` file with the appropriate credentials provided during the Postgres database setup.\n\n### Creating a Blob Instance\n\nFollow the steps outlined in the [quick start guide](https://vercel.com/docs/storage/vercel-blob) provided by Vercel. This guide will assist you in creating and configuring your Blob instance on Vercel, enabling your application to interact with it.\n\nRemember to update your environment variable (`BLOB_READ_WRITE_TOKEN`) in the `.env` file with the appropriate credentials provided during the Blob setup.\n\n\n## Running locally\n\nYou will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Semantic Image Search. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary.\n\n> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts.\n\n1. Install Vercel CLI: `npm i -g vercel`\n2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`\n3. Download your environment variables: `vercel env pull`\n\n```bash\npnpm install\n```\n\n## Add OpenAI API Key\nBe sure to add your OpenAI API Key to your `.env`.\n\n## Database Setup\nTo push your schema changes to your Vercel Postgres database, run the following command.\n```bash\npnpm run db:generate\npnpm run db:push\n```\n\n## Prepare your Images (Indexing Step)\nTo get your application ready for Semantic search, you will have to complete three steps.\n1. Upload Images to storage\n2. Send Images to a Large Language Model to generate metadata (title, description)\n3. Iterate over each image, embed the metadata, and then save to the database\n\n### Upload Images\nPut the images you want to upload in the `images-to-index` directory (.jpg format) at the root of your application. Run the following command.\n```bash\npnpm run upload\n```\nThis script will upload the images to your Vercel Blob store.\nDepending on how many photos you are uploading, this step could take a while.\n\n### Generate Metadata\nRun the following command.\n```bash\npnpm run generate-metadata\n```\nThis script will generate metadata for each of the images you uploaded in the previous step.\nDepending on how many photos you are uploading, this step could take a while.\n\n### Embed Metadata and Save to Database\nRun the following command.\n```bash\npnpm run embed-and-save\n```\nDepending on how many photos you are uploading, this step could take a while. This script will embed the descriptions generated in the previous step and save them to your Vercel Postgres instance.\n\n## Starting the Server\nRun the following command\n```bash\npnpm run dev\n```\nYour app template should now be running on [localhost:3000](http://localhost:3000/).\n\n## Authors\n\nThis library is created by [Vercel](https://vercel.com) and [Next.js](https://nextjs.org) team members, with contributions from:\n\n- Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com)\n- Shu Ding ([@shuding\\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com)\n- shadcn ([@shadcn](https://twitter.com/shadcn)) - [Vercel](https://vercel.com)\n- Lars Grammel ([@lgrammel](https://twitter.com/lgrammel)) - [Vercel](https://vercel.com)\n- Nico Albanese ([@nicoalbanese10](https://twitter.com/nicoalbanese10)) - [Vercel](https://vercel.com)\n"
  },
  {
    "path": "app/globals.css",
    "content": "@tailwind base;\n  @tailwind components;\n  @tailwind utilities;\n\n  @layer base {\n    :root {\n      --background: 0 0% 100%;\n      --foreground: 240 10% 3.9%;\n\n      --card: 0 0% 100%;\n      --card-foreground: 240 10% 3.9%;\n\n      --popover: 0 0% 100%;\n      --popover-foreground: 240 10% 3.9%;\n\n      --primary: 240 5.9% 10%;\n      --primary-foreground: 0 0% 98%;\n\n      --secondary: 240 4.8% 95.9%;\n      --secondary-foreground: 240 5.9% 10%;\n\n      --muted: 240 4.8% 95.9%;\n      --muted-foreground: 240 3.8% 46.1%;\n\n      --accent: 240 4.8% 95.9%;\n      --accent-foreground: 240 5.9% 10%;\n\n      --destructive: 0 84.2% 60.2%;\n      --destructive-foreground: 0 0% 98%;\n\n      --border: 240 5.9% 90%;\n      --input: 240 5.9% 90%;\n      --ring: 240 10% 3.9%;\n\n      --radius: 0.5rem;\n    }\n\n    .dark {\n      --background: 240 10% 3.9%;\n      --foreground: 0 0% 98%;\n\n      --card: 240 10% 3.9%;\n      --card-foreground: 0 0% 98%;\n\n      --popover: 240 10% 3.9%;\n      --popover-foreground: 0 0% 98%;\n\n      --primary: 0 0% 98%;\n      --primary-foreground: 240 5.9% 10%;\n\n      --secondary: 240 3.7% 15.9%;\n      --secondary-foreground: 0 0% 98%;\n\n      --muted: 240 3.7% 15.9%;\n      --muted-foreground: 240 5% 64.9%;\n\n      --accent: 240 3.7% 15.9%;\n      --accent-foreground: 0 0% 98%;\n\n      --destructive: 0 62.8% 30.6%;\n      --destructive-foreground: 0 0% 98%;\n\n      --border: 240 3.7% 15.9%;\n      --input: 240 3.7% 15.9%;\n      --ring: 240 4.9% 83.9%;\n    }\n  }\n\n  @layer base {\n    * {\n      @apply border-border;\n    }\n    body {\n      @apply bg-background text-foreground;\n    }\n  }\n\nbody {\n  overflow-y: scroll;\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { GeistSans } from \"geist/font/sans\";\nimport \"./globals.css\";\nimport { cn } from \"@/lib/utils\";\nimport { TransitionProvider } from \"@/lib/hooks/use-shared-transition\";\n\nexport const metadata: Metadata = {\n  title: \"Semantic Image Search Demo\",\n  description: \"Semantic Image Search Demo built with the Vercel AI SDK.\",\n  metadataBase: process.env.VERCEL_URL\n    ? new URL(`https://${process.env.VERCEL_URL}`)\n    : undefined,\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body className={cn(\"font-sans antialiased\", GeistSans.variable)}>\n        <TransitionProvider>{children}</TransitionProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/page.tsx",
    "content": "import { CardGridSkeleton } from \"@/components/card-grid-skeleton\";\nimport { DeployButton } from \"@/components/deploy-button\";\nimport { SearchBox } from \"@/components/search-box\";\nimport { SuspendedImageSearch } from \"@/components/suspended-image-search\";\nimport Link from \"next/link\";\nimport { Suspense } from \"react\";\n\nexport default async function Home({\n  searchParams,\n}: {\n  searchParams: Promise<{ q?: string }>;\n}) {\n  const query = (await searchParams).q;\n  return (\n    <main className=\"p-8 space-y-4\">\n      <div className=\"flex justify-between items-center\">\n        <div>\n          <h1 className=\"font-semibold text-2xl\">Semantic Search</h1>\n        </div>\n        <DeployButton />\n      </div>\n      <div>\n        <p>\n          This demo showcases how to use the{\" \"}\n          <Link\n            href=\"https://sdk.vercel.ai/docs\"\n            className=\"text-blue-600 hover:underline\"\n            target=\"_blank\"\n          >\n            AI SDK\n          </Link>{\" \"}\n          to build semantic search applications. Try searching for something\n          semantically, like &quot;tasty food&quot;.\n        </p>\n      </div>\n      <div className=\"\">\n        <div className=\"pt-2\">\n          <SearchBox query={query} />\n        </div>\n        <Suspense fallback={<CardGridSkeleton />} key={query}>\n          <SuspendedImageSearch query={query} />\n        </Suspense>\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "components/card-grid-skeleton.tsx",
    "content": "import { Skeleton } from \"@/components/ui/skeleton\";\n\nexport function CardGridSkeleton() {\n  return (\n    <div className=\"grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-2\">\n      {new Array(16).fill(\"\").map((_, i) => (\n        <SkeletonCard key={i} />\n      ))}\n    </div>\n  );\n}\n\nexport function SkeletonCard() {\n  return (\n    <div className=\"flex flex-col space-y-3\">\n      <Skeleton className=\"h-[250px] sm:h-[450px] rounded-xl\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/deploy-button.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport { buttonVariants } from \"./ui/button\";\n\nexport const DeployButton = () => {\n  return (\n    <div className=\"flex items-center justify-end space-x-2\">\n      <a\n        href=\"https://vercel.com/templates/next.js/semantic-image-search\"\n        target=\"_blank\"\n        className={cn(buttonVariants())}\n      >\n        <IconVercel className=\"mr-2\" />\n        <span className=\"hidden sm:block\">Deploy to Vercel</span>\n        <span className=\"sm:hidden\">Deploy</span>\n      </a>\n    </div>\n  );\n};\nfunction IconVercel({ className, ...props }: React.ComponentProps<\"svg\">) {\n  return (\n    <svg\n      aria-label=\"Vercel logomark\"\n      role=\"img\"\n      viewBox=\"0 0 74 64\"\n      className={cn(\"size-4\", className)}\n      {...props}\n    >\n      <path\n        d=\"M37.5896 0.25L74.5396 64.25H0.639648L37.5896 0.25Z\"\n        fill=\"currentColor\"\n      ></path>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "components/error.tsx",
    "content": "import { AlertCircle } from \"lucide-react\";\n\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\n\nexport function ErrorComponent({ error }: { error: Error }) {\n  return (\n    <Alert variant=\"destructive\">\n      <AlertCircle className=\"h-4 w-4\" />\n      <AlertTitle>Error</AlertTitle>\n      <AlertDescription>\n        {error.message ?? \"An error occured. Please try again later.\"}\n      </AlertDescription>\n    </Alert>\n  );\n}\n"
  },
  {
    "path": "components/image-card.tsx",
    "content": "\"use client\";\n\nimport { DBImage } from \"@/lib/db/schema\";\nimport Image from \"next/image\";\nimport { MatchBadge } from \"./match-badge\";\nimport { Card } from \"./ui/card\";\n\nexport function ImageCard({\n  image,\n  similarity,\n}: {\n  image: DBImage;\n  similarity?: number;\n}) {\n  return (\n    <Card\n      key={image.id}\n      className=\"h-[250px] md:h-[450px] relative group rounded-lg overflow-hidden\"\n    >\n      <div className=\"absolute inset-0 z-10\">\n        <span className=\"sr-only\">View image</span>\n      </div>\n      <Image\n        src={image.path}\n        alt={image.title}\n        width={300}\n        height={450}\n        className=\"w-full h-full object-cover transition-transform group-hover:scale-105\"\n      />\n      <div className=\"absolute inset-0 bg-zinc-900/70 group-hover:opacity-100 opacity-0 transition-opacity flex flex-col items-center justify-center p-6 text-white text-center\">\n        <h3 className=\"text-xl font-semibold\">{image.title}</h3>\n        <p className=\"hidden md:block text-sm mt-2 overflow-y-hidden\">\n          {image.description}\n        </p>\n        <p className=\"hidden md:block text-xs font-medium text-gray-100 italic mt-4\">\n          Metadata Generated by GPT-4o\n        </p>\n      </div>\n      {similarity ? (\n        <div className=\"py-2 z-10 absolute bottom-2 left-2\">\n          <MatchBadge\n            type={similarity === 1 ? \"direct\" : \"semantic\"}\n            similarity={similarity}\n          />\n        </div>\n      ) : null}\n    </Card>\n  );\n}\n"
  },
  {
    "path": "components/image-search.tsx",
    "content": "\"use client\";\nimport { ImageCard } from \"./image-card\";\nimport { DBImage } from \"@/lib/db/schema\";\nimport { NoImagesFound } from \"./no-images-found\";\nimport { useSharedTransition } from \"@/lib/hooks/use-shared-transition\";\nimport { CardGridSkeleton } from \"./card-grid-skeleton\";\n\nexport const ImageSearch = ({\n  images,\n  query,\n}: {\n  images: DBImage[];\n  query?: string;\n}) => {\n  const { isPending } = useSharedTransition();\n\n  if (isPending) return <CardGridSkeleton />;\n\n  if (images.length === 0) {\n    return <NoImagesFound query={query ?? \"\"} />;\n  }\n\n  return <ImageGrid images={images} />;\n};\n\nconst ImageGrid = ({ images }: { images: DBImage[] }) => {\n  return (\n    <div className=\"grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-2 relative\">\n      {images.map((image) => (\n        <ImageCard\n          key={\"image_\" + image.id}\n          image={image}\n          similarity={image.similarity}\n        />\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "components/loading-spinner.tsx",
    "content": "/**\n * v0 by Vercel.\n * @see https://v0.dev/t/tm9EX5NO8KN\n * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app\n */\n\"use client\";\n\nimport { ImageStreamStatus } from \"@/lib/utils\";\n\nexport function LoadingSpinner({ status }: { status?: ImageStreamStatus }) {\n  return (\n    <div className=\"absolute h-full w-full bg-white z-10 top-0 flex items-start justify-center\">\n      <div className=\"flex flex-col items-center space-y-4 pt-16\">\n        <div className=\"animate-spin rounded-full h-12 w-12 border-4 border-gray-900 border-t-transparent dark:border-gray-50 dark:border-t-transparent\" />\n        <p className=\"text-gray-500 dark:text-gray-400\">\n          Searching\n          {status\n            ? status?.regular\n              ? \" for direct matches\"\n              : \" for semantic results\"\n            : \"\"}\n          ...\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/match-badge.tsx",
    "content": "import { Badge } from \"@/components/ui/badge\";\n\nexport const MatchBadge = ({\n  type,\n  similarity,\n}: {\n  type: \"direct\" | \"semantic\";\n  similarity?: number;\n}) => {\n  return (\n    <div className=\"\">\n      {type === \"semantic\" ? (\n        <>\n          <Badge\n            variant={\"default\"}\n            className=\"block sm:hidden bg-green-100 text-green-700\"\n          >\n            Similarity: {similarity?.toFixed(3)}\n          </Badge>\n          <Badge\n            variant={\"default\"}\n            className=\"hidden sm:block bg-green-100 text-green-700\"\n          >\n            Semantic Match: {similarity?.toFixed(3)}\n          </Badge>\n        </>\n      ) : (\n        <Badge variant={\"secondary\"}>Direct Match</Badge>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "components/no-images-found.tsx",
    "content": "/**\n * This code was generated by v0 by Vercel.\n * @see https://v0.dev/t/Q2jvX35BnWA\n * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app\n */\n\n/** Add fonts into your Next.js project:\n\nimport { Inter } from 'next/font/google'\n\ninter({\n  subsets: ['latin'],\n  display: 'swap',\n})\n\nTo read more about using these font, please visit the Next.js documentation:\n- App Directory: https://nextjs.org/docs/app/building-your-application/optimizing/fonts\n- Pages Directory: https://nextjs.org/docs/pages/building-your-application/optimizing/fonts\n**/\nexport function NoImagesFound({ query }: { query: string }) {\n  return (\n    <div className=\"flex flex-col items-center justify-start p-16 h-[50vh]\">\n      <div className=\"text-center space-y-2\">\n        <h3 className=\"text-lg font-medium text-gray-700 dark:text-gray-300\">\n          No images found\n        </h3>\n        <p className=\"text-sm text-gray-500 dark:text-gray-400\">\n          There were no results (semantic or direct) found for the query &apos;\n          {query}&apos;.\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/search-box.tsx",
    "content": "\"use client\";\n/**\n * This code was generated by v0 by Vercel.\n * @see https://v0.dev/t/GHXhZDO4KL4\n * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app\n */\n\nimport { Input } from \"@/components/ui/input\";\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\";\nimport { SearchIcon } from \"lucide-react\";\nimport { useRef, useState } from \"react\";\nimport { Button } from \"./ui/button\";\nimport { X } from \"lucide-react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { useSharedTransition } from \"@/lib/hooks/use-shared-transition\";\n\nexport function SearchBox({\n  query,\n  disabled,\n}: {\n  query?: string | null;\n  disabled?: boolean;\n}) {\n  const { startTransition } = useSharedTransition();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [isValid, setIsValid] = useState(true);\n\n  const searchParams = useSearchParams();\n  const q = searchParams.get(\"q\")?.toString() ?? \"\";\n  const pathname = usePathname();\n\n  const router = useRouter();\n\n  const handleSearch = useDebouncedCallback((term: string) => {\n    const params = new URLSearchParams(searchParams);\n\n    if (term) {\n      params.set(\"q\", term);\n    } else {\n      params.delete(\"q\");\n    }\n    startTransition &&\n      startTransition(() => {\n        router.push(`${pathname}?${params.toString()}`);\n      });\n  }, 300);\n\n  const resetQuery = () => {\n    startTransition &&\n      startTransition(() => {\n        router.push(\"/\");\n        if (inputRef.current) {\n          inputRef.current.value = \"\";\n          inputRef.current?.focus();\n        }\n      });\n  };\n\n  return (\n    <div className=\"flex flex-col\">\n      <div className=\"w-full mx-auto mb-4\">\n        <div className=\"relative flex items-center space-x-2\">\n          <div className=\"relative w-full flex items-center\">\n            <SearchIcon className=\"absolute left-4 w-5 h-5 text-gray-500\" />\n            <Input\n              disabled={disabled}\n              ref={inputRef}\n              defaultValue={query ?? \"\"}\n              minLength={3}\n              onChange={(e) => {\n                const newValue = e.target.value;\n                if (newValue.length > 2) {\n                  setIsValid(true);\n                  handleSearch(newValue);\n                } else if (newValue.length === 0) {\n                  handleSearch(newValue);\n                  setIsValid(false);\n                } else {\n                  setIsValid(false);\n                }\n              }}\n              className={\n                \"text-base w-full pl-12 pr-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:focus:ring-blue-500\"\n              }\n              placeholder=\"Search...\"\n            />\n            {q.length > 0 ? (\n              <Button\n                className=\"absolute right-2 text-gray-400 rounded-full h-8 w-8\"\n                variant=\"ghost\"\n                type=\"reset\"\n                size={\"icon\"}\n                onClick={resetQuery}\n              >\n                <X height=\"20\" width=\"20\" />\n              </Button>\n            ) : null}\n          </div>\n        </div>\n        {!isValid ? (\n          <div className=\"text-xs pt-2 text-destructive\">\n            Query must be 3 characters or longer\n          </div>\n        ) : (\n          <div className=\"h-6\" />\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/suspended-image-search.tsx",
    "content": "import { getImages } from \"@/lib/db/api\";\nimport { ErrorComponent } from \"./error\";\nimport { ImageSearch } from \"./image-search\";\n\nexport const SuspendedImageSearch = async ({ query }: { query?: string }) => {\n  const { images, error } = await getImages(query);\n\n  if (error) {\n    return <ErrorComponent error={error} />;\n  }\n\n  return <ImageSearch images={images} query={query} />;\n};\n"
  },
  {
    "path": "components/ui/alert.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        destructive:\n          \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role=\"alert\"\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n))\nAlert.displayName = \"Alert\"\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h5\n    ref={ref}\n    className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)}\n    {...props}\n  />\n))\nAlertTitle.displayName = \"AlertTitle\"\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm [&_p]:leading-relaxed\", className)}\n    {...props}\n  />\n))\nAlertDescription.displayName = \"AlertDescription\"\n\nexport { Alert, AlertTitle, AlertDescription }\n"
  },
  {
    "path": "components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default: \"border-transparent bg-primary text-primary-foreground \",\n        secondary: \"border-transparent bg-secondary text-secondary-foreground \",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\"\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n      className\n    )}\n    {...props}\n  />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\n      \"text-2xl font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n"
  },
  {
    "path": "components/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n"
  },
  {
    "path": "components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-muted\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { Skeleton }\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"zinc\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  }\n}"
  },
  {
    "path": "drizzle.config.ts",
    "content": "import type { Config } from \"drizzle-kit\";\n\nexport default {\n  schema: \"./lib/db/schema.ts\",\n  out: \"./lib/db/migrations\",\n  dialect: \"postgresql\",\n  dbCredentials: {\n    url: process.env.POSTGRES_URL!,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "lib/ai/0-upload.ts",
    "content": "import dotenv from \"dotenv\";\nimport { getJpgFiles } from \"./utils\";\nimport { list, put } from \"@vercel/blob\";\nimport fs from \"fs\";\n\ndotenv.config();\n\nasync function main() {\n  const basePath = \"images-to-index\";\n  const files = await getJpgFiles(basePath);\n  const { blobs } = await list();\n\n  for (const file of files) {\n    const exists = blobs.some((blob) => blob.pathname === file);\n    if (exists) {\n      console.log(`File (${file}) already exists in Blob store`);\n      continue;\n    }\n    const filePath = basePath + \"/\" + file;\n    const fileContent = fs.readFileSync(filePath);\n\n    console.clear();\n    console.log(\n      `Uploading ${file} (${files.indexOf(file) + 1}/${files.length}) to Blob storage`,\n    );\n    try {\n      await put(file, fileContent, { access: \"public\" });\n      console.log(`Uploaded ${file}`);\n    } catch (e) {\n      console.error(e);\n    }\n  }\n  console.log(\"All images uploaded!\");\n  process.exit(0);\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "lib/ai/1-generate-metadata.ts",
    "content": "import { openai } from \"@ai-sdk/openai\";\nimport { generateObject } from \"ai\";\nimport dotenv from \"dotenv\";\nimport { z } from \"zod\";\nimport { ImageMetadata, writeAllMetadataToFile } from \"./utils\";\nimport { list } from \"@vercel/blob\";\n\ndotenv.config();\n\nasync function main() {\n  const blobs = await list();\n  const files = blobs.blobs.map((b) => b.url);\n\n  console.log(\"files to process:\\n\", files);\n\n  const images: ImageMetadata[] = [];\n\n  for (const file of files) {\n    console.clear();\n    console.log(\n      `Generating description for ${file} (${files.indexOf(file) + 1}/${files.length})`,\n    );\n    const result = await generateObject({\n      model: openai(\"gpt-4o\"),\n      schema: z.object({\n        image: z.object({\n          title: z.string().describe(\"an artistic title for the image\"),\n          description: z\n            .string()\n            .describe(\"A one sentence description of the image\"),\n        }),\n      }),\n      maxTokens: 512,\n      messages: [\n        {\n          role: \"user\",\n          content: [\n            { type: \"text\", text: \"Describe the image in detail.\" },\n            {\n              type: \"image\",\n              image: file,\n            },\n          ],\n        },\n      ],\n    });\n    images.push({ path: file, metadata: result.object.image });\n  }\n  await writeAllMetadataToFile(images, \"images-with-metadata.json\");\n  console.log(\"All images processed!\");\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "lib/ai/2-embed-and-save.ts",
    "content": "import dotenv from \"dotenv\";\nimport { embeddingModel, getMetadataFile } from \"./utils\";\nimport { embed } from \"ai\";\nimport { nanoid } from \"nanoid\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport { DBImage, dbImageSchema, images } from \"../db/schema\";\n\ndotenv.config();\n\nexport const client = postgres(process.env.POSTGRES_URL!);\nexport const db = drizzle(client);\n\nconst saveImage = async (image: DBImage) => {\n  try {\n    const safeImage = dbImageSchema.parse(image);\n    const [savedImage] = await db.insert(images).values(safeImage);\n    return savedImage;\n  } catch (e) {\n    console.error(e);\n  }\n};\n\nasync function main() {\n  // read metadata json file\n  const imagesWithMetadata = await getMetadataFile(\"images-with-metadata.json\");\n\n  // map over it and embed each .metadata key\n  for (const image of imagesWithMetadata) {\n    console.clear();\n    console.log(\n      `Generating embedding for ${image.path} (${imagesWithMetadata.indexOf(image) + 1}/${imagesWithMetadata.length})`,\n    );\n\n    // create embedding\n    const { embedding } = await embed({\n      model: embeddingModel,\n      value: image.metadata.title + \"\\n\" + image.metadata.description,\n    });\n    //\n\n    console.log(\n      `Saving ${image.path} to the DB (${imagesWithMetadata.indexOf(image) + 1}/${imagesWithMetadata.length})`,\n    );\n    // push to db\n    try {\n      await saveImage({\n        title: image.metadata.title,\n        description: image.metadata.description,\n        id: nanoid(),\n        path: image.path,\n        embedding,\n      });\n    } catch (e) {\n      console.error(e);\n    }\n  }\n  console.log(\"Successfully embedded and saved all images!\");\n  process.exit(0);\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "lib/ai/utils.ts",
    "content": "import { openai } from \"@ai-sdk/openai\";\nimport { embed } from \"ai\";\nimport fs from \"fs\";\nimport path from \"path\";\n\nexport type ImageMetadata = {\n  path: string;\n  metadata: {\n    title: string;\n    description: string;\n  };\n};\n\nexport const embeddingModel = openai.embedding(\"text-embedding-3-small\");\n\n/**\n * Asynchronously gets all `.jpg` files in the specified directory.\n *\n * @param dir The directory to search within.\n * @returns A promise that resolves to an array of filenames.\n */\nexport async function getJpgFiles(dir: string): Promise<string[]> {\n  try {\n    const files = await fs.promises.readdir(dir);\n    const jpgFiles = files.filter(\n      (file) => path.extname(file).toLowerCase() === \".jpg\",\n    );\n    return jpgFiles;\n  } catch (error) {\n    console.error(\"Error reading directory:\", error);\n    throw error; // Re-throw the error for further handling if necessary\n  }\n}\n\n/**\n * Writes all metadata to a single JSON file.\n *\n * @param metadataArray An array of metadata objects.\n * @param outputPath The path including filename to the output JSON file.\n */\nexport async function writeAllMetadataToFile(\n  metadataArray: ImageMetadata[],\n  outputPath: string,\n) {\n  try {\n    await fs.promises.writeFile(\n      outputPath,\n      JSON.stringify(metadataArray, null, 2),\n    );\n    console.log(`All metadata written to ${outputPath}`);\n  } catch (error) {\n    console.error(\"Error writing metadata to file:\", error);\n    throw error;\n  }\n}\n\nexport async function getMetadataFile(path: string): Promise<ImageMetadata[]> {\n  try {\n    const rawFile = await fs.promises.readFile(path, { encoding: \"utf-8\" });\n    const file = JSON.parse(rawFile) as ImageMetadata[];\n    return file;\n  } catch (error) {\n    console.error(\"Error reading file:\", error);\n    throw error; // Re-throw the error for further handling if necessary\n  }\n}\n\nexport const generateEmbedding = async (value: string): Promise<number[]> => {\n  const input = value.replaceAll(\"\\n\", \" \");\n  const { embedding } = await embed({\n    model: embeddingModel,\n    value: input,\n  });\n  return embedding;\n};\n"
  },
  {
    "path": "lib/db/api.ts",
    "content": "\"use server\";\n\nimport {\n  cosineDistance,\n  desc,\n  getTableColumns,\n  gt,\n  or,\n  sql,\n} from \"drizzle-orm\";\nimport { db } from \".\";\nimport { DBImage, images } from \"./schema\";\nimport { generateEmbedding } from \"../ai/utils\";\nimport { kv } from \"@vercel/kv\";\n\nconst { embedding: _, ...rest } = getTableColumns(images);\nconst imagesWithoutEmbedding = {\n  ...rest,\n  embedding: sql<number[]>`ARRAY[]::integer[]`,\n};\n\nexport const findSimilarContent = async (description: string) => {\n  const embedding = await generateEmbedding(description);\n  const similarity = sql<number>`1 - (${cosineDistance(images.embedding, embedding)})`;\n  const similarGuides = await db\n    .select({ image: imagesWithoutEmbedding, similarity })\n    .from(images)\n    .where(gt(similarity, 0.28)) // experiment with this value based on your embedding model\n    .orderBy((t) => desc(t.similarity))\n    .limit(10);\n\n  return similarGuides;\n};\n\nexport const findImageByQuery = async (query: string) => {\n  const result = await db\n    .select({ image: imagesWithoutEmbedding, similarity: sql<number>`1` })\n    .from(images)\n    .where(\n      or(\n        sql`title ILIKE ${\"%\" + query + \"%\"}`,\n        sql`description ILIKE ${\"%\" + query + \"%\"}`,\n      ),\n    );\n  return result;\n};\n\nfunction uniqueItemsByObject(items: DBImage[]): DBImage[] {\n  const seenObjects = new Set<string>();\n  const uniqueItems: DBImage[] = [];\n\n  for (const item of items) {\n    if (!seenObjects.has(item.title)) {\n      seenObjects.add(item.title);\n      uniqueItems.push(item);\n    }\n  }\n\n  return uniqueItems;\n}\n\nexport const getImages = async (\n  query?: string,\n): Promise<{ images: DBImage[]; error?: Error }> => {\n  try {\n    const formattedQuery = query\n      ? \"q:\" + query?.replaceAll(\" \", \"_\")\n      : \"all_images\";\n\n    const cached = await kv.get<DBImage[]>(formattedQuery);\n    if (cached) {\n      return { images: cached };\n    } else {\n      if (query === undefined || query.length < 3) {\n        const allImages = await db\n          .select(imagesWithoutEmbedding)\n          .from(images)\n          .limit(20);\n        await kv.set(\"all_images\", JSON.stringify(allImages));\n        return { images: allImages };\n      } else {\n        const directMatches = await findImageByQuery(query);\n        const semanticMatches = await findSimilarContent(query);\n        const allMatches = uniqueItemsByObject(\n          [...directMatches, ...semanticMatches].map((image) => ({\n            ...image.image,\n            similarity: image.similarity,\n          })),\n        );\n\n        await kv.set(formattedQuery, JSON.stringify(allMatches));\n        return { images: allMatches };\n      }\n    }\n  } catch (e) {\n    if (e instanceof Error) return { error: e, images: [] };\n    return {\n      images: [],\n      error: { message: \"Error, please try again.\" } as Error,\n    };\n  }\n};\n"
  },
  {
    "path": "lib/db/index.ts",
    "content": "import { sql } from \"@vercel/postgres\";\nimport { drizzle } from \"drizzle-orm/vercel-postgres\";\n\nexport const db = drizzle(sql);\n"
  },
  {
    "path": "lib/db/schema.ts",
    "content": "import { varchar, index, pgTable, vector, text } from \"drizzle-orm/pg-core\";\nimport { nanoid } from \"nanoid\";\nimport { z } from \"zod\";\n\nexport const images = pgTable(\n  \"images\",\n  {\n    id: varchar(\"id\", { length: 191 })\n      .primaryKey()\n      .$defaultFn(() => nanoid()),\n    title: text(\"title\").notNull(),\n    description: text(\"description\").notNull(),\n    path: text(\"path\").notNull(),\n    embedding: vector(\"embedding\", { dimensions: 1536 }).notNull(),\n  },\n  (table) => ({\n    embeddingIndex: index(\"embeddingIndex\").using(\n      \"hnsw\",\n      table.embedding.op(\"vector_cosine_ops\"),\n    ),\n  }),\n);\n\nexport const dbImageSchema = z.object({\n  id: z.string(),\n  embedding: z.array(z.number()),\n  title: z.string(),\n  path: z.string(),\n  description: z.string(),\n  similarity: z.number().optional(),\n});\n\nexport type DBImage = z.infer<typeof dbImageSchema>;\n"
  },
  {
    "path": "lib/hooks/use-shared-transition.tsx",
    "content": "\"use client\";\n\nimport React, { createContext, useContext, useTransition } from \"react\";\n\nconst defaultValue: {\n  isPending: boolean;\n  startTransition?: React.TransitionStartFunction;\n} = { isPending: false };\nconst TransitionContext = createContext(defaultValue);\n\nexport const TransitionProvider = ({\n  children,\n}: {\n  children: React.ReactNode;\n}) => {\n  const [isPending, startTransition] = useTransition();\n\n  return (\n    <TransitionContext.Provider value={{ isPending, startTransition }}>\n      {children}\n    </TransitionContext.Provider>\n  );\n};\n\nexport const useSharedTransition = () => useContext(TransitionContext);\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport type ImageStreamStatus = {\n  regular: boolean;\n  semantic: boolean;\n};\n"
  },
  {
    "path": "next.config.mjs",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  images: {\n    remotePatterns: [\n      {\n        protocol: 'https',\n        hostname: 'bodo0tgbs4falkp7.public.blob.vercel-storage.com',\n        port: '',\n      },\n    ],\n    minimumCacheTTL: 60,\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"semantic-image-search\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"db:generate\": \"drizzle-kit generate\",\n    \"db:push\": \"drizzle-kit push\",\n    \"db:studio\": \"drizzle-kit studio\",\n    \"upload\": \"tsx lib/ai/0-upload.ts\",\n    \"generate-metadata\": \"tsx lib/ai/1-generate-metadata.ts\",\n    \"embed-and-save\": \"tsx lib/ai/2-embed-and-save.ts\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/openai\": \"^1.2.1\",\n    \"@radix-ui/react-slot\": \"^1.1.0\",\n    \"@vercel/blob\": \"^0.27.0\",\n    \"@vercel/kv\": \"^3.0.0\",\n    \"@vercel/postgres\": \"^0.10.0\",\n    \"ai\": \"^4.1.54\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"drizzle-orm\": \"^0.38.1\",\n    \"geist\": \"^1.3.1\",\n    \"lucide-react\": \"^0.468.0\",\n    \"nanoid\": \"^5.0.9\",\n    \"next\": \"^15.5.15\",\n    \"postgres\": \"^3.4.5\",\n    \"react\": \"^19.0.3\",\n    \"react-dom\": \"^19.0.3\",\n    \"sharp\": \"^0.33.5\",\n    \"tailwind-merge\": \"^2.5.5\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"use-debounce\": \"^10.0.4\",\n    \"zod\": \"^3.24.1\"\n  },\n  \"devDependencies\": {\n    \"@types/lodash\": \"^4.17.13\",\n    \"@types/node\": \"^22.10.2\",\n    \"@types/react\": \"^19.0.1\",\n    \"@types/react-dom\": \"^19.0.2\",\n    \"dotenv\": \"^16.4.7\",\n    \"drizzle-kit\": \"^0.30.0\",\n    \"eslint\": \"^9.16.0\",\n    \"eslint-config-next\": \"15.1.0\",\n    \"postcss\": \"^8.4.49\",\n    \"tailwindcss\": \"^3.4.16\",\n    \"tsx\": \"^4.19.2\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\"\n\nconst config = {\n  darkMode: [\"class\"],\n  content: [\n    './pages/**/*.{ts,tsx}',\n    './components/**/*.{ts,tsx}',\n    './app/**/*.{ts,tsx}',\n    './src/**/*.{ts,tsx}',\n\t],\n  prefix: \"\",\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: \"0\" },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\")],\n} satisfies Config\n\nexport default config"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": 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      \"@/*\": [\n        \"./*\"\n      ]\n    },\n    \"target\": \"ES2017\"\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  }
]