[
  {
    "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\ncopy_github_release.sh\n\nsupabase/"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1.  Definitions.\n\n    \"License\" shall mean the terms and conditions for use, reproduction,\n    and distribution as defined by Sections 1 through 9 of this document.\n\n    \"Licensor\" shall mean the copyright owner or entity authorized by\n    the copyright owner that is granting the License.\n\n    \"Legal Entity\" shall mean the union of the acting entity and all\n    other entities that control, are controlled by, or are under common\n    control with that entity. For the purposes of this definition,\n    \"control\" means (i) the power, direct or indirect, to cause the\n    direction or management of such entity, whether by contract or\n    otherwise, or (ii) ownership of fifty percent (50%) or more of the\n    outstanding shares, or (iii) beneficial ownership of such entity.\n\n    \"You\" (or \"Your\") shall mean an individual or Legal Entity\n    exercising permissions granted by this License.\n\n    \"Source\" form shall mean the preferred form for making modifications,\n    including but not limited to software source code, documentation\n    source, and configuration files.\n\n    \"Object\" form shall mean any form resulting from mechanical\n    transformation or translation of a Source form, including but\n    not limited to compiled object code, generated documentation,\n    and conversions to other media types.\n\n    \"Work\" shall mean the work of authorship, whether in Source or\n    Object form, made available under the License, as indicated by a\n    copyright notice that is included in or attached to the work\n    (an example is provided in the Appendix below).\n\n    \"Derivative Works\" shall mean any work, whether in Source or Object\n    form, that is based on (or derived from) the Work and for which the\n    editorial revisions, annotations, elaborations, or other modifications\n    represent, as a whole, an original work of authorship. For the purposes\n    of this License, Derivative Works shall not include works that remain\n    separable from, or merely link (or bind by name) to the interfaces of,\n    the Work and Derivative Works thereof.\n\n    \"Contribution\" shall mean any work of authorship, including\n    the original version of the Work and any modifications or additions\n    to that Work or Derivative Works thereof, that is intentionally\n    submitted to Licensor for inclusion in the Work by the copyright owner\n    or by an individual or Legal Entity authorized to submit on behalf of\n    the copyright owner. For the purposes of this definition, \"submitted\"\n    means any form of electronic, verbal, or written communication sent\n    to the Licensor or its representatives, including but not limited to\n    communication on electronic mailing lists, source code control systems,\n    and issue tracking systems that are managed by, or on behalf of, the\n    Licensor for the purpose of discussing and improving the Work, but\n    excluding communication that is conspicuously marked or otherwise\n    designated in writing by the copyright owner as \"Not a Contribution.\"\n\n    \"Contributor\" shall mean Licensor and any individual or Legal Entity\n    on behalf of whom a Contribution has been received by Licensor and\n    subsequently incorporated within the Work.\n\n2.  Grant of Copyright License. Subject to the terms and conditions of\n    this License, each Contributor hereby grants to You a perpetual,\n    worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n    copyright license to reproduce, prepare Derivative Works of,\n    publicly display, publicly perform, sublicense, and distribute the\n    Work and such Derivative Works in Source or Object form.\n\n3.  Grant of Patent License. Subject to the terms and conditions of\n    this License, each Contributor hereby grants to You a perpetual,\n    worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n    (except as stated in this section) patent license to make, have made,\n    use, offer to sell, sell, import, and otherwise transfer the Work,\n    where such license applies only to those patent claims licensable\n    by such Contributor that are necessarily infringed by their\n    Contribution(s) alone or by combination of their Contribution(s)\n    with the Work to which such Contribution(s) was submitted. If You\n    institute patent litigation against any entity (including a\n    cross-claim or counterclaim in a lawsuit) alleging that the Work\n    or a Contribution incorporated within the Work constitutes direct\n    or contributory patent infringement, then any patent licenses\n    granted to You under this License for that Work shall terminate\n    as of the date such litigation is filed.\n\n4.  Redistribution. You may reproduce and distribute copies of the\n    Work or Derivative Works thereof in any medium, with or without\n    modifications, and in Source or Object form, provided that You\n    meet the following conditions:\n\n    (a) You must give any other recipients of the Work or\n    Derivative Works a copy of this License; and\n\n    (b) You must cause any modified files to carry prominent notices\n    stating that You changed the files; and\n\n    (c) You must retain, in the Source form of any Derivative Works\n    that You distribute, all copyright, patent, trademark, and\n    attribution notices from the Source form of the Work,\n    excluding those notices that do not pertain to any part of\n    the Derivative Works; and\n\n    (d) If the Work includes a \"NOTICE\" text file as part of its\n    distribution, then any Derivative Works that You distribute must\n    include a readable copy of the attribution notices contained\n    within such NOTICE file, excluding those notices that do not\n    pertain to any part of the Derivative Works, in at least one\n    of the following places: within a NOTICE text file distributed\n    as part of the Derivative Works; within the Source form or\n    documentation, if provided along with the Derivative Works; or,\n    within a display generated by the Derivative Works, if and\n    wherever such third-party notices normally appear. The contents\n    of the NOTICE file are for informational purposes only and\n    do not modify the License. You may add Your own attribution\n    notices within Derivative Works that You distribute, alongside\n    or as an addendum to the NOTICE text from the Work, provided\n    that such additional attribution notices cannot be construed\n    as modifying the License.\n\n    You may add Your own copyright statement to Your modifications and\n    may provide additional or different license terms and conditions\n    for use, reproduction, or distribution of Your modifications, or\n    for any such Derivative Works as a whole, provided Your use,\n    reproduction, and distribution of the Work otherwise complies with\n    the conditions stated in this License.\n\n5.  Submission of Contributions. Unless You explicitly state otherwise,\n    any Contribution intentionally submitted for inclusion in the Work\n    by You to the Licensor shall be under the terms and conditions of\n    this License, without any additional terms or conditions.\n    Notwithstanding the above, nothing herein shall supersede or modify\n    the terms of any separate license agreement you may have executed\n    with Licensor regarding such Contributions.\n\n6.  Trademarks. This License does not grant permission to use the trade\n    names, trademarks, service marks, or product names of the Licensor,\n    except as required for reasonable and customary use in describing the\n    origin of the Work and reproducing the content of the NOTICE file.\n\n7.  Disclaimer of Warranty. Unless required by applicable law or\n    agreed to in writing, Licensor provides the Work (and each\n    Contributor provides its Contributions) on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n    implied, including, without limitation, any warranties or conditions\n    of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n    PARTICULAR PURPOSE. You are solely responsible for determining the\n    appropriateness of using or redistributing the Work and assume any\n    risks associated with Your exercise of permissions under this License.\n\n8.  Limitation of Liability. In no event and under no legal theory,\n    whether in tort (including negligence), contract, or otherwise,\n    unless required by applicable law (such as deliberate and grossly\n    negligent acts) or agreed to in writing, shall any Contributor be\n    liable to You for damages, including any direct, indirect, special,\n    incidental, or consequential damages of any character arising as a\n    result of this License or out of the use or inability to use the\n    Work (including but not limited to damages for loss of goodwill,\n    work stoppage, computer failure or malfunction, or any and all\n    other commercial damages or losses), even if such Contributor\n    has been advised of the possibility of such damages.\n\n9.  Accepting Warranty or Additional Liability. While redistributing\n    the Work or Derivative Works thereof, You may choose to offer,\n    and charge a fee for, acceptance of support, warranty, indemnity,\n    or other liability obligations and/or rights consistent with this\n    License. However, in accepting such obligations, You may act only\n    on Your own behalf and on Your sole responsibility, not on behalf\n    of any other Contributor, and only if You agree to indemnify,\n    defend, and hold each Contributor harmless for any liability\n    incurred by, or claims asserted against, such Contributor by reason\n    of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\nCopyright 2024 Yoshiki Miura\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.\n"
  },
  {
    "path": "README.md",
    "content": "# DiscovAI\n\nAn AI-powered search engine for AI tools, or your own data.\n\nhttps://github.com/user-attachments/assets/2cdc92d0-d0c9-4098-8166-260e973783f0\n\nPlease feel free to contact me on [Twitter](https://x.com/ruiyanghim) or [create an issue](https://github.com/DiscovAI/DiscovAI-search/issues/new) if you have any questions.\n\n## 💻 Live Demo\n\n[DiscovAI.io](https://discovai.io/) (use it for free without signin or credit card)\n\n## 🗂️ Overview\n\n- 🛠 [Features](#-features)\n- 🧱 [Tech-Stack](#-stack)\n- 🚀 [Quickstart](#-quickstart)\n- 🌐 [Deploy](#-deploy)\n\n## 🛠 Features\n\n- **Vector-based Search**: Converts user queries into vectors for precise similarity matching in our AI product database.\n\n- **Redis-powered Caching**: Utilizes Redis to cache search results and outputs, significantly improving response times for repeated queries.\n\n- **Comprehensive AI Database**: Maintains an up-to-date collection of AI products across various categories and industries.\n\n- **LLM-powered Responses**: Leverages large language models to provide detailed, context-aware answers based on search results.\n\n- **User-friendly Interface**: Offers an intuitive design for effortless navigation and efficient AI product discovery.\n\n## 🧱 Stack\n\n- App framework: [Next.js](https://nextjs.org/)\n- Text streaming: [Vercel AI SDK](https://sdk.vercel.ai/docs)\n- LLM Model: [gpt-4o-mini](https://openai.com/)\n- Database: [Supabase](https://supabase.com/)\n- Vector: [Pgvector](https://github.com/pgvector/pgvector)\n- Embedding Model: [Jina AI](https://jina.ai/embeddings)\n- Redis Cache: [Upstash](https://upstash.com/)\n- Component library: [shadcn/ui](https://ui.shadcn.com/)\n- Headless component primitives: [Radix UI](https://www.radix-ui.com/)\n- Styling: [Tailwind CSS](https://tailwindcss.com/)\n\n## 🚀 Quickstart\n\n### 1. Clone repo\n\nrun the following command to clone the repo:\n\n```\ngit clone https://github.com/DiscovAI/DiscovAI-search\n```\n\n### 2. Install dependencies\n\n```\ncd discovai-search\npnpm i\n```\n\n### 3. Setting up Supabase\n\ncreate a supabase [project](https://supabase.com/dashboard/projects), then run the src/db/init.sql in [SQL Editor](https://supabase.com/docs/guides/database/overview) to setup database\n\n### 4. Setting up Upstash\n\nFollow the guide below to set up Upstash Redis. Create a database and obtain `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`. Refer to the [Upstash guide](https://upstash.com/blog/rag-chatbot-upstash#setting-up-upstash-redis) for instructions on how to proceed.\n\n### 4. Fill out secrets\n\n```\ncp .env.local.example .env.local\n```\n\nYour .env.local file should look like this:\n\n```\n# Required\n\n# for match documents\nNEXT_PUBLIC_SUPABASE_URL=\nNEXT_PUBLIC_SUPABASE_ANON_KEY=\n\n# for embedding query, retrieved here: https://jina.ai/embeddings/\nJINA_API_KEY=\n\n# for llm output, retrieved here: https://platform.openai.com/api-keys\nOPENAI_API_KEY=\nOPENAI_API_URL=\n\n# for llm cache and serach cache\nUPSTASH_REDIS_REST_URL=\nUPSTASH_REDIS_REST_TOKEN=\n```\n\n### 5. Run app locally\n\n```\npnpm dev\n```\n\nYou can now visit http://localhost:3000.\n\n## 🌐 Deploy\n\nYou can deploy on any saas platform like vercel, zeabur, cloudflare pages.\n\n## 🌟 History\n<picture>\n  <source\n    media=\"(prefers-color-scheme: dark)\"\n    srcset=\"\n      https://api.star-history.com/svg?repos=DiscovAI/DiscovAI-search&type=Date&theme=dark\n    \"\n  />\n  <source\n    media=\"(prefers-color-scheme: light)\"\n    srcset=\"\n      https://api.star-history.com/svg?repos=DiscovAI/DiscovAI-search&type=Date\n    \"\n  />\n  <img\n    alt=\"Star History Chart\"\n    src=\"https://api.star-history.com/svg?repos=DiscovAI/DiscovAI-search&type=Date\"\n  />\n</picture>\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"src/app/globals.css\",\n    \"baseColor\": \"zinc\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"magicui\": \"@/components/magicui\"\n  }\n}\n"
  },
  {
    "path": "next.config.mjs",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  images: {\n    remotePatterns: [\n      {\n        protocol: \"https\",\n        hostname: \"**\",\n      },\n      {\n        protocol: \"http\",\n        hostname: \"**\",\n      },\n    ],\n  },\n  compiler: {\n    removeConsole: !!process.env.CI,\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"discovai-search\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/openai\": \"^0.0.40\",\n    \"@langchain/community\": \"^0.2.21\",\n    \"@microsoft/fetch-event-source\": \"^2.0.1\",\n    \"@next/env\": \"^14.2.3\",\n    \"@radix-ui/react-accordion\": \"^1.2.0\",\n    \"@radix-ui/react-avatar\": \"^1.1.0\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.0.6\",\n    \"@radix-ui/react-hover-card\": \"^1.0.7\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/react-label\": \"^2.0.2\",\n    \"@radix-ui/react-select\": \"^2.0.0\",\n    \"@radix-ui/react-separator\": \"^1.0.3\",\n    \"@radix-ui/react-slot\": \"^1.0.2\",\n    \"@radix-ui/react-switch\": \"^1.0.3\",\n    \"@radix-ui/react-tabs\": \"^1.1.0\",\n    \"@radix-ui/react-toast\": \"^1.1.5\",\n    \"@radix-ui/react-toggle\": \"^1.0.3\",\n    \"@radix-ui/react-tooltip\": \"^1.0.7\",\n    \"@supabase/supabase-js\": \"^2.44.4\",\n    \"@t3-oss/env-nextjs\": \"^0.10.1\",\n    \"@tanstack/react-query\": \"^5.32.0\",\n    \"@upstash/ratelimit\": \"^2.0.1\",\n    \"@upstash/redis\": \"^1.33.0\",\n    \"ai\": \"^3.2.38\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"framer-motion\": \"^11.1.7\",\n    \"geist\": \"^1.3.0\",\n    \"ioredis\": \"^5.4.1\",\n    \"lodash\": \"^4.17.21\",\n    \"lucide-react\": \"^0.376.0\",\n    \"next\": \"14.2.3\",\n    \"next-themes\": \"^0.3.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"react-hot-toast\": \"^2.4.1\",\n    \"react-markdown\": \"^9.0.1\",\n    \"react-textarea-autosize\": \"^8.5.3\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"tailwind-merge\": \"^2.3.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"zod\": \"^3.23.8\",\n    \"zustand\": \"^4.5.2\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/typography\": \"^0.5.13\",\n    \"@types/lodash\": \"^4.17.0\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"eslint\": \"^8\",\n    \"eslint-config-next\": \"14.2.3\",\n    \"postcss\": \"^8\",\n    \"prettier\": \"^3.2.5\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"^5\"\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": "src/app/api/chat/route.ts",
    "content": "// app/api/stream/route.ts\n\nimport { embeddingVectorCacheKey, llmResultCacheKey, redis } from \"@/db/redis\";\nimport { supabase } from \"@/db/supabase\";\nimport { generateQueyEmbedding } from \"@/lib/chat/embedding\";\nimport { genLLMTextChunk, translate } from \"@/lib/chat/llm\";\nimport { addRefToUrl, genStream, sleep } from \"@/lib/utils\";\nimport { StreamEvent } from \"@/schema/chat\";\nimport { PostgrestError } from \"@supabase/supabase-js\";\nimport { NextRequest } from \"next/server\";\n\nexport async function POST(req: NextRequest) {\n  const body = await req.json();\n  const { query } = body;\n\n  const customReadable = new ReadableStream({\n    async start(controller) {\n      try {\n        const beginData = {\n          event: StreamEvent.BEGIN_STREAM,\n          data: { event_type: StreamEvent.BEGIN_STREAM, query: query },\n        };\n        controller.enqueue(genStream(beginData));\n        const cacheResult: null | any = await redis.get(\n          embeddingVectorCacheKey(query)\n        );\n\n        let documents: any[], queryEmbeddingError: PostgrestError;\n\n        if (cacheResult) {\n          documents = cacheResult;\n          console.log(\"search result\", \"cached\");\n        } else {\n          // match documents\n          const embedding = await generateQueyEmbedding(\n            await translate({ query })\n          );\n\n          let result = await supabase.rpc(\"match_embeddings\", {\n            query_embedding: embedding, // Pass the embedding you want to compare\n            match_threshold: 0.78, // Choose an appropriate threshold for your data\n            match_count: 15, // Choose the number of matches\n          });\n          documents = result.data;\n          queryEmbeddingError = result.error;\n        }\n\n        if (queryEmbeddingError) {\n          console.error(queryEmbeddingError);\n          controller.enqueue(\n            genStream({\n              event: StreamEvent.ERROR,\n              data: {\n                event_type: StreamEvent.ERROR,\n                detail: \"error on query embeddings\",\n              },\n            })\n          );\n          controller.close();\n        }\n        redis.setex(\n          embeddingVectorCacheKey(query),\n          60 * 60 * 24, // 1 day\n          JSON.stringify(documents)\n        );\n        // filter for unique docs\n        const uniqueDocuments = [\n          ...new Set(documents.map((tool) => tool.metadata.url)),\n        ].map((url) => documents.find((tool) => tool.metadata.url === url));\n\n        for (let doc of uniqueDocuments) {\n          doc.metadata.url = addRefToUrl(doc.metadata.url);\n        }\n\n        documents = uniqueDocuments.slice(0, 5);\n\n        const searchResult = documents.map((d) => {\n          const safeContent = d.chunk_text.includes(\"DESCRIPTION\")\n            ? d.chunk_text?.split(\"---\")?.[0]?.split(\"DESCRIPTION:\")?.[1]\n            : d.chunk_text;\n          return {\n            title: d.metadata.title,\n            url: d.metadata.url,\n            content: safeContent,\n            description: safeContent,\n            screenshot_url: d.screenshot_url,\n          };\n        });\n\n        controller.enqueue(\n          genStream({\n            event: StreamEvent.SEARCH_RESULTS,\n            data: {\n              event_type: StreamEvent.SEARCH_RESULTS,\n              results: searchResult,\n              images: uniqueDocuments.map((r) => r.screenshot_url),\n            },\n          })\n        );\n\n        // stream llm text chunk\n        const llmKey = llmResultCacheKey(query);\n        const llmCache: string | null = await redis.get(llmKey);\n        let gathered = \"\";\n        if (llmCache) {\n          console.log(\"llm result cache\", \"cached\");\n          gathered = llmCache;\n          // simulate stream\n          let cacheArray = llmCache.split(\" \");\n          for await (const c of cacheArray) {\n            await sleep(10);\n            controller.enqueue(\n              genStream({\n                event: StreamEvent.TEXT_CHUNK,\n                data: {\n                  event_type: StreamEvent.TEXT_CHUNK,\n                  text: c + \" \",\n                },\n              })\n            );\n          }\n        } else {\n          const stream = await genLLMTextChunk({\n            query,\n            contexts: documents,\n          });\n          for await (const chunk of stream.textStream) {\n            controller.enqueue(\n              genStream({\n                event: StreamEvent.TEXT_CHUNK,\n                data: {\n                  event_type: StreamEvent.TEXT_CHUNK,\n                  text: chunk,\n                },\n              })\n            );\n            gathered += chunk;\n          }\n        }\n        redis.setex(llmKey, 60 * 60 * 12, gathered);\n\n        // more results or related query\n        const moreTools = uniqueDocuments.slice(5);\n        controller.enqueue(\n          genStream({\n            event: StreamEvent.MORE_RESULTS,\n            data: {\n              event_type: StreamEvent.MORE_RESULTS,\n              more_results: moreTools.map((d) => ({\n                title: d.metadata.title,\n                url: d.metadata.url,\n                screenshot_url: d.screenshot_url,\n              })),\n            },\n          })\n        );\n\n        controller.enqueue(\n          genStream({\n            event: StreamEvent.FINAL_RESPONSE,\n            data: {\n              event_type: StreamEvent.FINAL_RESPONSE,\n              message: gathered,\n            },\n          })\n        );\n\n        controller.enqueue(\n          genStream({\n            event: StreamEvent.STREAM_END,\n            data: { event_type: StreamEvent.STREAM_END, thread_id: null },\n          })\n        );\n\n        controller.close();\n      } catch (error) {\n        console.error(error);\n        controller.enqueue(\n          genStream({\n            event: StreamEvent.ERROR,\n            data: {\n              event_type: StreamEvent.ERROR,\n              detail: \"Oops~\",\n            },\n          })\n        );\n        controller.close();\n      }\n    },\n  });\n\n  return new Response(customReadable, {\n    headers: {\n      \"Content-Type\": \"text/event-stream\",\n      \"Cache-Control\": \"no-cache\",\n      Connection: \"keep-alive\",\n    },\n  });\n}\n"
  },
  {
    "path": "src/app/api/join-wait/route.ts",
    "content": "import { NextRequest } from \"next/server\";\nimport { supabase } from \"@/db/supabase\";\n\nasync function postHandler(req: NextRequest) {\n  try {\n    const body = await req.json();\n    const { email } = body;\n    const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$/;\n    if (!emailRegex.test(email)) {\n      return new Response(JSON.stringify(\"Invalid email format\"), {\n        status: 400,\n      });\n    }\n    const { error, data } = await supabase\n      .from(\"searchzero-waitlist\")\n      .insert({\n        email_address: email,\n      })\n      .select();\n    if (error) {\n      if (error.code === \"23505\") {\n        return new Response(\n          JSON.stringify({\n            isSuc: true,\n            code: 0,\n            msg: \"Already subscribed\",\n          })\n        );\n      }\n      throw error;\n    }\n    if (data && data.length) {\n      return new Response(\n        JSON.stringify({\n          isSuc: true,\n          code: 0,\n          msg: \"Subscription successful\",\n        })\n      );\n    }\n  } catch (error) {\n    console.error(error);\n\n    return new Response(\"Failed to subscribe, pleas Try again\", {\n      status: 501,\n    });\n  }\n}\n\nexport const POST = postHandler;\nexport const runtime = \"edge\";\n"
  },
  {
    "path": "src/app/blocked/page.tsx",
    "content": "export default function Blocked() {\n  return (\n    <div>\n      <main>\n        <h3>Access blocked.</h3>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 60 30% 98%;\n    --foreground: 0 0% 3.9%;\n\n    --card: 0 0% 96.1%;\n    --card-foreground: 0 0% 45.1%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 0 0% 3.9%;\n\n    --primary: 0 0% 9%;\n    --primary-foreground: 0 0% 98%;\n\n    --secondary: 0 0% 96.1%;\n    --secondary-foreground: 0 0% 9%;\n\n    --muted: 0 0% 96.1%;\n    --muted-foreground: 0 0% 45.1%;\n\n    --accent: 240 4.8% 95.9%;\n    --accent-foreground: 240 5.9% 10%;\n\n    --tint: 27 100% 49.8%;\n    --tint-foreground: 25 76% 31%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 0 0% 98%;\n\n    --border: 0 0% 89.8%;\n    --input: 0 0% 89.8%;\n    --ring: 0 0% 89.8%;\n\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 180 2% 10%;\n    --foreground: 0 0% 98%;\n\n    --card: 180 3% 13%;\n    --card-foreground: 0 0% 63.9%;\n\n    --popover: 180 2% 10%;\n    --popover-foreground: 0 0% 98%;\n\n    --primary: 0 0% 98%;\n    --primary-foreground: 0 0% 9%;\n\n    --secondary: 0 0% 14.9%;\n    --secondary-foreground: 0 0% 98%;\n\n    --muted: 0 0% 14.9%;\n    --muted-foreground: 0 0% 63.9%;\n\n    --accent: 240 3.7% 15.9%;\n    --accent-foreground: 0 0% 98%;\n\n    --tint: 22.4 100% 53%;\n    --tint-foreground: 0 0% 98%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 0 0% 98%;\n\n    --border: 0 0% 14.9%;\n    --input: 0 0% 14.9%;\n    --ring: 0 0% 14.9%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n.cl-internal-11ttlho {\n  display: none;\n  opacity: 0;\n}\n\n.cl-internal-180wb59 {\n  display: none;\n  opacity: 0;\n}\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import { Navbar } from \"@/components/nav\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { LinkConfig, SiteConfig } from \"@/config/sites\";\nimport { cn } from \"@/lib/utils\";\nimport Providers from \"@/providers\";\nimport { GeistSans } from \"geist/font/sans\";\nimport type { Metadata } from \"next\";\nimport Script from \"next/script\";\nimport { Toaster } from \"react-hot-toast\";\nimport \"./globals.css\";\n\nconst title = SiteConfig.metaTitle;\nconst description = SiteConfig.desc;\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(LinkConfig.site),\n  title,\n  description,\n  keywords: [\n    \"searchgpt\",\n    \"topaitools\",\n    \"ai\",\n    \"chatgpt\",\n    \"discov-ai\",\n    \"discover ai\",\n    \"search engine\",\n    \"top ai traffic\",\n    \"ai search engine\",\n  ],\n  openGraph: {\n    title,\n    description,\n    images: \"/og.png\",\n    url: new URL(LinkConfig.site),\n    type: \"website\",\n    siteName: SiteConfig.name,\n  },\n  twitter: {\n    title,\n    description,\n    card: \"summary_large_image\",\n    creator: \"@ruiyanghim\",\n    images: \"/og.png\",\n    site: LinkConfig.site,\n  },\n  icons: [\"/favicon.svg\"],\n  alternates: {\n    canonical: \"./\",\n  },\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <>\n      <html lang=\"en\" suppressHydrationWarning>\n        <body className={cn(\"antialiased\", GeistSans.className)}>\n          <Providers>\n            <ThemeProvider\n              attribute=\"class\"\n              defaultTheme=\"dark\"\n              enableSystem\n              disableTransitionOnChange\n            >\n              <Navbar />\n              {children}\n              <Toaster />\n            </ThemeProvider>\n          </Providers>\n          <Script\n            defer\n            src=\"https://ryan-umami-mamimami.vercel.app/script.js\"\n            data-website-id=\"5bcb1ea9-c57e-44fb-9a8b-d1f4af19c179\"\n          ></Script>\n        </body>\n      </html>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/app/page.tsx",
    "content": "import { ChatPanel } from \"@/components/chat-panel\";\nimport { Suspense } from \"react\";\n\nexport default function Home() {\n  return (\n    <>\n      <div className=\"h-screen\">\n        <div className=\"flex grow h-full mx-auto max-w-screen-md px-4 md:px-8\">\n          <Suspense>\n            <ChatPanel />\n          </Suspense>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/app/robots.txt",
    "content": "User-Agent: *\nAllow: /\nDisallow: /auth"
  },
  {
    "path": "src/app/waitlist/page.tsx",
    "content": "import HeroLanding from \"@/components/landing/hero\";\nimport PreviewLanding from \"@/components/landing/preview-landing\";\nexport default function Page() {\n  return (\n    <>\n      <HeroLanding />\n      <PreviewLanding />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/ask-input.tsx",
    "content": "import TextareaAutosize from \"react-textarea-autosize\";\nimport { useState } from \"react\";\nimport { Button } from \"./ui/button\";\nimport { ArrowRight, ArrowUp } from \"lucide-react\";\nconst InputBar = ({\n  input,\n  setInput,\n}: {\n  input: string;\n  setInput: (input: string) => void;\n}) => {\n  return (\n    <div className=\"w-full flex rounded-2xl focus:outline-none px-2 py-1 bg-card border-2 \">\n      <div className=\"w-full\">\n        <TextareaAutosize\n          className=\"w-full bg-transparent text-md resize-none focus:outline-none p-2\"\n          placeholder=\"the best ai tools for...\"\n          onChange={(e) => setInput(e.target.value)}\n          value={input}\n        />\n      </div>\n      <div className=\"flex justify-between\">\n        <div className=\"flex items-center gap-2\">\n          <Button\n            type=\"submit\"\n            variant=\"default\"\n            size=\"icon\"\n            className=\"rounded-full bg-tint aspect-square h-8 w-8 disabled:opacity-20 hover:bg-tint/80 overflow-hidden\"\n            disabled={input.trim().length < 5}\n          >\n            <ArrowRight size={20} className=\"\" />\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst FollowingUpInput = ({\n  input,\n  setInput,\n}: {\n  input: string;\n  setInput: (input: string) => void;\n}) => {\n  return (\n    <div className=\"w-full flex flex-row rounded-full focus:outline-none px-2 py-1 bg-card border-2 items-center \">\n      <div className=\"w-full\">\n        <TextareaAutosize\n          className=\"w-full bg-transparent text-md resize-none focus:outline-none p-2\"\n          placeholder=\"ask follow up questions...\"\n          onChange={(e) => setInput(e.target.value)}\n          value={input}\n        />\n      </div>\n      <div className=\"flex items-center gap-2\">\n        {/* <ProToggle /> */}\n        <Button\n          type=\"submit\"\n          variant=\"default\"\n          size=\"icon\"\n          className=\"rounded-full bg-tint aspect-square h-8 w-8 disabled:opacity-20 hover:bg-tint/80 overflow-hidden\"\n          disabled={input.trim().length < 5}\n        >\n          <ArrowUp size={20} />\n        </Button>\n      </div>\n    </div>\n  );\n};\n\nexport const AskInput = ({\n  sendMessage,\n  isFollowingUp = false,\n}: {\n  sendMessage: (message: string) => void;\n  isFollowingUp?: boolean;\n}) => {\n  const [input, setInput] = useState(\"\");\n  return (\n    <>\n      <form\n        className=\"w-full overflow-hidden\"\n        onSubmit={(e) => {\n          if (input.trim().length < 5) return;\n          e.preventDefault();\n          sendMessage(input);\n          setInput(\"\");\n        }}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\" && !e.shiftKey) {\n            e.preventDefault();\n            if (input.trim().length < 5) return;\n            sendMessage(input);\n            setInput(\"\");\n          }\n        }}\n      >\n        {isFollowingUp ? (\n          // <FollowingUpInput input={input} setInput={setInput} />\n          <></>\n        ) : (\n          <InputBar input={input} setInput={setInput} />\n        )}\n      </form>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/components/assistant-message.tsx",
    "content": "import { MessageComponent, MessageComponentSkeleton } from \"./message\";\nimport { SearchResultsSkeleton, SearchResults } from \"./search-results\";\nimport { Section } from \"./section\";\nimport { AlertCircle } from \"lucide-react\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { ImagePreload } from \"./image-section\";\nimport { ChatMessage } from \"@/schema/chat\";\nimport MoreResults from \"./more-results\";\n\nexport function ErrorMessage({ content }: { content: string }) {\n  return (\n    <Alert variant=\"destructive\">\n      <AlertCircle className=\"h-4 w-4\" />\n      <AlertTitle>Error</AlertTitle>\n      <AlertDescription>{content}</AlertDescription>\n    </Alert>\n  );\n}\n\nexport const AssistantMessageContent = ({\n  message,\n  isStreaming = false,\n  onRelatedQuestionSelect,\n}: {\n  message: ChatMessage;\n  isStreaming?: boolean;\n  onRelatedQuestionSelect: (question: string) => void;\n}) => {\n  const {\n    sources,\n    content,\n    related_queries,\n    images,\n    is_error_message = false,\n    more_results,\n  } = message;\n\n  if (is_error_message) {\n    return <ErrorMessage content={message.content} />;\n  }\n\n  return (\n    <div className=\"flex flex-col\">\n      <Section title=\"Sources\" animate={isStreaming}>\n        {!sources || sources.length === 0 ? (\n          <SearchResultsSkeleton />\n        ) : (\n          <>\n            <SearchResults results={sources} />\n          </>\n        )}\n      </Section>\n      <ImagePreload images={images || []} />\n      <Section title=\"Answer\" animate={isStreaming} streaming={isStreaming}>\n        {content ? (\n          <MessageComponent message={message} isStreaming={isStreaming} />\n        ) : (\n          <MessageComponentSkeleton />\n        )}\n      </Section>\n      {more_results && more_results.length > 0 && (\n        <Section title=\"More Results\" animate={isStreaming}>\n          <MoreResults results={more_results} />\n        </Section>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/chat-panel.tsx",
    "content": "\"use client\";\nimport { useChat } from \"@/hooks/chat\";\nimport { useChatStore } from \"@/stores\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { AskInput } from \"./ask-input\";\n\nimport { SiteConfig } from \"@/config/sites\";\nimport { LoaderIcon, TrendingUpIcon } from \"lucide-react\";\nimport { MessageRole } from \"@/schema/chat\";\n\nimport MessagesList from \"./messages-list\";\nimport { StarterQuestionsList } from \"./starter-questions\";\n\nconst useAutoScroll = (ref: React.RefObject<HTMLDivElement>) => {\n  const { messages } = useChatStore();\n\n  useEffect(() => {\n    if (messages.at(-1)?.role === MessageRole.USER) {\n      ref.current?.scrollIntoView({\n        behavior: \"smooth\",\n        block: \"end\",\n      });\n    }\n  }, [messages, ref]);\n};\n\nconst useAutoResizeInput = (\n  ref: React.RefObject<HTMLDivElement>,\n  setWidth: (width: number) => void\n) => {\n  const { messages } = useChatStore();\n\n  useEffect(() => {\n    const updatePosition = () => {\n      if (ref.current) {\n        setWidth(ref.current.scrollWidth);\n      }\n    };\n    updatePosition();\n    window.addEventListener(\"resize\", updatePosition);\n    return () => {\n      window.removeEventListener(\"resize\", updatePosition);\n    };\n  }, [messages, ref, setWidth]);\n};\n\nconst useAutoFocus = (ref: React.RefObject<HTMLTextAreaElement>) => {\n  useEffect(() => {\n    ref.current?.focus();\n  }, [ref]);\n};\n\nexport const ChatPanel = ({ threadId }: { threadId?: number }) => {\n  const searchParams = useSearchParams();\n  const queryMessage = searchParams.get(\"q\");\n  const hasRun = useRef(false);\n\n  const { handleSend, streamingMessage, isStreamingMessage } = useChat();\n  const { messages, setMessages, setThreadId } = useChatStore();\n\n  const [width, setWidth] = useState(0);\n  const messagesRef = useRef<HTMLDivElement | null>(null);\n  const messageBottomRef = useRef<HTMLDivElement | null>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n\n  useAutoScroll(messageBottomRef);\n  useAutoResizeInput(messagesRef, setWidth);\n  useAutoFocus(inputRef);\n\n  useEffect(() => {\n    if (queryMessage && !hasRun.current) {\n      setThreadId(null);\n      hasRun.current = true;\n      handleSend(queryMessage);\n    }\n  }, [queryMessage]);\n\n  useEffect(() => {\n    if (messages.length == 0) {\n      setThreadId(null);\n    }\n  }, [messages, setThreadId]);\n\n  return (\n    <>\n      {messages.length > 0 ? (\n        <div ref={messagesRef} className=\"pt-10 w-full relative\">\n          <MessagesList\n            messages={messages}\n            streamingMessage={streamingMessage}\n            isStreamingMessage={isStreamingMessage}\n            onRelatedQuestionSelect={handleSend}\n          />\n          <div ref={messageBottomRef} className=\"h-0\" />\n          <div\n            className=\"bottom-12 fixed px-2 max-w-screen-md justify-center items-center md:px-2\"\n            style={{ width: `${width}px` }}\n          >\n            <AskInput isFollowingUp sendMessage={handleSend} />\n          </div>\n        </div>\n      ) : (\n        <div className=\"w-full flex flex-col justify-center items-center\">\n          <div className=\"flex flex-col items-center justify-center mb-8\">\n            <span className=\"text-3xl\">{SiteConfig.subPanel}</span>\n          </div>\n          <AskInput sendMessage={handleSend} />\n          <div className=\"w-full flex flex-row px-3 justify-between items-center space-y-2 pt-8\">\n            <div className=\"hidden text-tint lg:flex space-x-1 items-center\">\n              <span>People are searching</span>\n              <TrendingUpIcon className=\"w-4 h-4\" />\n            </div>\n            <StarterQuestionsList handleSend={handleSend} />\n          </div>\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "src/components/image-section.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\n\"use client\";\n\nimport { Skeleton } from \"./ui/skeleton\";\nexport const ImageSectionSkeleton = () => {\n  return (\n    <>\n      <div className=\"my-4 grid grid-cols-1 gap-2 lg:grid-cols-2 w-full\">\n        {[...Array(4)].map((_, index) => (\n          <div className=\"w-full h-full\" key={`image-skeleton-${index}`}>\n            <Skeleton className=\"rounded-md object-cover shadow-none border-none w-full bg-card h-[160px] \" />\n          </div>\n        ))}\n      </div>\n    </>\n  );\n};\n\nexport function ImagePreload({ images }: { images: string[] }) {\n  if (images && images.length > 0) {\n    return (\n      <div className=\"flex flex-wrap\">\n        {images.map((image) => (\n          <img key={image} src={image} className=\"w-1 h-1 opacity-0\" />\n        ))}\n      </div>\n    );\n  }\n  return null;\n}\n"
  },
  {
    "path": "src/components/landing/hero.tsx",
    "content": "import { Icons } from \"@/components/shared/icons\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport Link from \"next/link\";\nimport { TypingTitle } from \"./typing-title\";\nimport { SubscribeForm } from \"./wailtlist\";\n\nexport default async function HeroLanding() {\n  return (\n    <section className=\"space-y-6 py-12 sm:py-20 lg:py-20\">\n      <div className=\"container flex max-w-5xl flex-col items-center gap-5 text-center\">\n        <Link\n          href=\"https://x.com/ruiyanghim/status/1816801501161062674\"\n          className={cn(\n            buttonVariants({ variant: \"outline\", size: \"sm\", rounded: \"full\" }),\n            \"px-4\"\n          )}\n          target=\"_blank\"\n        >\n          <span className=\"mr-3\">🎉</span>\n          <span className=\"hidden md:flex\">Introducing&nbsp;</span> DiscovAI on{\" \"}\n          <Icons.twitter className=\"ml-2 size-3.5\" />\n        </Link>\n\n        <TypingTitle />\n\n        <p\n          className=\"max-w-2xl text-balance leading-normal text-muted-foreground sm:text-base sm:leading-8 lg:text-xl\"\n          style={{ animationDelay: \"0.35s\", animationFillMode: \"forwards\" }}\n        >\n          Stay ahead in AI with DiscovAI, Your Go-To Source for the Latest\n          <span className=\"text-tint\">\n            {\" \"}\n            AI Products | News | Companies | Models\n          </span>\n        </p>\n        <SubscribeForm />\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/components/landing/preview-landing.tsx",
    "content": "import Image from \"next/image\";\n\nimport MaxWidthWrapper from \"@/components/shared/max-width-wrapper\";\n\nexport default function PreviewLanding() {\n  return (\n    <div className=\"pb-6 sm:pb-16\">\n      <MaxWidthWrapper>\n        <div className=\"rounded-xl md:bg-muted/30 md:p-3.5 md:ring-1 md:ring-inset md:ring-border\">\n          <div className=\"relative aspect-video overflow-hidden rounded-xl border md:rounded-lg\">\n            <Image\n              className=\"size-full object-cover object-center dark:opacity-85\"\n              src=\"/demo.png\"\n              alt=\"preview landing\"\n              width={2000}\n              height={1000}\n              priority={true}\n            />\n          </div>\n        </div>\n      </MaxWidthWrapper>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/landing/typing-title.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport {\n  TypewriterEffect,\n  TypewriterEffectSmooth,\n} from \"../ui/typewriter-effect\";\nconst typingTextList = [\n  {\n    text: \"Your\",\n  },\n  {\n    text: \"Serach \",\n  },\n  {\n    text: \"Engine\",\n  },\n  {\n    text: \"for \",\n  },\n  {\n    text: \"Anything\",\n  },\n  {\n    text: \"About AI\",\n    className: \"text-tint dark:text-tint\",\n  },\n];\n\nexport function TypingTitle() {\n  const [textList, setTextList] = useState(typingTextList);\n  // useEffect(() => {\n  //   let effect;\n  //   setTimeout(() => {\n  //     effect = setInterval(() => {\n  //       console.log(\"setting\");\n  //       const newTextList = [...textList];\n  //       newTextList[newTextList.length - 1].text = \"Models\";\n  //       setTextList(newTextList);\n  //     }, 1000);\n  //   }, 2000);\n  // }, []);\n  return (\n    <>\n      <h1 className=\"flex lg:hidden items-center text-balance text-3xl font-extrabold tracking-tight md:text-5xl\">\n        Your Serach Engine for Anything About AI\n      </h1>\n      <h1 className=\"hidden lg:flex items-center text-balance text-3xl font-extrabold tracking-tight sm:text-5xl md:text-6xl lg:text-[66px]\">\n        <TypewriterEffectSmooth words={textList} />\n      </h1>\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/landing/wailtlist.tsx",
    "content": "\"use client\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { cn, nFormatter } from \"@/lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { Icons } from \"../shared/icons\";\nimport { useState, FC } from \"react\";\nimport toast from \"react-hot-toast\";\n\nimport { Loader2, ChevronRight } from \"lucide-react\";\n\nexport function WailtList() {\n  return (\n    <div\n      className=\"flex justify-center space-x-2 md:space-x-4\"\n      style={{ animationDelay: \"0.4s\", animationFillMode: \"forwards\" }}\n    >\n      <Input\n        type=\"email\"\n        placeholder=\"Email\"\n        className=\"h-10 rounded-md px-8\"\n      />\n      <Button size={\"lg\"} rounded={\"full\"} className={\"gap-2\"}>\n        <span>Join Waitlist Now</span>\n        <Icons.arrowRight className=\"size-4\" />\n      </Button>\n    </div>\n  );\n}\n\nexport const SubscribeForm: FC = () => {\n  const [email, setEmail] = useState<string>(\"\");\n  const [message, setMessage] = useState<string>(\"\");\n  const [loading, setLoading] = useState<boolean>(false);\n\n  const handleSubscribe = async (e: React.FormEvent) => {\n    e.preventDefault();\n    // Validate email format\n    const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$/;\n    if (!emailRegex.test(email)) {\n      toast(\"Please enter a valid email address\", {\n        id: \"subscripe-toast\",\n      });\n      return;\n    }\n\n    try {\n      setLoading(true);\n      toast.loading(\"🙏 Thank you...\", { id: \"subscripe-toast\" });\n      const response = await fetch(\"/api/join-waitlist\", {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({ email }),\n      });\n\n      if (!response.ok) {\n        throw new Error(\"Failed to subscribe\");\n      }\n\n      toast.success(\"👍 You have joined the wailt list of DiscovAI !\", {\n        id: \"subscripe-toast\",\n      });\n      setMessage(\"Subscription successful!\");\n      setEmail(\"\");\n    } catch (error: any) {\n      toast.error(\"Something wrong happens, please try again!\", {\n        id: \"subscripe-toast\",\n      });\n\n      setMessage(error.message);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <form\n      onSubmit={handleSubscribe}\n      className=\"flex justify-center space-x-2 md:space-x-4\"\n      style={{ animationDelay: \"0.4s\", animationFillMode: \"forwards\" }}\n    >\n      <div className=\"flex-1\">\n        <label htmlFor=\"email\" className=\"sr-only\">\n          Email\n        </label>\n\n        <Input\n          type=\"email\"\n          placeholder=\"Your Email address\"\n          value={email}\n          className=\"h-10 rounded-md px-8\"\n          onChange={(e) => setEmail(e.target.value)}\n        />\n      </div>\n\n      <Button\n        type=\"submit\"\n        disabled={loading}\n        size={\"lg\"}\n        rounded={\"full\"}\n        className={\"gap-2\"}\n      >\n        {loading ? (\n          <>\n            <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n            Please wait\n          </>\n        ) : (\n          <>\n            <ChevronRight className=\"mr-2 h-4 w-4\" />\n            Join Waitlist\n            {/* <Icons.arrowRight className=\"size-4\" /> */}\n          </>\n        )}\n      </Button>\n    </form>\n  );\n};\n"
  },
  {
    "path": "src/components/magicui/typing-animation.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface TypingAnimationProps {\n  duration?: number;\n  className?: string;\n  textList: string[];\n}\n\nexport default function TypingAnimation({\n  duration = 200,\n  className,\n  textList,\n}: TypingAnimationProps) {\n  const [displayedText, setDisplayedText] = useState<string>(\"\");\n  const [i, setI] = useState<number>(0);\n  const [wordI, setWordI] = useState(0);\n\n  useEffect(() => {\n    const typingEffect = setInterval(() => {\n      if (i < textList[wordI].length) {\n        setDisplayedText(textList[wordI].substring(0, i + 1));\n        setI(i + 1);\n      } else {\n        setTimeout(() => {\n          setWordI((wordI + 1) % textList.length);\n          setI(0);\n        }, 1000);\n        // clearInterval(typingEffect);\n      }\n    }, duration);\n\n    return () => {\n      clearInterval(typingEffect);\n    };\n  }, [duration, i, wordI]);\n\n  return (\n    <h1\n      className={cn(\n        \"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm\",\n        className\n      )}\n    >\n      {displayedText ? displayedText : textList[wordI]}\n    </h1>\n  );\n}\n"
  },
  {
    "path": "src/components/markdown.tsx",
    "content": "import { FC, memo } from \"react\";\nimport ReactMarkdown, { Options } from \"react-markdown\";\n\nexport const MemoizedReactMarkdown: FC<Options> = memo(\n  ReactMarkdown,\n  (prevProps, nextProps) =>\n    prevProps.children === nextProps.children &&\n    prevProps.className === nextProps.className,\n);\n"
  },
  {
    "path": "src/components/message.tsx",
    "content": "import React, { FC, memo, useEffect, useMemo, useState } from \"react\";\nimport { MemoizedReactMarkdown } from \"./markdown\";\nimport rehypeRaw from \"rehype-raw\";\n\nimport _ from \"lodash\";\nimport { cn } from \"@/lib/utils\";\nimport { Skeleton } from \"./ui/skeleton\";\nimport { ChatMessage } from \"@/schema/chat\";\n\nfunction chunkString(str: string): string[] {\n  const words = str.split(\" \");\n  const chunks = _.chunk(words, 2).map((chunk) => chunk.join(\" \") + \" \");\n  return chunks;\n}\n\nexport interface MessageProps {\n  message: ChatMessage;\n  isStreaming?: boolean;\n}\n\nconst CitationText = ({ number, href }: { number: number; href: string }) => {\n  return `\n  <button className=\"select-none no-underline\">\n  <a className=\"\" href=\"${href}\" target=\"_blank\">\n        <span className=\"relative -top-[0rem] inline-flex\">\n          <span className=\"h-[1rem] min-w-[1rem] items-center justify-center rounded-full  text-center px-1 text-xs font-mono bg-muted text-[0.60rem] text-muted-foreground\">\n            ${number}\n          </span>\n        </span>\n      </a>\n    </button>`;\n};\n\nconst Text = ({\n  children,\n  isStreaming,\n  containerElement = \"p\",\n}: {\n  children: React.ReactNode;\n  isStreaming: boolean;\n  containerElement: React.ElementType;\n}) => {\n  const renderText = (node: React.ReactNode): React.ReactNode => {\n    if (typeof node === \"string\") {\n      const chunks = isStreaming ? chunkString(node) : [node];\n      return chunks.flatMap((chunk, index) => {\n        return (\n          <span\n            key={`${index}-streaming`}\n            className={cn(\n              isStreaming ? \"animate-in fade-in-25 duration-700\" : \"\"\n            )}\n          >\n            {chunk}\n          </span>\n        );\n      });\n    } else if (React.isValidElement(node)) {\n      return React.cloneElement(\n        node,\n        node.props,\n        renderText(node.props.children)\n      );\n    } else if (Array.isArray(node)) {\n      return node.map((child, index) => (\n        <React.Fragment key={index}>{renderText(child)}</React.Fragment>\n      ));\n    }\n    return null;\n  };\n\n  const text = renderText(children);\n  return React.createElement(containerElement, {}, text);\n};\n\nconst StreamingParagraph = memo(\n  ({ children }: React.HTMLProps<HTMLParagraphElement>) => {\n    return (\n      <Text isStreaming={true} containerElement=\"p\">\n        {children}\n      </Text>\n    );\n  }\n);\nconst Paragraph = memo(\n  ({ children }: React.HTMLProps<HTMLParagraphElement>) => {\n    return (\n      <Text isStreaming={false} containerElement=\"p\">\n        {children}\n      </Text>\n    );\n  }\n);\n\nconst ListItem = memo(({ children }: React.HTMLProps<HTMLLIElement>) => {\n  return (\n    <Text isStreaming={false} containerElement=\"li\">\n      {children}\n    </Text>\n  );\n});\n\nconst StreamingListItem = memo(\n  ({ children }: React.HTMLProps<HTMLLIElement>) => {\n    return (\n      <Text isStreaming={true} containerElement=\"li\">\n        {children}\n      </Text>\n    );\n  }\n);\n\nStreamingParagraph.displayName = \"StreamingParagraph\";\nParagraph.displayName = \"Paragraph\";\nListItem.displayName = \"ListItem\";\nStreamingListItem.displayName = \"StreamingListItem\";\n\nexport const MessageComponent: FC<MessageProps> = ({\n  message,\n  isStreaming = false,\n}) => {\n  const { content, sources } = message;\n  const [parsedMessage, setParsedMessage] = useState<string>(content);\n\n  useEffect(() => {\n    const citationRegex = /(\\[\\d+\\])/g;\n    const newMessage = content.replace(citationRegex, (match) => {\n      const number = match.slice(1, -1);\n      const source = sources?.find(\n        (source, idx) => idx + 1 === parseInt(number)\n      );\n      return CitationText({\n        number: parseInt(number),\n        href: source?.url ?? \"\",\n      });\n    });\n    setParsedMessage(newMessage);\n  }, [content, sources]);\n\n  return (\n    <MemoizedReactMarkdown\n      components={{\n        // TODO: For some reason, can't pass props into the components\n        // @ts-ignore\n        p: isStreaming ? StreamingParagraph : Paragraph,\n        // @ts-ignore\n        li: isStreaming ? StreamingListItem : ListItem,\n      }}\n      className=\"prose dark:prose-invert inline leading-relaxed break-words \"\n      rehypePlugins={[rehypeRaw]}\n    >\n      {parsedMessage}\n    </MemoizedReactMarkdown>\n  );\n};\n\nexport const MessageComponentSkeleton = () => {\n  return (\n    <>\n      <Skeleton className=\"w-full py-4 bg-card\">\n        <div className=\"flex flex-col gap-4\">\n          <Skeleton className=\"mx-5 h-2 bg-primary/30\" />\n          <Skeleton className=\"mx-5 h-2 bg-primary/30 mr-20\" />\n          <Skeleton className=\"mx-5 h-2 bg-primary/30 mr-40\" />\n        </div>\n      </Skeleton>\n    </>\n  );\n};\n"
  },
  {
    "path": "src/components/messages-list.tsx",
    "content": "import { AssistantMessageContent } from \"./assistant-message\";\nimport { Separator } from \"./ui/separator\";\nimport { UserMessageContent } from \"./user-message\";\nimport { memo } from \"react\";\nimport { ChatMessage, MessageRole } from \"@/schema/chat\";\n\nconst MessagesList = ({\n  messages,\n  streamingMessage,\n  isStreamingMessage,\n  onRelatedQuestionSelect,\n}: {\n  messages: ChatMessage[];\n  streamingMessage: ChatMessage | null;\n  isStreamingMessage: boolean;\n  onRelatedQuestionSelect: (question: string) => void;\n}) => {\n  return (\n    <div className=\"flex flex-col pb-28\">\n      {messages.map((message, index) =>\n        message.role === MessageRole.USER ? (\n          <UserMessageContent key={index} message={message} />\n        ) : (\n          <div key={index}>\n            <AssistantMessageContent\n              key={index}\n              message={message}\n              onRelatedQuestionSelect={onRelatedQuestionSelect}\n            />\n            {index !== messages.length - 1 && <Separator />}\n          </div>\n        )\n      )}\n      {streamingMessage && isStreamingMessage && (\n        <AssistantMessageContent\n          message={streamingMessage}\n          isStreaming={true}\n          onRelatedQuestionSelect={onRelatedQuestionSelect}\n        />\n      )}\n    </div>\n  );\n};\n\nexport default memo(MessagesList);\n"
  },
  {
    "path": "src/components/mode-toggle.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ComputerIcon, Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nexport function ModeToggle() {\n  const { setTheme } = useTheme();\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button variant=\"ghost\" size=\"icon\">\n          <Sun className=\"h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n          <Moon className=\"absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n          <span className=\"sr-only\">Toggle theme</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        {[\"light\", \"dark\", \"system\"].map((theme) => (\n          <DropdownMenuItem\n            key={theme}\n            className=\"flex gap-2 items-center font-medium\"\n            onClick={() => setTheme(theme)}\n          >\n            {theme === \"light\" && <Sun size={12} />}\n            {theme === \"dark\" && <Moon size={12} />}\n            {theme === \"system\" && <ComputerIcon size={12} />}\n            {theme.charAt(0).toUpperCase() + theme.slice(1)}\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "src/components/more-results.tsx",
    "content": "import { PlusIcon } from \"lucide-react\";\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion\";\nimport Image from \"next/image\";\nexport default function MoreResults({\n  results,\n}: {\n  results: { url: string; title: string; screenshot_url: string }[];\n}) {\n  return (\n    <div className=\"divide-y border-t mt-2\">\n      <Accordion type=\"single\" collapsible className=\"w-full\">\n        {results.map((res, i) => (\n          <AccordionItem value={res.url} key={i}>\n            <AccordionTrigger>\n              <div className=\"text-left\">{res.title}</div>\n            </AccordionTrigger>\n            <AccordionContent>\n              <a href={res.url} target=\"_blank\">\n                <img\n                  src={res.screenshot_url}\n                  alt={res.title}\n                  width={1920}\n                  height={1080}\n                  className=\"rounded-lg\"\n                />\n              </a>\n            </AccordionContent>\n          </AccordionItem>\n        ))}\n      </Accordion>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/nav.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { ModeToggle } from \"./mode-toggle\";\nimport { useTheme } from \"next-themes\";\nimport { Button, buttonVariants } from \"./ui/button\";\nimport { PlusIcon } from \"lucide-react\";\nimport { useChatStore } from \"@/stores\";\nimport { useRouter } from \"next/navigation\";\nimport { SiteConfig, LinkConfig } from \"@/config/sites\";\nimport { GitHubLogoIcon } from \"@radix-ui/react-icons\";\nimport { Icons } from \"./shared/icons\";\nimport { cn } from \"@/lib/utils\";\nconst NewChatButton = () => {\n  return (\n    <Button variant=\"secondary\" size=\"sm\" onClick={() => (location.href = \"/\")}>\n      <PlusIcon className=\"w-4 h-4\" />\n    </Button>\n  );\n};\n\nconst TextLogo = () => {\n  // return <></>;\n  return (\n    <div className=\"text-2xl font-medium hidden sm:block\">\n      {SiteConfig.name}\n    </div>\n  );\n};\n\nexport function Navbar() {\n  const router = useRouter();\n  const { theme } = useTheme();\n  const { messages } = useChatStore();\n\n  const onHomePage = messages.length === 0;\n\n  return (\n    <header className=\"w-full flex fixed p-1 z-50 px-2 bg-background/95 justify-between items-center\">\n      <div className=\"flex items-center gap-2 p-2\">\n        <Link href=\"/\" passHref onClick={() => (location.href = \"/\")}>\n          <img\n            src={theme === \"light\" ? \"/logo.svg\" : \"/logo.svg\"}\n            alt=\"Logo\"\n            className=\"w-4 h-4 sm:w-8 sm:h-8\"\n          />\n        </Link>\n        {onHomePage ? <TextLogo /> : <NewChatButton />}\n      </div>\n      <div className=\"flex items-center gap-4 pr-2\">\n        <ModeToggle />\n        <Link\n          href={LinkConfig.github}\n          target=\"_blank\"\n          className={cn(buttonVariants({ variant: \"ghost\", size: \"icon\" }))}\n        >\n          <GitHubLogoIcon />\n        </Link>\n        <Link\n          href={LinkConfig.twitter}\n          target=\"_blank\"\n          className={cn(buttonVariants({ variant: \"ghost\", size: \"icon\" }))}\n        >\n          <Icons.twitter className=\"w-3 h-3\" />\n        </Link>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "src/components/related-questions.tsx",
    "content": "import { PlusIcon } from \"lucide-react\";\n\nexport default function RelatedQuestions({\n  questions,\n  onSelect,\n}: {\n  questions: string[];\n  onSelect: (question: string) => void;\n}) {\n  return (\n    <div className=\"divide-y border-t mt-2\">\n      {questions.map((question, index) => (\n        <div\n          key={`question-${index}`}\n          className=\"flex cursor-pointer items-center py-2 font-medium justify-between \"\n          onClick={() => onSelect(question)}\n        >\n          <span>{question.toLowerCase()}</span>\n          <PlusIcon className=\"text-tint mr-2\" size={20} />\n        </div>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/search-results.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\n\"use client\";\nimport { useState } from \"react\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Skeleton } from \"./ui/skeleton\";\nimport {\n  HoverCard,\n  HoverCardContent,\n  HoverCardTrigger,\n} from \"@/components/ui/hover-card\";\nimport { SearchResult } from \"@/schema/chat\";\n\nexport const SearchResultsSkeleton = () => {\n  return (\n    <>\n      <div className=\"flex flex-wrap w-full\">\n        {[...Array(4)].map((_, index) => (\n          <div className=\"w-1/2 md:w-1/4 p-1\" key={`skeleton-${index}`}>\n            <Skeleton className=\"rounded-md shadow-none border-none h-[70px] bg-card \" />\n          </div>\n        ))}\n      </div>\n    </>\n  );\n};\n\nexport const Logo = ({ url }: { url: string }) => {\n  return (\n    <div className=\"rounded-full overflow-hidden relative\">\n      <img\n        className=\"block relative\"\n        src={`https://www.google.com/s2/favicons?sz=128&domain=${url}`}\n        alt=\"favicon\"\n        width={16}\n        height={16}\n      />\n    </div>\n  );\n};\n\nexport function SearchResults({ results }: { results: SearchResult[] }) {\n  const [showAll, setShowAll] = useState(false);\n\n  const displayedResults = showAll ? results : results.slice(0, 3);\n  const additionalCount = results.length > 3 ? results.length - 3 : 0;\n  const additionalResults = results.slice(3, 3 + additionalCount);\n  return (\n    <div className=\"flex flex-wrap w-full \">\n      {displayedResults.map(({ title, url, content, description }, index) => {\n        const formattedUrl = new URL(url).hostname.split(\".\").slice(-2, -1)[0];\n\n        return (\n          <HoverCard key={`source-${index}`}>\n            <HoverCardTrigger asChild>\n              <div className=\"w-1/2 md:w-1/4 p-1\">\n                <a className=\"\" href={url} target=\"_blank\">\n                  <Card className=\"flex-1 rounded-md flex-col shadow-none border-none h-[70px]\">\n                    <CardContent className=\"p-2 flex flex-col justify-between h-full\">\n                      <p className=\"text-xs line-clamp-2 font-medium text-foreground/80\">\n                        {title} | {description}\n                      </p>\n                      <div className=\"flex space-x-1\">\n                        <div className=\"flex items-center space-x-2\">\n                          <div className=\"rounded-full overflow-hidden relative\">\n                            <Logo url={url} />\n                          </div>\n                          <div className=\"text-xs text-muted-foreground truncate font-medium\">\n                            {formattedUrl}\n                          </div>\n                        </div>\n                        <div className=\"text-xs text-muted-foreground font-medium\">\n                          ·\n                        </div>\n                        <div className=\"text-xs text-muted-foreground truncate font-medium\">\n                          {index + 1}\n                        </div>\n                      </div>\n                    </CardContent>\n                  </Card>\n                </a>\n              </div>\n            </HoverCardTrigger>\n            <HoverCardContent className=\"w-80 py-2\">\n              <div className=\"flex justify-between space-x-4\">\n                <div className=\"space-y-1\">\n                  <div className=\"flex items-center space-x-2\">\n                    <div className=\"rounded-full overflow-hidden relative\">\n                      <Logo url={url} />\n                    </div>\n                    <div className=\"text-xs text-muted-foreground truncate font-medium\">\n                      {formattedUrl}\n                    </div>\n                  </div>\n                  <p className=\"text-sm font-medium\">{title}</p>\n                  <span className=\"text-sm line-clamp-3 font-light text-foreground/90\">\n                    {content}\n                  </span>\n                </div>\n              </div>\n            </HoverCardContent>\n          </HoverCard>\n        );\n      })}\n      {!showAll && additionalCount > 0 && (\n        <div\n          className=\"cursor-pointer\n        w-1/2 md:w-1/4  p-1\"\n          onClick={() => setShowAll(true)}\n        >\n          <Card className=\"flex-1 rounded-md flex-col shadow-none border-none h-[70px]\">\n            <CardContent className=\"p-2 flex flex-col justify-between h-full\">\n              <div className=\"flex items-center space-x-2\">\n                {additionalResults.map(({ url }, index) => {\n                  return <Logo url={url} key={`logo-${index}`} />;\n                })}\n              </div>\n              <div className=\"text-xs text-muted-foreground truncate font-medium\">\n                View {additionalCount} more\n              </div>\n            </CardContent>\n          </Card>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/section.tsx",
    "content": "import { cn } from \"@/lib/utils\";\nimport {\n  BookOpen,\n  BookOpenIcon,\n  CameraIcon,\n  ListOrderedIcon,\n  ListPlusIcon,\n  SparkleIcon,\n  SparklesIcon,\n  StarIcon,\n  TextSearchIcon,\n} from \"lucide-react\";\n\nimport { motion } from \"framer-motion\";\n\nexport const Section = ({\n  title,\n  children,\n  animate = true,\n  streaming = false,\n}: {\n  title: \"Sources\" | \"Answer\" | \"Related\" | \"Images\" | \"More Results\";\n  children: React.ReactNode;\n  animate?: boolean;\n  streaming?: boolean;\n}) => {\n  const iconMap = {\n    Sources: BookOpenIcon,\n    Answer: SparkleIcon,\n    Related: ListPlusIcon,\n    Images: CameraIcon,\n    \"More Results\": ListOrderedIcon,\n  };\n\n  const IconComponent = iconMap[title] || StarIcon;\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col mb-8\",\n        animate ? \"animate-in fade-in duration-1000 ease-out\" : \"\"\n      )}\n    >\n      <div className=\"flex items-center space-x-2\">\n        {title === \"Answer\" && streaming ? (\n          <motion.div\n            animate={{ rotate: [0, 360] }}\n            transition={{ repeat: Infinity, duration: 1.5, ease: \"linear\" }}\n          >\n            <IconComponent size={22} />\n          </motion.div>\n        ) : (\n          <IconComponent size={22} />\n        )}\n        <div className=\"text-lg font-medium\">{title}</div>\n      </div>\n      <div className=\"pt-1\">{children}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/shared/icons.tsx",
    "content": "import {\n  AlertTriangle,\n  ArrowRight,\n  ArrowUpRight,\n  Check,\n  ChevronLeft,\n  ChevronRight,\n  Copy,\n  CreditCard,\n  File,\n  FileText,\n  HelpCircle,\n  Image,\n  Laptop,\n  Loader2,\n  LucideIcon,\n  LucideProps,\n  Moon,\n  MoreVertical,\n  Plus,\n  Puzzle,\n  Search,\n  Settings,\n  SunMedium,\n  Trash,\n  User,\n  X,\n} from \"lucide-react\";\n\nexport type Icon = LucideIcon;\n\nexport const Icons = {\n  add: Plus,\n  arrowRight: ArrowRight,\n  arrowUpRight: ArrowUpRight,\n  billing: CreditCard,\n  chevronLeft: ChevronLeft,\n  chevronRight: ChevronRight,\n  check: Check,\n  close: X,\n  copy: Copy,\n  ellipsis: MoreVertical,\n  gitHub: ({ ...props }: LucideProps) => (\n    <svg\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      data-prefix=\"fab\"\n      data-icon=\"github\"\n      role=\"img\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 496 512\"\n      {...props}\n    >\n      <path\n        fill=\"currentColor\"\n        d=\"M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z\"\n      ></path>\n    </svg>\n  ),\n  google: ({ ...props }: LucideProps) => (\n    <svg\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      data-prefix=\"fab\"\n      data-icon=\"google\"\n      role=\"img\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 488 512\"\n      {...props}\n    >\n      <path\n        d=\"M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  nextjs: ({ ...props }: LucideProps) => (\n    <svg\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      data-prefix=\"fab\"\n      data-icon=\"nextjs\"\n      role=\"img\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 15 15\"\n      {...props}\n    >\n      <path\n        fill=\"currentColor\"\n        d=\"m4.5 4.5l.405-.293A.5.5 0 0 0 4 4.5zm3 9.5A6.5 6.5 0 0 1 1 7.5H0A7.5 7.5 0 0 0 7.5 15zM14 7.5A6.5 6.5 0 0 1 7.5 14v1A7.5 7.5 0 0 0 15 7.5zM7.5 1A6.5 6.5 0 0 1 14 7.5h1A7.5 7.5 0 0 0 7.5 0zm0-1A7.5 7.5 0 0 0 0 7.5h1A6.5 6.5 0 0 1 7.5 1zM5 12V4.5H4V12zm-.905-7.207l6.5 9l.81-.586l-6.5-9zM10 4v6h1V4z\"\n      ></path>\n    </svg>\n  ),\n  help: HelpCircle,\n  laptop: Laptop,\n  logo: Puzzle,\n  media: Image,\n  moon: Moon,\n  page: File,\n  post: FileText,\n  search: Search,\n  settings: Settings,\n  spinner: Loader2,\n  sun: SunMedium,\n  trash: Trash,\n  twitter: ({ ...props }: LucideProps) => (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      data-prefix=\"fab\"\n      data-icon=\"twitter\"\n      role=\"img\"\n      {...props}\n    >\n      <path\n        d=\"M14.258 10.152L23.176 0h-2.113l-7.747 8.813L7.133 0H0l9.352 13.328L0 23.973h2.113l8.176-9.309 6.531 9.309h7.133zm-2.895 3.293l-.949-1.328L2.875 1.56h3.246l6.086 8.523.945 1.328 7.91 11.078h-3.246zm0 0\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  user: User,\n  warning: AlertTriangle,\n};\n"
  },
  {
    "path": "src/components/shared/max-width-wrapper.tsx",
    "content": "import { ReactNode } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport default function MaxWidthWrapper({\n  className,\n  children,\n  large = false,\n}: {\n  className?: string;\n  large?: boolean;\n  children: ReactNode;\n}) {\n  return (\n    <div\n      className={cn(\n        \"container\",\n        large ? \"max-w-screen-2xl\" : \"max-w-6xl\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/components/starter-questions.tsx",
    "content": "import { ArrowRight, ArrowUpRight } from \"lucide-react\";\n\nconst starterQuestions = [\n  \"I want to make old photos high definition\",\n  \"Tools that can help me market better on Twitter\",\n  \"Auto-generated seo friendly blogs from my website\",\n  \"I need an AI girlfriend with multiple roles to choose from\",\n];\n\nexport const StarterQuestionsList = ({\n  handleSend,\n}: {\n  handleSend: (question: string) => void;\n}) => {\n  return (\n    <ul className=\"flex flex-col space-y-2 pt-2\">\n      {starterQuestions.map((question) => (\n        <li key={question} className=\"flex items-center space-x-2\">\n          <button\n            onClick={() => handleSend(question)}\n            className=\"flex gap-1 items-center font-normal hover:underline decoration-tint underline-offset-4 transition-all duration-200 ease-in-out transform hover:scale-[1.02] text-left break-words normal-case\"\n          >\n            {question}\n            <ArrowUpRight size={18} className=\"text-tint\" />\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n};\n"
  },
  {
    "path": "src/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport { type ThemeProviderProps } from \"next-themes/dist/types\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "src/components/ui/accordion.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDownIcon } from \"@radix-ui/react-icons\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <AccordionPrimitive.Item\n    ref={ref}\n    className={cn(\"border-b\", className)}\n    {...props}\n  />\n));\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Header className=\"flex\">\n    <AccordionPrimitive.Trigger\n      ref={ref}\n      className={cn(\n        \"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronDownIcon className=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200\" />\n    </AccordionPrimitive.Trigger>\n  </AccordionPrimitive.Header>\n));\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    ref={ref}\n    className=\"overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n    {...props}\n  >\n    <div className={cn(\"pb-4 pt-0\", className)}>{children}</div>\n  </AccordionPrimitive.Content>\n));\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
  },
  {
    "path": "src/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 px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7\",\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": "src/components/ui/avatar.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
  },
  {
    "path": "src/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-md 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:\n          \"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground shadow 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": "src/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 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-sm 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-9 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        lg: \"h-10 rounded-md px-8\",\n        icon: \"h-9 w-9\",\n      },\n      rounded: {\n        default: \"rounded-md\",\n        sm: \"rounded-sm\",\n        lg: \"rounded-lg\",\n        xl: \"rounded-xl\",\n        \"2xl\": \"rounded-2xl\",\n        full: \"rounded-full\",\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": "src/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-xl border bg-card text-card-foreground shadow\",\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(\"font-semibold leading-none tracking-tight\", className)}\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 {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "src/components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  DotFilledIcon,\n} from \"@radix-ui/react-icons\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRightIcon className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md\",\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <CheckIcon className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <DotFilledIcon className=\"h-4 w-4 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  );\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "src/components/ui/hover-card.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst HoverCard = React.forwardRef<\n  React.ElementRef<typeof HoverCardPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Root>\n>(({ ...props }, ref) => (\n  <HoverCardPrimitive.Root openDelay={200} closeDelay={100} {...props} />\n));\n\nHoverCard.displayName = HoverCardPrimitive.Root.displayName;\n\nconst HoverCardTrigger = HoverCardPrimitive.Trigger;\n\nconst HoverCardContent = React.forwardRef<\n  React.ElementRef<typeof HoverCardPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <HoverCardPrimitive.Content\n    ref={ref}\n    align={align}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nHoverCardContent.displayName = HoverCardPrimitive.Content.displayName;\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent };\n"
  },
  {
    "path": "src/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-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring 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": "src/components/ui/label.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "src/components/ui/select.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  CaretSortIcon,\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronUpIcon,\n} from \"@radix-ui/react-icons\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <CaretSortIcon className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronUpIcon />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronDownIcon />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <CheckIcon className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "src/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref,\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "src/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-primary/10\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "src/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-5 w-10 shrink-0 cursor-pointer items-center rounded-full border border-card shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 bg-input \",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-4 w-4 rounded-full bg-tint/80 shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:bg-primary/50\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "src/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea };\n"
  },
  {
    "path": "src/components/ui/timeline.tsx",
    "content": "//github.com/shadcn-ui/ui/pull/3374 + cursor for modifications :)\nimport React from \"react\";\nimport { CheckIcon, CircleIcon, Cross1Icon } from \"@radix-ui/react-icons\";\nimport { VariantProps, cva } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst timelineVariants = cva(\"grid\", {\n  variants: {\n    positions: {\n      left: \"[&>li]:grid-cols-[0_min-content_1fr]\",\n      right: \"[&>li]:grid-cols-[1fr_min-content]\",\n      center: \"[&>li]:grid-cols-[1fr_min-content_1fr]\",\n    },\n  },\n  defaultVariants: {\n    positions: \"left\",\n  },\n});\n\ninterface TimelineProps\n  extends React.HTMLAttributes<HTMLUListElement>,\n    VariantProps<typeof timelineVariants> {}\n\nconst Timeline = React.forwardRef<HTMLUListElement, TimelineProps>(\n  ({ children, className, positions, ...props }, ref) => {\n    return (\n      <ul\n        className={cn(timelineVariants({ positions }), className)}\n        ref={ref}\n        {...props}\n      >\n        {children}\n      </ul>\n    );\n  },\n);\nTimeline.displayName = \"Timeline\";\n\nconst timelineItemVariants = cva(\"grid items-start gap-x-2\", {\n  variants: {\n    status: {\n      done: \"text-primary\",\n      default: \"text-muted-foreground\",\n    },\n  },\n  defaultVariants: {\n    status: \"default\",\n  },\n});\n\ninterface TimelineItemProps\n  extends React.HTMLAttributes<HTMLLIElement>,\n    VariantProps<typeof timelineItemVariants> {}\n\nconst TimelineItem = React.forwardRef<HTMLLIElement, TimelineItemProps>(\n  ({ className, status, ...props }, ref) => (\n    <li\n      className={cn(timelineItemVariants({ status }), className)}\n      ref={ref}\n      {...props}\n    />\n  ),\n);\nTimelineItem.displayName = \"TimelineItem\";\n\nconst timelineDotVariants = cva(\n  \"col-start-2 col-end-3 row-start-1 row-end-2 mt-3.5 flex size-3 items-center justify-center rounded-full z-10 bg-background\",\n  {\n    variants: {\n      status: {\n        default: \"[&>*]:hidden border-tint border-[0.5px]\",\n        current:\n          \"[&>*:not(.radix-circle)]:hidden [&>.radix-circle]:bg-tint [&>.radix-circle]:fill-tint [&>.radix-circle]:size-3\",\n        done: \"bg-tint [&>*:not(.radix-check)]:hidden [&>.radix-check]:text-background [&>.radix-check]:size-2\",\n        error:\n          \"border-destructive bg-destructive [&>*:not(.radix-cross)]:hidden [&>.radix-cross]:text-background [&>.radix-cross]:size-2\",\n        custom:\n          \"[&>*:not(:nth-child(4))]:hidden [&>*:nth-child(4)]:block [&>*:nth-child(4)]:size-2\",\n      },\n    },\n    defaultVariants: {\n      status: \"default\",\n    },\n  },\n);\n\ninterface TimelineDotProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof timelineDotVariants> {\n  customIcon?: React.ReactNode;\n}\n\nconst TimelineDot = React.forwardRef<HTMLDivElement, TimelineDotProps>(\n  ({ className, status, customIcon, ...props }, ref) => (\n    <div\n      role=\"status\"\n      className={cn(\"timeline-dot\", timelineDotVariants({ status }), className)}\n      ref={ref}\n      {...props}\n    >\n      <div className=\"radix-circle size-2 rounded-full\" />\n      <CheckIcon className=\"radix-check size-2\" />\n      <Cross1Icon className=\"radix-cross size-2\" />\n      {customIcon}\n    </div>\n  ),\n);\nTimelineDot.displayName = \"TimelineDot\";\n\nconst timelineContentVariants = cva(\n  \"row-start-1 row-end-2 text-muted-foreground\",\n  {\n    variants: {\n      side: {\n        right: \"col-start-3 col-end-4 mr-auto text-left\",\n        left: \"col-start-1 col-end-2 ml-auto text-right\",\n      },\n    },\n    defaultVariants: {\n      side: \"right\",\n    },\n  },\n);\n\ninterface TimelineConentProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof timelineContentVariants> {}\n\nconst TimelineContent = React.forwardRef<HTMLDivElement, TimelineConentProps>(\n  ({ className, side, ...props }, ref) => (\n    <div\n      className={cn(timelineContentVariants({ side }), className)}\n      ref={ref}\n      {...props}\n    />\n  ),\n);\nTimelineContent.displayName = \"TimelineContent\";\n\nconst timelineHeadingVariants = cva(\n  \"row-start-2 row-end-3 line-clamp-1 max-w-full truncate\",\n  {\n    variants: {\n      side: {\n        right: \"col-start-3 col-end-4 mr-auto text-left\",\n        left: \"col-start-1 col-end-2 ml-auto text-right\",\n      },\n      variant: {\n        primary: \"text-base font-medium text-primary\",\n        secondary: \"text-sm font-light text-muted-foreground\",\n      },\n    },\n    defaultVariants: {\n      side: \"right\",\n      variant: \"primary\",\n    },\n  },\n);\n\ninterface TimelineHeadingProps\n  extends React.HTMLAttributes<HTMLParagraphElement>,\n    VariantProps<typeof timelineHeadingVariants> {}\n\nconst TimelineHeading = React.forwardRef<\n  HTMLParagraphElement,\n  TimelineHeadingProps\n>(({ className, side, variant, ...props }, ref) => (\n  <p\n    role=\"heading\"\n    aria-level={variant === \"primary\" ? 2 : 3}\n    className={cn(timelineHeadingVariants({ side, variant }), className)}\n    ref={ref}\n    {...props}\n  />\n));\nTimelineHeading.displayName = \"TimelineHeading\";\n\ninterface TimelineLineProps extends React.HTMLAttributes<HTMLHRElement> {\n  done?: boolean;\n}\n\nconst TimelineLine = React.forwardRef<HTMLHRElement, TimelineLineProps>(\n  ({ className, done = false, ...props }, ref) => {\n    return (\n      <hr\n        role=\"separator\"\n        aria-orientation=\"vertical\"\n        className={cn(\n          \"col-start-2 col-end-3 row-start-1 row-end-3 mx-auto flex h-full w-[3px] justify-center rounded-full\",\n          \"mt-6 mb-2\",\n          done ? \"bg-muted\" : \"bg-muted\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nTimelineLine.displayName = \"TimelineLine\";\n\nexport {\n  Timeline,\n  TimelineDot,\n  TimelineItem,\n  TimelineContent,\n  TimelineHeading,\n  TimelineLine,\n};\n"
  },
  {
    "path": "src/components/ui/toast.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Cross2Icon } from \"@radix-ui/react-icons\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  );\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className,\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <Cross2Icon className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold [&+div]:text-xs\", className)}\n    {...props}\n  />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90\", className)}\n    {...props}\n  />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n};\n"
  },
  {
    "path": "src/components/ui/toaster.tsx",
    "content": "\"use client\";\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"@/components/ui/toast\";\nimport { useToast } from \"@/components/ui/use-toast\";\n\nexport function Toaster() {\n  const { toasts } = useToast();\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        );\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  );\n}\n"
  },
  {
    "path": "src/components/ui/toggle.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 px-3\",\n        sm: \"h-8 px-2\",\n        lg: \"h-10 px-3\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nconst Toggle = React.forwardRef<\n  React.ElementRef<typeof TogglePrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n  <TogglePrimitive.Root\n    ref={ref}\n    className={cn(toggleVariants({ variant, size, className }))}\n    {...props}\n  />\n));\n\nToggle.displayName = TogglePrimitive.Root.displayName;\n\nexport { Toggle, toggleVariants };\n"
  },
  {
    "path": "src/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "src/components/ui/typewriter-effect.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, stagger, useAnimate, useInView } from \"framer-motion\";\nimport { useEffect } from \"react\";\n\nexport const TypewriterEffect = ({\n  words,\n  className,\n  cursorClassName,\n}: {\n  words: {\n    text: string;\n    className?: string;\n  }[];\n  className?: string;\n  cursorClassName?: string;\n}) => {\n  // split text inside of words into array of characters\n  const wordsArray = words.map((word) => {\n    return {\n      ...word,\n      text: word.text.split(\"\"),\n    };\n  });\n\n  const [scope, animate] = useAnimate();\n  const isInView = useInView(scope);\n  useEffect(() => {\n    if (isInView) {\n      animate(\n        \"span\",\n        {\n          display: \"inline-block\",\n          opacity: 1,\n          width: \"fit-content\",\n        },\n        {\n          duration: 0.3,\n          delay: stagger(0.1),\n          ease: \"easeInOut\",\n        }\n      );\n    }\n  }, [isInView]);\n\n  const renderWords = () => {\n    return (\n      <motion.div ref={scope} className=\"inline\">\n        {wordsArray.map((word, idx) => {\n          return (\n            <div key={`word-${idx}`} className=\"inline-block\">\n              {word.text.map((char, index) => (\n                <motion.span\n                  initial={{}}\n                  key={`char-${index}`}\n                  className={cn(\n                    `dark:text-white text-black opacity-0 hidden`,\n                    word.className\n                  )}\n                >\n                  {char}\n                </motion.span>\n              ))}\n              &nbsp;\n            </div>\n          );\n        })}\n      </motion.div>\n    );\n  };\n  return (\n    <div\n      className={cn(\n        \"text-base sm:text-xl md:text-3xl lg:text-5xl font-bold text-center\",\n        className\n      )}\n    >\n      {renderWords()}\n      <motion.span\n        initial={{\n          opacity: 0,\n        }}\n        animate={{\n          opacity: 1,\n        }}\n        transition={{\n          duration: 0.8,\n          repeat: Infinity,\n          repeatType: \"reverse\",\n        }}\n        className={cn(\n          \"inline-block rounded-sm w-[4px] h-4 md:h-6 lg:h-10 bg-blue-500\",\n          cursorClassName\n        )}\n      ></motion.span>\n    </div>\n  );\n};\n\nexport const TypewriterEffectSmooth = ({\n  words,\n  className,\n  cursorClassName,\n}: {\n  words: {\n    text: string;\n    className?: string;\n  }[];\n  className?: string;\n  cursorClassName?: string;\n}) => {\n  // split text inside of words into array of characters\n  const wordsArray = words.map((word) => {\n    return {\n      ...word,\n      text: word.text.split(\"\"),\n    };\n  });\n  const renderWords = () => {\n    return (\n      <div>\n        {wordsArray.map((word, idx) => {\n          return (\n            <div key={`word-${idx}`} className=\"inline-block\">\n              {word.text.map((char, index) => (\n                <span\n                  key={`char-${index}`}\n                  className={cn(`dark:text-white text-black `, word.className)}\n                >\n                  {char}\n                </span>\n              ))}\n              &nbsp;\n            </div>\n          );\n        })}\n      </div>\n    );\n  };\n\n  return (\n    <div className={cn(\"flex space-x-1 my-6\", className)}>\n      <motion.div\n        className=\"overflow-hidden pb-2\"\n        initial={{\n          width: \"0%\",\n        }}\n        whileInView={{\n          width: \"fit-content\",\n        }}\n        transition={{\n          duration: 2,\n          ease: \"linear\",\n          delay: 1,\n        }}\n      >\n        <div\n          className=\"text-xs sm:text-base md:text-xl lg:text-5xl xl:text-5xl font-bold\"\n          style={{\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {renderWords()}{\" \"}\n        </div>{\" \"}\n      </motion.div>\n      <motion.span\n        initial={{\n          opacity: 0,\n        }}\n        animate={{\n          opacity: 1,\n        }}\n        transition={{\n          duration: 0.8,\n\n          repeat: Infinity,\n          repeatType: \"reverse\",\n        }}\n        className={cn(\n          \"block rounded-sm w-[4px] h-4 sm:h-6 lg:h-12 bg-tint\",\n          cursorClassName\n        )}\n      ></motion.span>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/ui/use-toast.ts",
    "content": "\"use client\";\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\";\n\nimport type { ToastActionElement, ToastProps } from \"@/components/ui/toast\";\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n  id: string;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  action?: ToastActionElement;\n};\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER;\n  return count.toString();\n}\n\ntype ActionType = typeof actionTypes;\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"];\n      toast: ToasterToast;\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"];\n      toast: Partial<ToasterToast>;\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"];\n      toastId?: ToasterToast[\"id\"];\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"];\n      toastId?: ToasterToast[\"id\"];\n    };\n\ninterface State {\n  toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return;\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId);\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    });\n  }, TOAST_REMOVE_DELAY);\n\n  toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      };\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t,\n        ),\n      };\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action;\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId);\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id);\n        });\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t,\n        ),\n      };\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        };\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      };\n  }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action);\n  listeners.forEach((listener) => {\n    listener(memoryState);\n  });\n}\n\ntype Toast = Omit<ToasterToast, \"id\">;\n\nfunction toast({ ...props }: Toast) {\n  const id = genId();\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    });\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id });\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss();\n      },\n    },\n  });\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  };\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState);\n\n  React.useEffect(() => {\n    listeners.push(setState);\n    return () => {\n      const index = listeners.indexOf(setState);\n      if (index > -1) {\n        listeners.splice(index, 1);\n      }\n    };\n  }, [state]);\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  };\n}\n\nexport { useToast, toast };\n"
  },
  {
    "path": "src/components/user-message.tsx",
    "content": "import { ChatMessage } from \"@/schema/chat\";\n\nexport const UserMessageContent = ({ message }: { message: ChatMessage }) => {\n  return (\n    <div className=\"my-4\">\n      <span className=\"text-3xl\">{message.content}</span>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/config/sites.ts",
    "content": "export const SiteConfig = {\n  name: \"DiscovAI\",\n  title: \"DiscovAI\",\n  metaTitle: \"DiscovAI - Discover top ai tools best match your need\",\n  desc: \"Search over 15,349 top ai tools in our database by chatgpt, Discover the latest AI Products with detailed traffic data, best match your need\",\n  panel: \"Everything about AI\",\n  subPanel: \"Discover AI from over 15,349 tools for you need\",\n};\n\nexport const LinkConfig = {\n  site: \"https://discovai.io\",\n  github: \"https://github.com/DiscovAI/DiscovAI-search\",\n  twitter: \"https://x.com/ruiyanghim\",\n};\n"
  },
  {
    "path": "src/db/init.sql",
    "content": "CREATE EXTENSION IF NOT EXISTS \"vector\";\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\n\n-- content table\nCREATE TABLE aitools (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n  title TEXT NOT NULL,\n  url TEXT NOT NULL,\n  description TEXT,\n  content TEXT,\n  screenshot_url TEXT,\n  full_content TEXT,\n  detail TEXT,\n  cat TEXT,\n  ext_info JSONB,\n  total_visits_last_three_months INT,\n  visits_last_month INT,\n  bounce_rate DECIMAL,\n  page_per_visit DECIMAL,\n  time_on_site DECIMAL,\n  traffic_detail JSON;\n);\n-- chunk table\nCREATE TABLE aitools_chunk (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n  chunk_text TEXT,\n  metadata JSONB,\n  tool_id UUID NOT NULL,\n  embedding vector(768),\n  FOREIGN KEY (tool_id) REFERENCES aitools(id)\n);\n\n--  hnsw index for query performance\ncreate index on aitools_chunk using hnsw (embedding vector_l2_ops);\n\n-- rpc function for supabase client\ncreate or replace function match_embeddings (\n  query_embedding vector(768),\n  match_threshold float,\n  match_count int\n)\nreturns table (\n  id UUID,\n  metadata JSONB,\n  tool_id text,\n  chunk_text text,\n  similarity float,\n  screenshot_url text\n)\nlanguage sql stable\nas $$\n  select\n    aitools_chunk.id,\n    aitools_chunk.metadata,\n    aitools_chunk.tool_id,\n    aitools_chunk.chunk_text,\n    1 - (aitools_chunk.embedding <=> query_embedding) as similarity,\n    aitools.screenshot_url\n  from aitools_chunk\n  join aitools on aitools_chunk.tool_id = aitools.id\n  where 1 - (aitools_chunk.embedding <=> query_embedding) > match_threshold\n  order by (aitools_chunk.embedding <=> query_embedding) asc\n  limit match_count;\n$$;\n\n"
  },
  {
    "path": "src/db/redis.ts",
    "content": "// lib/redis.ts\nimport { Redis } from \"@upstash/redis\";\nimport { Ratelimit } from \"@upstash/ratelimit\";\nimport { env } from \"@/env.mjs\";\n\nexport const embeddingVectorCacheKey = (query: string) =>\n  `cache:search:${query}`;\nexport const llmResultCacheKey = (prompt: string) => `cache:llm:${prompt}`;\n\nexport const redis = new Redis({\n  url: env.UPSTASH_REDIS_REST_URL || \"\",\n  token: env.UPSTASH_REDIS_REST_TOKEN || \"\",\n});\n\nexport const ratelimit = new Ratelimit({\n  redis: redis,\n  limiter: Ratelimit.slidingWindow(10, \"10 s\"),\n  analytics: true,\n  prefix: \"@upstash/ratelimit\",\n});\n"
  },
  {
    "path": "src/db/supabase.ts",
    "content": "import { createClient } from \"@supabase/supabase-js\";\n\nexport const supabase = createClient(\n  process.env.NEXT_PUBLIC_SUPABASE_URL!,\n  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n);\n"
  },
  {
    "path": "src/env.mjs",
    "content": "/* eslint-disable no-process-env */\n\nimport { createEnv } from \"@t3-oss/env-nextjs\";\nimport z from \"zod\";\n\nexport const env = createEnv({\n  server: {\n    NEXT_PUBLIC_SUPABASE_URL: z.string(),\n    NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string(),\n    JINA_API_KEY: z.string(),\n    OPENAI_API_KEY: z.string(),\n    OPENAI_API_URL: z.string().nullable(),\n    UPSTASH_REDIS_REST_URL: z.string(),\n    UPSTASH_REDIS_REST_TOKEN: z.string(),\n  },\n  client: {},\n  runtimeEnv: {\n    NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,\n    NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,\n    JINA_API_KEY: process.env.JINA_API_KEY,\n    OPENAI_API_KEY: process.env.OPENAI_API_KEY,\n    OPENAI_API_URL: process.env.OPENAI_API_URL,\n    UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,\n    UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,\n  },\n});\n"
  },
  {
    "path": "src/hooks/chat.ts",
    "content": "import { useMutation } from \"@tanstack/react-query\";\nimport {\n  ChatMessage,\n  ChatRequest,\n  ChatResponseEvent,\n  ErrorStream,\n  Message,\n  MessageRole,\n  MoreResultsStream,\n  RelatedQueriesStream,\n  SearchResult,\n  SearchResultStream,\n  StreamEndStream,\n  StreamEvent,\n  TextChunkStream,\n} from \"@/schema/chat\";\nimport Error from \"next/error\";\nimport {\n  fetchEventSource,\n  FetchEventSourceInit,\n} from \"@microsoft/fetch-event-source\";\nimport { useState } from \"react\";\nimport { useChatStore } from \"@/stores\";\nimport { env } from \"../env.mjs\";\nimport { useRouter } from \"next/navigation\";\n\nconst streamChat = async ({\n  request,\n  onMessage,\n}: {\n  request: ChatRequest;\n  onMessage?: FetchEventSourceInit[\"onmessage\"];\n}): Promise<void> => {\n  try {\n    return await fetchEventSource(\"api/chat\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      keepalive: true,\n      openWhenHidden: true,\n      body: JSON.stringify({ ...request }),\n      // credentials: \"include\", // if need cookie\n      onmessage: onMessage,\n      onerror: (error) => {\n        console.error(error);\n      },\n    });\n  } catch (error) {\n    console.error(error);\n  }\n};\n\nconst convertToChatRequest = (query: string, history: ChatMessage[]) => {\n  const newHistory: Message[] = history.map((message) => ({\n    role:\n      message.role === MessageRole.USER\n        ? MessageRole.USER\n        : MessageRole.ASSISTANT,\n    content: message.content,\n  }));\n  return { query, history: newHistory };\n};\n\nexport const useChat = () => {\n  const { addMessage, messages, threadId, setThreadId } = useChatStore();\n\n  const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(\n    null\n  );\n  const [isStreamingProSearch, setIsStreamingProSearch] = useState(false);\n  const [isStreamingMessage, setIsStreamingMessage] = useState(false);\n\n  const handleEvent = (eventItem: ChatResponseEvent, state: ChatMessage) => {\n    switch (eventItem.event) {\n      case StreamEvent.BEGIN_STREAM:\n        setIsStreamingMessage(true);\n        setStreamingMessage({\n          ...state,\n          role: MessageRole.ASSISTANT,\n          content: \"\",\n          related_queries: [],\n          sources: [],\n          images: [],\n        });\n        break;\n      case StreamEvent.SEARCH_RESULTS:\n        const data = eventItem.data as SearchResultStream;\n        state.sources = data.results ?? [];\n        state.images = data.images ?? [];\n        break;\n      case StreamEvent.TEXT_CHUNK:\n        state.content += (eventItem.data as TextChunkStream).text;\n        break;\n      case StreamEvent.RELATED_QUERIES:\n        state.related_queries =\n          (eventItem.data as RelatedQueriesStream).related_queries ?? [];\n        break;\n      case StreamEvent.MORE_RESULTS:\n        state.more_results =\n          (eventItem.data as MoreResultsStream).more_results ?? [];\n        break;\n      case StreamEvent.STREAM_END:\n        const endData = eventItem.data as StreamEndStream;\n        addMessage({ ...state });\n        setStreamingMessage(null);\n        setIsStreamingMessage(false);\n        setIsStreamingProSearch(false);\n\n        // Only if the backend is using the DB\n        if (endData.thread_id) {\n          setThreadId(endData.thread_id);\n          window.history.pushState({}, \"\", `/search/${endData.thread_id}`);\n        }\n        return;\n      case StreamEvent.ERROR:\n        const errorData = eventItem.data as ErrorStream;\n        addMessage({\n          role: MessageRole.ASSISTANT,\n          content: errorData.detail,\n          related_queries: [],\n          sources: [],\n          images: [],\n          is_error_message: true,\n        });\n        setStreamingMessage(null);\n        setIsStreamingMessage(false);\n        setIsStreamingProSearch(false);\n        return;\n    }\n    setStreamingMessage({\n      role: MessageRole.ASSISTANT,\n      content: state.content,\n      related_queries: state.related_queries,\n      sources: state.sources,\n      images: state.images,\n      more_results: state.more_results,\n    });\n  };\n\n  const { mutateAsync: chat } = useMutation<void, Error, ChatRequest>({\n    retry: false,\n    mutationFn: async (request) => {\n      const state: ChatMessage = {\n        role: MessageRole.ASSISTANT,\n        content: \"\",\n        sources: [],\n        related_queries: [],\n        images: [],\n        more_results: [],\n      };\n      addMessage({ role: MessageRole.USER, content: request.query });\n\n      const req = {\n        ...request,\n        thread_id: threadId,\n      };\n      await streamChat({\n        request: req,\n        onMessage: (event) => {\n          if (!event.data) return;\n          const eventItem: ChatResponseEvent = JSON.parse(event.data);\n          handleEvent(eventItem, state);\n        },\n      });\n    },\n  });\n\n  const handleSend = async (query: string) => {\n    await chat(convertToChatRequest(query, messages));\n  };\n\n  return {\n    handleSend,\n    streamingMessage,\n    isStreamingMessage,\n    isStreamingProSearch,\n  };\n};\n"
  },
  {
    "path": "src/lib/chat/embedding.ts",
    "content": "import { JinaEmbeddings } from \"@langchain/community/embeddings/jina\";\nimport { env } from \"@/env.mjs\";\n\nconst embeddings = new JinaEmbeddings({\n  model: \"jina-embeddings-v2-base-en\",\n  apiKey: env.JINA_API_KEY,\n});\n\nexport async function generateDocEmbedding(contents: string[]) {\n  const documentEmbeddings = await embeddings.embedDocuments(contents);\n  return documentEmbeddings;\n}\nexport async function generateQueyEmbedding(query: string) {\n  const embedding = await embeddings.embedQuery(query);\n  return embedding;\n}\n"
  },
  {
    "path": "src/lib/chat/llm.ts",
    "content": "import { CHAT_PROMPT, RELATED_QUESTION_PROMPT, TRANSLATE } from \"./prompts\";\n// import { OpenAI } from \"@langchain/openai\";\n// import type { AIMessageChunk } from \"@langchain/core/messages\";\n// import { concat } from \"@langchain/core/utils/stream\";\nimport { streamText, generateText, generateObject } from \"ai\";\nimport { createOpenAI } from \"@ai-sdk/openai\";\nimport z from \"zod\";\nimport { containsChinese } from \"../utils\";\nimport { env } from \"@/env.mjs\";\n\nconst openai = createOpenAI({\n  apiKey: env.OPENAI_API_KEY,\n  baseURL: env.OPENAI_API_URL,\n});\n\nfunction documentToStr(doc) {\n  const { metadata, chunk_text, screenshot_url } = doc;\n  return `Title: ${metadata.title}\\nURL: ${metadata.url}\\nScreenshotUrl: ${screenshot_url}\\nSummary: ${chunk_text}`;\n}\n\nfunction formatContext(searchResults) {\n  return searchResults\n    .map((result, index) => `Citation ${index + 1}. ${documentToStr(result)}`)\n    .join(\"\\n\\n\");\n}\n\nexport const formatePrompt = (contexts, query) =>\n  CHAT_PROMPT(formatContext(contexts), query);\n\nexport async function genLLMTextChunk({ query, contexts }) {\n  const prompt = formatePrompt(contexts, query);\n  const model = openai(\"gpt-4o-mini\");\n  const result = await streamText({\n    model: model,\n    prompt: prompt,\n  });\n  return result;\n}\n\nexport async function genRelatedQuery({ query, contexts }) {\n  const prompt = RELATED_QUESTION_PROMPT(\n    JSON.stringify(contexts).slice(0, 4000),\n    query\n  );\n  const model = openai(\"gpt-4o-mini\");\n  const result = await generateObject({\n    model,\n    prompt,\n    schema: z.object({\n      items: z.array(z.string()).length(3),\n    }),\n  });\n  return result.object.items;\n}\n\nexport async function translate({ query }) {\n  if (!containsChinese(query)) {\n    return query;\n  }\n  const prompt = TRANSLATE(query);\n  const model = openai(\"gpt-4o-mini\");\n  const { text } = await generateText({\n    model,\n    prompt,\n  });\n  return text;\n}\n"
  },
  {
    "path": "src/lib/chat/prompts.ts",
    "content": "export const CHAT_PROMPT = (contexts: string, query: string) => `\\\nAs a professional AI tool search expert. Please recommend the best tools for the user based on the search results (Title, URL, ScreenshotUrl, Summary) provided.\n\nYou must only use the information in the search results provided.Use a professional tone.\n\nYou must introduce each tool in context. \nIf the summary contains the number of visits to the page, be sure to point it out, otherwise ignore it.\n\nYou must cite the answer using [number] notation. You must cite sentences with their relevant citation number. Cite every part of the answer.\nPlace citations at the end of the sentence. You can do multiple citations in a row with the format [number1][number2].\n\nOnly cite the most relevant results that answer the question accurately. If different results refer to different entities with the same name, write separate answers for each entity.\n\nONLY cite inline.\nDO NOT include a reference section, DO NOT include URLs.\nDO NOT repeat the question.\nYou can use markdown formatting. You should include bullets to list the information in your answer.\nFor each item, you must add the image of screenshotUrl below like this:\n<a href={URL} target=\"_blank\"><img src={ScreenshotUrl} alt={title} style=\"border-radius:8px;\" /></a>\n\n\n<context>\n${contexts}\n</context>\n---------------------\n\nMake sure to match the language of the user's question.\n\nQuestion: ${query}\nAnswer (in the language of the user's question): \\\n`;\n\nexport const RELATED_QUESTION_PROMPT = (context: string, query: string) => `\nGiven a question and search result context, generate 3 follow-up questions the user might ask. Use the original question and context.\n\nInstructions:\n- Generate exactly 3 questions.\n- These questions should be concise, and simple.\n- Ensure the follow-up questions are relevant to the original question and context.\nMake sure to match the language of the user's question.\n\nOriginal Question: ${query}\n<context>\n${context}\n</context>\n\nOutput:\nrelated_questions: A list of EXACTLY three concise, simple follow-up questions\n`;\n\nexport const TRANSLATE = (query: string) => `\nDirectly translate it to english, no other words.\nQuestion: ${query}\n`;\n"
  },
  {
    "path": "src/lib/utils.ts",
    "content": "import { ChatResponseEvent } from \"@/schema/chat\";\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport function nFormatter(num: number, digits?: number) {\n  if (!num) return \"0\";\n  const lookup = [\n    { value: 1, symbol: \"\" },\n    { value: 1e3, symbol: \"K\" },\n    { value: 1e6, symbol: \"M\" },\n    { value: 1e9, symbol: \"G\" },\n    { value: 1e12, symbol: \"T\" },\n    { value: 1e15, symbol: \"P\" },\n    { value: 1e18, symbol: \"E\" },\n  ];\n  const rx = /\\.0+$|(\\.[0-9]*[1-9])0+$/;\n  var item = lookup\n    .slice()\n    .reverse()\n    .find(function (item) {\n      return num >= item.value;\n    });\n  return item\n    ? (num / item.value).toFixed(digits || 1).replace(rx, \"$1\") + item.symbol\n    : \"0\";\n}\n\nexport const genStream = (o: ChatResponseEvent) => {\n  const encoder = new TextEncoder();\n  return encoder.encode(`data: ${JSON.stringify(o)}\\n\\n`);\n};\n\nexport function containsChinese(str: string) {\n  const chineseRegex = /[\\u4e00-\\u9fa5]/;\n  return chineseRegex.test(str);\n}\n\nfunction addQueryParams(url, params) {\n  const urlObj = new URL(url);\n  Object.keys(params).forEach((key) =>\n    urlObj.searchParams.append(key, params[key])\n  );\n  return urlObj.toString();\n}\n\nconst params = {\n  ref: \"discovai-io\",\n  utm_source: \"discovai-io\",\n  utm_medium: \"referral\",\n};\nexport function addRefToUrl(url) {\n  return addQueryParams(url, params);\n}\n\nexport const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));\n"
  },
  {
    "path": "src/middleware.ts",
    "content": "import { NextFetchEvent, NextRequest, NextResponse } from \"next/server\";\nimport { ratelimit } from \"./db/redis\";\n\nexport default async function middleware(\n  request: NextRequest,\n  event: NextFetchEvent\n): Promise<Response | undefined> {\n  const ip = request.ip ?? \"127.0.0.1\";\n  const { success, pending, limit, reset, remaining } =\n    await ratelimit.limit(ip);\n  return success\n    ? NextResponse.next()\n    : NextResponse.redirect(new URL(\"/blocked\", request.url));\n}\n\nexport const config = {\n  matcher: \"/\",\n};\n"
  },
  {
    "path": "src/providers.tsx",
    "content": "\"use client\";\n\nimport React, { useMemo } from \"react\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\n\nexport default function Providers({ children }: { children: React.ReactNode }) {\n  const [queryClient] = React.useState(() => new QueryClient());\n\n  return (\n    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n  );\n}\n"
  },
  {
    "path": "src/schema/chat.ts",
    "content": "export type BeginStream = {\n  event_type?: StreamEvent;\n  query: string;\n};\n\nexport type ChatHistoryResponse = {\n  snapshots?: Array<ChatSnapshot>;\n};\n\nexport type ChatMessage = {\n  content: string;\n  role: MessageRole;\n  related_queries?: Array<string> | null;\n  sources?: Array<SearchResult> | null;\n  images?: Array<string> | null;\n  is_error_message?: boolean;\n  more_results?: Array<{\n    title: string;\n    url: string;\n    screenshot_url: string;\n  }> | null;\n};\n\nexport type ChatRequest = {\n  thread_id?: number | null;\n  query: string;\n  history?: Array<Message>;\n  pro_search?: boolean;\n};\n\nexport type ChatResponseEvent = {\n  event: StreamEvent;\n  data:\n    | BeginStream\n    | SearchResultStream\n    | TextChunkStream\n    | RelatedQueriesStream\n    | StreamEndStream\n    | FinalResponseStream\n    | ErrorStream\n    | MoreResultsStream;\n};\n\nexport type ChatSnapshot = {\n  id: number;\n  title: string;\n  date: string;\n  preview: string;\n  model_name: string;\n};\n\nexport type ErrorStream = {\n  event_type?: StreamEvent;\n  detail: string;\n};\n\nexport type FinalResponseStream = {\n  event_type?: StreamEvent;\n  message: string;\n};\n\nexport type HTTPValidationError = {\n  detail?: Array<ValidationError>;\n};\n\nexport type Message = {\n  content: string;\n  role: MessageRole;\n};\n\nexport enum MessageRole {\n  USER = \"user\",\n  ASSISTANT = \"assistant\",\n}\n\nexport type RelatedQueriesStream = {\n  event_type?: StreamEvent;\n  related_queries?: Array<string>;\n};\n\nexport type MoreResultsStream = {\n  event_type?: StreamEvent;\n  more_results?: Array<any>;\n};\n\nexport type SearchResult = {\n  title: string;\n  url: string;\n  content: string;\n  description: string;\n};\n\nexport type SearchResultStream = {\n  event_type?: StreamEvent;\n  results?: Array<SearchResult>;\n  images?: Array<string>;\n};\n\nexport type StreamEndStream = {\n  event_type?: StreamEvent;\n  thread_id?: number | null;\n};\n\nexport enum StreamEvent {\n  BEGIN_STREAM = \"begin-stream\",\n  SEARCH_RESULTS = \"search-results\",\n  TEXT_CHUNK = \"text-chunk\",\n  RELATED_QUERIES = \"related-queries\",\n  MORE_RESULTS = \"more-results\",\n  STREAM_END = \"stream-end\",\n  FINAL_RESPONSE = \"final-response\",\n  ERROR = \"error\",\n}\n\nexport type TextChunkStream = {\n  event_type?: StreamEvent;\n  text: string;\n};\n\nexport type ThreadResponse = {\n  thread_id: number;\n  messages?: Array<ChatMessage>;\n};\n\nexport type ValidationError = {\n  loc: Array<string | number>;\n  msg: string;\n  type: string;\n};\n"
  },
  {
    "path": "src/stores/index.ts",
    "content": "import { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { createMessageSlice, ChatStore } from \"./slices/messageSlice\";\n\ntype StoreState = ChatStore;\n\nconst useStore = create<StoreState>()(\n  persist(\n    (...a) => ({\n      ...createMessageSlice(...a),\n    }),\n    {\n      name: \"store\",\n      partialize: (state) => ({\n        // messages: state.messages, TODO\n      }),\n    }\n  )\n);\n\nexport const useChatStore = () =>\n  useStore((state) => ({\n    messages: state.messages,\n    addMessage: state.addMessage,\n    setMessages: state.setMessages,\n    threadId: state.threadId,\n    setThreadId: state.setThreadId,\n  }));\n"
  },
  {
    "path": "src/stores/slices/messageSlice.ts",
    "content": "import { create, StateCreator } from \"zustand\";\nimport { ChatMessage } from \"@/schema/chat\";\n\ntype State = {\n  threadId: number | null;\n  messages: ChatMessage[];\n};\n\ntype Actions = {\n  addMessage: (message: ChatMessage) => void;\n  setThreadId: (threadId: number | null) => void;\n  setMessages: (messages: ChatMessage[]) => void;\n};\n\nexport type ChatStore = State & Actions;\n\nexport const createMessageSlice: StateCreator<ChatStore, [], [], ChatStore> = (\n  set\n) => ({\n  threadId: null,\n  messages: [],\n  addMessage: (message: ChatMessage) =>\n    set((state) => ({ messages: [...state.messages, message] })),\n  setThreadId: (threadId: number | null) => set((state) => ({ threadId })),\n  setMessages: (messages: ChatMessage[]) => set((state) => ({ messages })),\n});\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\nconst {\n  default: flattenColorPalette,\n} = require(\"tailwindcss/lib/util/flattenColorPalette\");\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  ],\n  prefix: \"\",\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      typography: (theme: any) => ({\n        custom: {\n          css: {\n            \"--tw-prose-body\": theme(\"colors.primary.foreground\"),\n            \"--tw-prose-invert-body\": theme(\"colors.primary.DEFAULT\"),\n          },\n        },\n      }),\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        tint: {\n          DEFAULT: \"hsl(var(--tint))\",\n          foreground: \"hsl(var(--tint-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: [\n    require(\"tailwindcss-animate\"),\n    require(\"@tailwindcss/typography\"),\n    addVariablesForColors,\n  ],\n} as Config;\n\nfunction addVariablesForColors({ addBase, theme }: any) {\n  let allColors = flattenColorPalette(theme(\"colors\"));\n  let newVars = Object.fromEntries(\n    Object.entries(allColors).map(([key, val]) => [`--${key}`, val])\n  );\n\n  addBase({\n    \":root\": newVars,\n  });\n}\n\nexport default config;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"target\": \"ES2020\",\n    \"strict\": false,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]