Repository: DiscovAI/DiscovAI-search
Branch: main
Commit: 0499fae24f94
Files: 79
Total size: 144.1 KB
Directory structure:
gitextract_sui8u3lf/
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── components.json
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ ├── chat/
│ │ │ │ └── route.ts
│ │ │ └── join-wait/
│ │ │ └── route.ts
│ │ ├── blocked/
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── robots.txt
│ │ └── waitlist/
│ │ └── page.tsx
│ ├── components/
│ │ ├── ask-input.tsx
│ │ ├── assistant-message.tsx
│ │ ├── chat-panel.tsx
│ │ ├── image-section.tsx
│ │ ├── landing/
│ │ │ ├── hero.tsx
│ │ │ ├── preview-landing.tsx
│ │ │ ├── typing-title.tsx
│ │ │ └── wailtlist.tsx
│ │ ├── magicui/
│ │ │ └── typing-animation.tsx
│ │ ├── markdown.tsx
│ │ ├── message.tsx
│ │ ├── messages-list.tsx
│ │ ├── mode-toggle.tsx
│ │ ├── more-results.tsx
│ │ ├── nav.tsx
│ │ ├── related-questions.tsx
│ │ ├── search-results.tsx
│ │ ├── section.tsx
│ │ ├── shared/
│ │ │ ├── icons.tsx
│ │ │ └── max-width-wrapper.tsx
│ │ ├── starter-questions.tsx
│ │ ├── theme-provider.tsx
│ │ ├── ui/
│ │ │ ├── accordion.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── timeline.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── typewriter-effect.tsx
│ │ │ └── use-toast.ts
│ │ └── user-message.tsx
│ ├── config/
│ │ └── sites.ts
│ ├── db/
│ │ ├── init.sql
│ │ ├── redis.ts
│ │ └── supabase.ts
│ ├── env.mjs
│ ├── hooks/
│ │ └── chat.ts
│ ├── lib/
│ │ ├── chat/
│ │ │ ├── embedding.ts
│ │ │ ├── llm.ts
│ │ │ └── prompts.ts
│ │ └── utils.ts
│ ├── middleware.ts
│ ├── providers.tsx
│ ├── schema/
│ │ └── chat.ts
│ └── stores/
│ ├── index.ts
│ └── slices/
│ └── messageSlice.ts
├── tailwind.config.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
copy_github_release.sh
supabase/
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Yoshiki Miura
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# DiscovAI
An AI-powered search engine for AI tools, or your own data.
https://github.com/user-attachments/assets/2cdc92d0-d0c9-4098-8166-260e973783f0
Please 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.
## 💻 Live Demo
[DiscovAI.io](https://discovai.io/) (use it for free without signin or credit card)
## 🗂️ Overview
- 🛠 [Features](#-features)
- 🧱 [Tech-Stack](#-stack)
- 🚀 [Quickstart](#-quickstart)
- 🌐 [Deploy](#-deploy)
## 🛠 Features
- **Vector-based Search**: Converts user queries into vectors for precise similarity matching in our AI product database.
- **Redis-powered Caching**: Utilizes Redis to cache search results and outputs, significantly improving response times for repeated queries.
- **Comprehensive AI Database**: Maintains an up-to-date collection of AI products across various categories and industries.
- **LLM-powered Responses**: Leverages large language models to provide detailed, context-aware answers based on search results.
- **User-friendly Interface**: Offers an intuitive design for effortless navigation and efficient AI product discovery.
## 🧱 Stack
- App framework: [Next.js](https://nextjs.org/)
- Text streaming: [Vercel AI SDK](https://sdk.vercel.ai/docs)
- LLM Model: [gpt-4o-mini](https://openai.com/)
- Database: [Supabase](https://supabase.com/)
- Vector: [Pgvector](https://github.com/pgvector/pgvector)
- Embedding Model: [Jina AI](https://jina.ai/embeddings)
- Redis Cache: [Upstash](https://upstash.com/)
- Component library: [shadcn/ui](https://ui.shadcn.com/)
- Headless component primitives: [Radix UI](https://www.radix-ui.com/)
- Styling: [Tailwind CSS](https://tailwindcss.com/)
## 🚀 Quickstart
### 1. Clone repo
run the following command to clone the repo:
```
git clone https://github.com/DiscovAI/DiscovAI-search
```
### 2. Install dependencies
```
cd discovai-search
pnpm i
```
### 3. Setting up Supabase
create 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
### 4. Setting up Upstash
Follow 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.
### 4. Fill out secrets
```
cp .env.local.example .env.local
```
Your .env.local file should look like this:
```
# Required
# for match documents
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
# for embedding query, retrieved here: https://jina.ai/embeddings/
JINA_API_KEY=
# for llm output, retrieved here: https://platform.openai.com/api-keys
OPENAI_API_KEY=
OPENAI_API_URL=
# for llm cache and serach cache
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
```
### 5. Run app locally
```
pnpm dev
```
You can now visit http://localhost:3000.
## 🌐 Deploy
You can deploy on any saas platform like vercel, zeabur, cloudflare pages.
## 🌟 History
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="
https://api.star-history.com/svg?repos=DiscovAI/DiscovAI-search&type=Date&theme=dark
"
/>
<source
media="(prefers-color-scheme: light)"
srcset="
https://api.star-history.com/svg?repos=DiscovAI/DiscovAI-search&type=Date
"
/>
<img
alt="Star History Chart"
src="https://api.star-history.com/svg?repos=DiscovAI/DiscovAI-search&type=Date"
/>
</picture>
================================================
FILE: components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"magicui": "@/components/magicui"
}
}
================================================
FILE: next.config.mjs
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
{
protocol: "http",
hostname: "**",
},
],
},
compiler: {
removeConsole: !!process.env.CI,
},
};
export default nextConfig;
================================================
FILE: package.json
================================================
{
"name": "discovai-search",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@ai-sdk/openai": "^0.0.40",
"@langchain/community": "^0.2.21",
"@microsoft/fetch-event-source": "^2.0.1",
"@next/env": "^14.2.3",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@supabase/supabase-js": "^2.44.4",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.32.0",
"@upstash/ratelimit": "^2.0.1",
"@upstash/redis": "^1.33.0",
"ai": "^3.2.38",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^11.1.7",
"geist": "^1.3.0",
"ioredis": "^5.4.1",
"lodash": "^4.17.21",
"lucide-react": "^0.376.0",
"next": "14.2.3",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"react-hot-toast": "^2.4.1",
"react-markdown": "^9.0.1",
"react-textarea-autosize": "^8.5.3",
"rehype-raw": "^7.0.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8",
"zustand": "^4.5.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.13",
"@types/lodash": "^4.17.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"postcss": "^8",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
================================================
FILE: postcss.config.mjs
================================================
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
================================================
FILE: src/app/api/chat/route.ts
================================================
// app/api/stream/route.ts
import { embeddingVectorCacheKey, llmResultCacheKey, redis } from "@/db/redis";
import { supabase } from "@/db/supabase";
import { generateQueyEmbedding } from "@/lib/chat/embedding";
import { genLLMTextChunk, translate } from "@/lib/chat/llm";
import { addRefToUrl, genStream, sleep } from "@/lib/utils";
import { StreamEvent } from "@/schema/chat";
import { PostgrestError } from "@supabase/supabase-js";
import { NextRequest } from "next/server";
export async function POST(req: NextRequest) {
const body = await req.json();
const { query } = body;
const customReadable = new ReadableStream({
async start(controller) {
try {
const beginData = {
event: StreamEvent.BEGIN_STREAM,
data: { event_type: StreamEvent.BEGIN_STREAM, query: query },
};
controller.enqueue(genStream(beginData));
const cacheResult: null | any = await redis.get(
embeddingVectorCacheKey(query)
);
let documents: any[], queryEmbeddingError: PostgrestError;
if (cacheResult) {
documents = cacheResult;
console.log("search result", "cached");
} else {
// match documents
const embedding = await generateQueyEmbedding(
await translate({ query })
);
let result = await supabase.rpc("match_embeddings", {
query_embedding: embedding, // Pass the embedding you want to compare
match_threshold: 0.78, // Choose an appropriate threshold for your data
match_count: 15, // Choose the number of matches
});
documents = result.data;
queryEmbeddingError = result.error;
}
if (queryEmbeddingError) {
console.error(queryEmbeddingError);
controller.enqueue(
genStream({
event: StreamEvent.ERROR,
data: {
event_type: StreamEvent.ERROR,
detail: "error on query embeddings",
},
})
);
controller.close();
}
redis.setex(
embeddingVectorCacheKey(query),
60 * 60 * 24, // 1 day
JSON.stringify(documents)
);
// filter for unique docs
const uniqueDocuments = [
...new Set(documents.map((tool) => tool.metadata.url)),
].map((url) => documents.find((tool) => tool.metadata.url === url));
for (let doc of uniqueDocuments) {
doc.metadata.url = addRefToUrl(doc.metadata.url);
}
documents = uniqueDocuments.slice(0, 5);
const searchResult = documents.map((d) => {
const safeContent = d.chunk_text.includes("DESCRIPTION")
? d.chunk_text?.split("---")?.[0]?.split("DESCRIPTION:")?.[1]
: d.chunk_text;
return {
title: d.metadata.title,
url: d.metadata.url,
content: safeContent,
description: safeContent,
screenshot_url: d.screenshot_url,
};
});
controller.enqueue(
genStream({
event: StreamEvent.SEARCH_RESULTS,
data: {
event_type: StreamEvent.SEARCH_RESULTS,
results: searchResult,
images: uniqueDocuments.map((r) => r.screenshot_url),
},
})
);
// stream llm text chunk
const llmKey = llmResultCacheKey(query);
const llmCache: string | null = await redis.get(llmKey);
let gathered = "";
if (llmCache) {
console.log("llm result cache", "cached");
gathered = llmCache;
// simulate stream
let cacheArray = llmCache.split(" ");
for await (const c of cacheArray) {
await sleep(10);
controller.enqueue(
genStream({
event: StreamEvent.TEXT_CHUNK,
data: {
event_type: StreamEvent.TEXT_CHUNK,
text: c + " ",
},
})
);
}
} else {
const stream = await genLLMTextChunk({
query,
contexts: documents,
});
for await (const chunk of stream.textStream) {
controller.enqueue(
genStream({
event: StreamEvent.TEXT_CHUNK,
data: {
event_type: StreamEvent.TEXT_CHUNK,
text: chunk,
},
})
);
gathered += chunk;
}
}
redis.setex(llmKey, 60 * 60 * 12, gathered);
// more results or related query
const moreTools = uniqueDocuments.slice(5);
controller.enqueue(
genStream({
event: StreamEvent.MORE_RESULTS,
data: {
event_type: StreamEvent.MORE_RESULTS,
more_results: moreTools.map((d) => ({
title: d.metadata.title,
url: d.metadata.url,
screenshot_url: d.screenshot_url,
})),
},
})
);
controller.enqueue(
genStream({
event: StreamEvent.FINAL_RESPONSE,
data: {
event_type: StreamEvent.FINAL_RESPONSE,
message: gathered,
},
})
);
controller.enqueue(
genStream({
event: StreamEvent.STREAM_END,
data: { event_type: StreamEvent.STREAM_END, thread_id: null },
})
);
controller.close();
} catch (error) {
console.error(error);
controller.enqueue(
genStream({
event: StreamEvent.ERROR,
data: {
event_type: StreamEvent.ERROR,
detail: "Oops~",
},
})
);
controller.close();
}
},
});
return new Response(customReadable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
================================================
FILE: src/app/api/join-wait/route.ts
================================================
import { NextRequest } from "next/server";
import { supabase } from "@/db/supabase";
async function postHandler(req: NextRequest) {
try {
const body = await req.json();
const { email } = body;
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
if (!emailRegex.test(email)) {
return new Response(JSON.stringify("Invalid email format"), {
status: 400,
});
}
const { error, data } = await supabase
.from("searchzero-waitlist")
.insert({
email_address: email,
})
.select();
if (error) {
if (error.code === "23505") {
return new Response(
JSON.stringify({
isSuc: true,
code: 0,
msg: "Already subscribed",
})
);
}
throw error;
}
if (data && data.length) {
return new Response(
JSON.stringify({
isSuc: true,
code: 0,
msg: "Subscription successful",
})
);
}
} catch (error) {
console.error(error);
return new Response("Failed to subscribe, pleas Try again", {
status: 501,
});
}
}
export const POST = postHandler;
export const runtime = "edge";
================================================
FILE: src/app/blocked/page.tsx
================================================
export default function Blocked() {
return (
<div>
<main>
<h3>Access blocked.</h3>
</main>
</div>
);
}
================================================
FILE: src/app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 60 30% 98%;
--foreground: 0 0% 3.9%;
--card: 0 0% 96.1%;
--card-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--tint: 27 100% 49.8%;
--tint-foreground: 25 76% 31%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 89.8%;
--radius: 0.5rem;
}
.dark {
--background: 180 2% 10%;
--foreground: 0 0% 98%;
--card: 180 3% 13%;
--card-foreground: 0 0% 63.9%;
--popover: 180 2% 10%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--tint: 22.4 100% 53%;
--tint-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 14.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
.cl-internal-11ttlho {
display: none;
opacity: 0;
}
.cl-internal-180wb59 {
display: none;
opacity: 0;
}
================================================
FILE: src/app/layout.tsx
================================================
import { Navbar } from "@/components/nav";
import { ThemeProvider } from "@/components/theme-provider";
import { LinkConfig, SiteConfig } from "@/config/sites";
import { cn } from "@/lib/utils";
import Providers from "@/providers";
import { GeistSans } from "geist/font/sans";
import type { Metadata } from "next";
import Script from "next/script";
import { Toaster } from "react-hot-toast";
import "./globals.css";
const title = SiteConfig.metaTitle;
const description = SiteConfig.desc;
export const metadata: Metadata = {
metadataBase: new URL(LinkConfig.site),
title,
description,
keywords: [
"searchgpt",
"topaitools",
"ai",
"chatgpt",
"discov-ai",
"discover ai",
"search engine",
"top ai traffic",
"ai search engine",
],
openGraph: {
title,
description,
images: "/og.png",
url: new URL(LinkConfig.site),
type: "website",
siteName: SiteConfig.name,
},
twitter: {
title,
description,
card: "summary_large_image",
creator: "@ruiyanghim",
images: "/og.png",
site: LinkConfig.site,
},
icons: ["/favicon.svg"],
alternates: {
canonical: "./",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<html lang="en" suppressHydrationWarning>
<body className={cn("antialiased", GeistSans.className)}>
<Providers>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
<Navbar />
{children}
<Toaster />
</ThemeProvider>
</Providers>
<Script
defer
src="https://ryan-umami-mamimami.vercel.app/script.js"
data-website-id="5bcb1ea9-c57e-44fb-9a8b-d1f4af19c179"
></Script>
</body>
</html>
</>
);
}
================================================
FILE: src/app/page.tsx
================================================
import { ChatPanel } from "@/components/chat-panel";
import { Suspense } from "react";
export default function Home() {
return (
<>
<div className="h-screen">
<div className="flex grow h-full mx-auto max-w-screen-md px-4 md:px-8">
<Suspense>
<ChatPanel />
</Suspense>
</div>
</div>
</>
);
}
================================================
FILE: src/app/robots.txt
================================================
User-Agent: *
Allow: /
Disallow: /auth
================================================
FILE: src/app/waitlist/page.tsx
================================================
import HeroLanding from "@/components/landing/hero";
import PreviewLanding from "@/components/landing/preview-landing";
export default function Page() {
return (
<>
<HeroLanding />
<PreviewLanding />
</>
);
}
================================================
FILE: src/components/ask-input.tsx
================================================
import TextareaAutosize from "react-textarea-autosize";
import { useState } from "react";
import { Button } from "./ui/button";
import { ArrowRight, ArrowUp } from "lucide-react";
const InputBar = ({
input,
setInput,
}: {
input: string;
setInput: (input: string) => void;
}) => {
return (
<div className="w-full flex rounded-2xl focus:outline-none px-2 py-1 bg-card border-2 ">
<div className="w-full">
<TextareaAutosize
className="w-full bg-transparent text-md resize-none focus:outline-none p-2"
placeholder="the best ai tools for..."
onChange={(e) => setInput(e.target.value)}
value={input}
/>
</div>
<div className="flex justify-between">
<div className="flex items-center gap-2">
<Button
type="submit"
variant="default"
size="icon"
className="rounded-full bg-tint aspect-square h-8 w-8 disabled:opacity-20 hover:bg-tint/80 overflow-hidden"
disabled={input.trim().length < 5}
>
<ArrowRight size={20} className="" />
</Button>
</div>
</div>
</div>
);
};
const FollowingUpInput = ({
input,
setInput,
}: {
input: string;
setInput: (input: string) => void;
}) => {
return (
<div className="w-full flex flex-row rounded-full focus:outline-none px-2 py-1 bg-card border-2 items-center ">
<div className="w-full">
<TextareaAutosize
className="w-full bg-transparent text-md resize-none focus:outline-none p-2"
placeholder="ask follow up questions..."
onChange={(e) => setInput(e.target.value)}
value={input}
/>
</div>
<div className="flex items-center gap-2">
{/* <ProToggle /> */}
<Button
type="submit"
variant="default"
size="icon"
className="rounded-full bg-tint aspect-square h-8 w-8 disabled:opacity-20 hover:bg-tint/80 overflow-hidden"
disabled={input.trim().length < 5}
>
<ArrowUp size={20} />
</Button>
</div>
</div>
);
};
export const AskInput = ({
sendMessage,
isFollowingUp = false,
}: {
sendMessage: (message: string) => void;
isFollowingUp?: boolean;
}) => {
const [input, setInput] = useState("");
return (
<>
<form
className="w-full overflow-hidden"
onSubmit={(e) => {
if (input.trim().length < 5) return;
e.preventDefault();
sendMessage(input);
setInput("");
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (input.trim().length < 5) return;
sendMessage(input);
setInput("");
}
}}
>
{isFollowingUp ? (
// <FollowingUpInput input={input} setInput={setInput} />
<></>
) : (
<InputBar input={input} setInput={setInput} />
)}
</form>
</>
);
};
================================================
FILE: src/components/assistant-message.tsx
================================================
import { MessageComponent, MessageComponentSkeleton } from "./message";
import { SearchResultsSkeleton, SearchResults } from "./search-results";
import { Section } from "./section";
import { AlertCircle } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { ImagePreload } from "./image-section";
import { ChatMessage } from "@/schema/chat";
import MoreResults from "./more-results";
export function ErrorMessage({ content }: { content: string }) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{content}</AlertDescription>
</Alert>
);
}
export const AssistantMessageContent = ({
message,
isStreaming = false,
onRelatedQuestionSelect,
}: {
message: ChatMessage;
isStreaming?: boolean;
onRelatedQuestionSelect: (question: string) => void;
}) => {
const {
sources,
content,
related_queries,
images,
is_error_message = false,
more_results,
} = message;
if (is_error_message) {
return <ErrorMessage content={message.content} />;
}
return (
<div className="flex flex-col">
<Section title="Sources" animate={isStreaming}>
{!sources || sources.length === 0 ? (
<SearchResultsSkeleton />
) : (
<>
<SearchResults results={sources} />
</>
)}
</Section>
<ImagePreload images={images || []} />
<Section title="Answer" animate={isStreaming} streaming={isStreaming}>
{content ? (
<MessageComponent message={message} isStreaming={isStreaming} />
) : (
<MessageComponentSkeleton />
)}
</Section>
{more_results && more_results.length > 0 && (
<Section title="More Results" animate={isStreaming}>
<MoreResults results={more_results} />
</Section>
)}
</div>
);
};
================================================
FILE: src/components/chat-panel.tsx
================================================
"use client";
import { useChat } from "@/hooks/chat";
import { useChatStore } from "@/stores";
import { useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { AskInput } from "./ask-input";
import { SiteConfig } from "@/config/sites";
import { LoaderIcon, TrendingUpIcon } from "lucide-react";
import { MessageRole } from "@/schema/chat";
import MessagesList from "./messages-list";
import { StarterQuestionsList } from "./starter-questions";
const useAutoScroll = (ref: React.RefObject<HTMLDivElement>) => {
const { messages } = useChatStore();
useEffect(() => {
if (messages.at(-1)?.role === MessageRole.USER) {
ref.current?.scrollIntoView({
behavior: "smooth",
block: "end",
});
}
}, [messages, ref]);
};
const useAutoResizeInput = (
ref: React.RefObject<HTMLDivElement>,
setWidth: (width: number) => void
) => {
const { messages } = useChatStore();
useEffect(() => {
const updatePosition = () => {
if (ref.current) {
setWidth(ref.current.scrollWidth);
}
};
updatePosition();
window.addEventListener("resize", updatePosition);
return () => {
window.removeEventListener("resize", updatePosition);
};
}, [messages, ref, setWidth]);
};
const useAutoFocus = (ref: React.RefObject<HTMLTextAreaElement>) => {
useEffect(() => {
ref.current?.focus();
}, [ref]);
};
export const ChatPanel = ({ threadId }: { threadId?: number }) => {
const searchParams = useSearchParams();
const queryMessage = searchParams.get("q");
const hasRun = useRef(false);
const { handleSend, streamingMessage, isStreamingMessage } = useChat();
const { messages, setMessages, setThreadId } = useChatStore();
const [width, setWidth] = useState(0);
const messagesRef = useRef<HTMLDivElement | null>(null);
const messageBottomRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
useAutoScroll(messageBottomRef);
useAutoResizeInput(messagesRef, setWidth);
useAutoFocus(inputRef);
useEffect(() => {
if (queryMessage && !hasRun.current) {
setThreadId(null);
hasRun.current = true;
handleSend(queryMessage);
}
}, [queryMessage]);
useEffect(() => {
if (messages.length == 0) {
setThreadId(null);
}
}, [messages, setThreadId]);
return (
<>
{messages.length > 0 ? (
<div ref={messagesRef} className="pt-10 w-full relative">
<MessagesList
messages={messages}
streamingMessage={streamingMessage}
isStreamingMessage={isStreamingMessage}
onRelatedQuestionSelect={handleSend}
/>
<div ref={messageBottomRef} className="h-0" />
<div
className="bottom-12 fixed px-2 max-w-screen-md justify-center items-center md:px-2"
style={{ width: `${width}px` }}
>
<AskInput isFollowingUp sendMessage={handleSend} />
</div>
</div>
) : (
<div className="w-full flex flex-col justify-center items-center">
<div className="flex flex-col items-center justify-center mb-8">
<span className="text-3xl">{SiteConfig.subPanel}</span>
</div>
<AskInput sendMessage={handleSend} />
<div className="w-full flex flex-row px-3 justify-between items-center space-y-2 pt-8">
<div className="hidden text-tint lg:flex space-x-1 items-center">
<span>People are searching</span>
<TrendingUpIcon className="w-4 h-4" />
</div>
<StarterQuestionsList handleSend={handleSend} />
</div>
</div>
)}
</>
);
};
================================================
FILE: src/components/image-section.tsx
================================================
/* eslint-disable @next/next/no-img-element */
"use client";
import { Skeleton } from "./ui/skeleton";
export const ImageSectionSkeleton = () => {
return (
<>
<div className="my-4 grid grid-cols-1 gap-2 lg:grid-cols-2 w-full">
{[...Array(4)].map((_, index) => (
<div className="w-full h-full" key={`image-skeleton-${index}`}>
<Skeleton className="rounded-md object-cover shadow-none border-none w-full bg-card h-[160px] " />
</div>
))}
</div>
</>
);
};
export function ImagePreload({ images }: { images: string[] }) {
if (images && images.length > 0) {
return (
<div className="flex flex-wrap">
{images.map((image) => (
<img key={image} src={image} className="w-1 h-1 opacity-0" />
))}
</div>
);
}
return null;
}
================================================
FILE: src/components/landing/hero.tsx
================================================
import { Icons } from "@/components/shared/icons";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { TypingTitle } from "./typing-title";
import { SubscribeForm } from "./wailtlist";
export default async function HeroLanding() {
return (
<section className="space-y-6 py-12 sm:py-20 lg:py-20">
<div className="container flex max-w-5xl flex-col items-center gap-5 text-center">
<Link
href="https://x.com/ruiyanghim/status/1816801501161062674"
className={cn(
buttonVariants({ variant: "outline", size: "sm", rounded: "full" }),
"px-4"
)}
target="_blank"
>
<span className="mr-3">🎉</span>
<span className="hidden md:flex">Introducing </span> DiscovAI on{" "}
<Icons.twitter className="ml-2 size-3.5" />
</Link>
<TypingTitle />
<p
className="max-w-2xl text-balance leading-normal text-muted-foreground sm:text-base sm:leading-8 lg:text-xl"
style={{ animationDelay: "0.35s", animationFillMode: "forwards" }}
>
Stay ahead in AI with DiscovAI, Your Go-To Source for the Latest
<span className="text-tint">
{" "}
AI Products | News | Companies | Models
</span>
</p>
<SubscribeForm />
</div>
</section>
);
}
================================================
FILE: src/components/landing/preview-landing.tsx
================================================
import Image from "next/image";
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
export default function PreviewLanding() {
return (
<div className="pb-6 sm:pb-16">
<MaxWidthWrapper>
<div className="rounded-xl md:bg-muted/30 md:p-3.5 md:ring-1 md:ring-inset md:ring-border">
<div className="relative aspect-video overflow-hidden rounded-xl border md:rounded-lg">
<Image
className="size-full object-cover object-center dark:opacity-85"
src="/demo.png"
alt="preview landing"
width={2000}
height={1000}
priority={true}
/>
</div>
</div>
</MaxWidthWrapper>
</div>
);
}
================================================
FILE: src/components/landing/typing-title.tsx
================================================
"use client";
import { useEffect, useState } from "react";
import {
TypewriterEffect,
TypewriterEffectSmooth,
} from "../ui/typewriter-effect";
const typingTextList = [
{
text: "Your",
},
{
text: "Serach ",
},
{
text: "Engine",
},
{
text: "for ",
},
{
text: "Anything",
},
{
text: "About AI",
className: "text-tint dark:text-tint",
},
];
export function TypingTitle() {
const [textList, setTextList] = useState(typingTextList);
// useEffect(() => {
// let effect;
// setTimeout(() => {
// effect = setInterval(() => {
// console.log("setting");
// const newTextList = [...textList];
// newTextList[newTextList.length - 1].text = "Models";
// setTextList(newTextList);
// }, 1000);
// }, 2000);
// }, []);
return (
<>
<h1 className="flex lg:hidden items-center text-balance text-3xl font-extrabold tracking-tight md:text-5xl">
Your Serach Engine for Anything About AI
</h1>
<h1 className="hidden lg:flex items-center text-balance text-3xl font-extrabold tracking-tight sm:text-5xl md:text-6xl lg:text-[66px]">
<TypewriterEffectSmooth words={textList} />
</h1>
</>
);
}
================================================
FILE: src/components/landing/wailtlist.tsx
================================================
"use client";
import { Input } from "@/components/ui/input";
import { cn, nFormatter } from "@/lib/utils";
import { Button } from "../ui/button";
import { Icons } from "../shared/icons";
import { useState, FC } from "react";
import toast from "react-hot-toast";
import { Loader2, ChevronRight } from "lucide-react";
export function WailtList() {
return (
<div
className="flex justify-center space-x-2 md:space-x-4"
style={{ animationDelay: "0.4s", animationFillMode: "forwards" }}
>
<Input
type="email"
placeholder="Email"
className="h-10 rounded-md px-8"
/>
<Button size={"lg"} rounded={"full"} className={"gap-2"}>
<span>Join Waitlist Now</span>
<Icons.arrowRight className="size-4" />
</Button>
</div>
);
}
export const SubscribeForm: FC = () => {
const [email, setEmail] = useState<string>("");
const [message, setMessage] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
// Validate email format
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
if (!emailRegex.test(email)) {
toast("Please enter a valid email address", {
id: "subscripe-toast",
});
return;
}
try {
setLoading(true);
toast.loading("🙏 Thank you...", { id: "subscripe-toast" });
const response = await fetch("/api/join-waitlist", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
throw new Error("Failed to subscribe");
}
toast.success("👍 You have joined the wailt list of DiscovAI !", {
id: "subscripe-toast",
});
setMessage("Subscription successful!");
setEmail("");
} catch (error: any) {
toast.error("Something wrong happens, please try again!", {
id: "subscripe-toast",
});
setMessage(error.message);
} finally {
setLoading(false);
}
};
return (
<form
onSubmit={handleSubscribe}
className="flex justify-center space-x-2 md:space-x-4"
style={{ animationDelay: "0.4s", animationFillMode: "forwards" }}
>
<div className="flex-1">
<label htmlFor="email" className="sr-only">
Email
</label>
<Input
type="email"
placeholder="Your Email address"
value={email}
className="h-10 rounded-md px-8"
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<Button
type="submit"
disabled={loading}
size={"lg"}
rounded={"full"}
className={"gap-2"}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</>
) : (
<>
<ChevronRight className="mr-2 h-4 w-4" />
Join Waitlist
{/* <Icons.arrowRight className="size-4" /> */}
</>
)}
</Button>
</form>
);
};
================================================
FILE: src/components/magicui/typing-animation.tsx
================================================
"use client";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
interface TypingAnimationProps {
duration?: number;
className?: string;
textList: string[];
}
export default function TypingAnimation({
duration = 200,
className,
textList,
}: TypingAnimationProps) {
const [displayedText, setDisplayedText] = useState<string>("");
const [i, setI] = useState<number>(0);
const [wordI, setWordI] = useState(0);
useEffect(() => {
const typingEffect = setInterval(() => {
if (i < textList[wordI].length) {
setDisplayedText(textList[wordI].substring(0, i + 1));
setI(i + 1);
} else {
setTimeout(() => {
setWordI((wordI + 1) % textList.length);
setI(0);
}, 1000);
// clearInterval(typingEffect);
}
}, duration);
return () => {
clearInterval(typingEffect);
};
}, [duration, i, wordI]);
return (
<h1
className={cn(
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm",
className
)}
>
{displayedText ? displayedText : textList[wordI]}
</h1>
);
}
================================================
FILE: src/components/markdown.tsx
================================================
import { FC, memo } from "react";
import ReactMarkdown, { Options } from "react-markdown";
export const MemoizedReactMarkdown: FC<Options> = memo(
ReactMarkdown,
(prevProps, nextProps) =>
prevProps.children === nextProps.children &&
prevProps.className === nextProps.className,
);
================================================
FILE: src/components/message.tsx
================================================
import React, { FC, memo, useEffect, useMemo, useState } from "react";
import { MemoizedReactMarkdown } from "./markdown";
import rehypeRaw from "rehype-raw";
import _ from "lodash";
import { cn } from "@/lib/utils";
import { Skeleton } from "./ui/skeleton";
import { ChatMessage } from "@/schema/chat";
function chunkString(str: string): string[] {
const words = str.split(" ");
const chunks = _.chunk(words, 2).map((chunk) => chunk.join(" ") + " ");
return chunks;
}
export interface MessageProps {
message: ChatMessage;
isStreaming?: boolean;
}
const CitationText = ({ number, href }: { number: number; href: string }) => {
return `
<button className="select-none no-underline">
<a className="" href="${href}" target="_blank">
<span className="relative -top-[0rem] inline-flex">
<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">
${number}
</span>
</span>
</a>
</button>`;
};
const Text = ({
children,
isStreaming,
containerElement = "p",
}: {
children: React.ReactNode;
isStreaming: boolean;
containerElement: React.ElementType;
}) => {
const renderText = (node: React.ReactNode): React.ReactNode => {
if (typeof node === "string") {
const chunks = isStreaming ? chunkString(node) : [node];
return chunks.flatMap((chunk, index) => {
return (
<span
key={`${index}-streaming`}
className={cn(
isStreaming ? "animate-in fade-in-25 duration-700" : ""
)}
>
{chunk}
</span>
);
});
} else if (React.isValidElement(node)) {
return React.cloneElement(
node,
node.props,
renderText(node.props.children)
);
} else if (Array.isArray(node)) {
return node.map((child, index) => (
<React.Fragment key={index}>{renderText(child)}</React.Fragment>
));
}
return null;
};
const text = renderText(children);
return React.createElement(containerElement, {}, text);
};
const StreamingParagraph = memo(
({ children }: React.HTMLProps<HTMLParagraphElement>) => {
return (
<Text isStreaming={true} containerElement="p">
{children}
</Text>
);
}
);
const Paragraph = memo(
({ children }: React.HTMLProps<HTMLParagraphElement>) => {
return (
<Text isStreaming={false} containerElement="p">
{children}
</Text>
);
}
);
const ListItem = memo(({ children }: React.HTMLProps<HTMLLIElement>) => {
return (
<Text isStreaming={false} containerElement="li">
{children}
</Text>
);
});
const StreamingListItem = memo(
({ children }: React.HTMLProps<HTMLLIElement>) => {
return (
<Text isStreaming={true} containerElement="li">
{children}
</Text>
);
}
);
StreamingParagraph.displayName = "StreamingParagraph";
Paragraph.displayName = "Paragraph";
ListItem.displayName = "ListItem";
StreamingListItem.displayName = "StreamingListItem";
export const MessageComponent: FC<MessageProps> = ({
message,
isStreaming = false,
}) => {
const { content, sources } = message;
const [parsedMessage, setParsedMessage] = useState<string>(content);
useEffect(() => {
const citationRegex = /(\[\d+\])/g;
const newMessage = content.replace(citationRegex, (match) => {
const number = match.slice(1, -1);
const source = sources?.find(
(source, idx) => idx + 1 === parseInt(number)
);
return CitationText({
number: parseInt(number),
href: source?.url ?? "",
});
});
setParsedMessage(newMessage);
}, [content, sources]);
return (
<MemoizedReactMarkdown
components={{
// TODO: For some reason, can't pass props into the components
// @ts-ignore
p: isStreaming ? StreamingParagraph : Paragraph,
// @ts-ignore
li: isStreaming ? StreamingListItem : ListItem,
}}
className="prose dark:prose-invert inline leading-relaxed break-words "
rehypePlugins={[rehypeRaw]}
>
{parsedMessage}
</MemoizedReactMarkdown>
);
};
export const MessageComponentSkeleton = () => {
return (
<>
<Skeleton className="w-full py-4 bg-card">
<div className="flex flex-col gap-4">
<Skeleton className="mx-5 h-2 bg-primary/30" />
<Skeleton className="mx-5 h-2 bg-primary/30 mr-20" />
<Skeleton className="mx-5 h-2 bg-primary/30 mr-40" />
</div>
</Skeleton>
</>
);
};
================================================
FILE: src/components/messages-list.tsx
================================================
import { AssistantMessageContent } from "./assistant-message";
import { Separator } from "./ui/separator";
import { UserMessageContent } from "./user-message";
import { memo } from "react";
import { ChatMessage, MessageRole } from "@/schema/chat";
const MessagesList = ({
messages,
streamingMessage,
isStreamingMessage,
onRelatedQuestionSelect,
}: {
messages: ChatMessage[];
streamingMessage: ChatMessage | null;
isStreamingMessage: boolean;
onRelatedQuestionSelect: (question: string) => void;
}) => {
return (
<div className="flex flex-col pb-28">
{messages.map((message, index) =>
message.role === MessageRole.USER ? (
<UserMessageContent key={index} message={message} />
) : (
<div key={index}>
<AssistantMessageContent
key={index}
message={message}
onRelatedQuestionSelect={onRelatedQuestionSelect}
/>
{index !== messages.length - 1 && <Separator />}
</div>
)
)}
{streamingMessage && isStreamingMessage && (
<AssistantMessageContent
message={streamingMessage}
isStreaming={true}
onRelatedQuestionSelect={onRelatedQuestionSelect}
/>
)}
</div>
);
};
export default memo(MessagesList);
================================================
FILE: src/components/mode-toggle.tsx
================================================
"use client";
import * as React from "react";
import { ComputerIcon, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{["light", "dark", "system"].map((theme) => (
<DropdownMenuItem
key={theme}
className="flex gap-2 items-center font-medium"
onClick={() => setTheme(theme)}
>
{theme === "light" && <Sun size={12} />}
{theme === "dark" && <Moon size={12} />}
{theme === "system" && <ComputerIcon size={12} />}
{theme.charAt(0).toUpperCase() + theme.slice(1)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
================================================
FILE: src/components/more-results.tsx
================================================
import { PlusIcon } from "lucide-react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import Image from "next/image";
export default function MoreResults({
results,
}: {
results: { url: string; title: string; screenshot_url: string }[];
}) {
return (
<div className="divide-y border-t mt-2">
<Accordion type="single" collapsible className="w-full">
{results.map((res, i) => (
<AccordionItem value={res.url} key={i}>
<AccordionTrigger>
<div className="text-left">{res.title}</div>
</AccordionTrigger>
<AccordionContent>
<a href={res.url} target="_blank">
<img
src={res.screenshot_url}
alt={res.title}
width={1920}
height={1080}
className="rounded-lg"
/>
</a>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}
================================================
FILE: src/components/nav.tsx
================================================
"use client";
import Link from "next/link";
import { ModeToggle } from "./mode-toggle";
import { useTheme } from "next-themes";
import { Button, buttonVariants } from "./ui/button";
import { PlusIcon } from "lucide-react";
import { useChatStore } from "@/stores";
import { useRouter } from "next/navigation";
import { SiteConfig, LinkConfig } from "@/config/sites";
import { GitHubLogoIcon } from "@radix-ui/react-icons";
import { Icons } from "./shared/icons";
import { cn } from "@/lib/utils";
const NewChatButton = () => {
return (
<Button variant="secondary" size="sm" onClick={() => (location.href = "/")}>
<PlusIcon className="w-4 h-4" />
</Button>
);
};
const TextLogo = () => {
// return <></>;
return (
<div className="text-2xl font-medium hidden sm:block">
{SiteConfig.name}
</div>
);
};
export function Navbar() {
const router = useRouter();
const { theme } = useTheme();
const { messages } = useChatStore();
const onHomePage = messages.length === 0;
return (
<header className="w-full flex fixed p-1 z-50 px-2 bg-background/95 justify-between items-center">
<div className="flex items-center gap-2 p-2">
<Link href="/" passHref onClick={() => (location.href = "/")}>
<img
src={theme === "light" ? "/logo.svg" : "/logo.svg"}
alt="Logo"
className="w-4 h-4 sm:w-8 sm:h-8"
/>
</Link>
{onHomePage ? <TextLogo /> : <NewChatButton />}
</div>
<div className="flex items-center gap-4 pr-2">
<ModeToggle />
<Link
href={LinkConfig.github}
target="_blank"
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
>
<GitHubLogoIcon />
</Link>
<Link
href={LinkConfig.twitter}
target="_blank"
className={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
>
<Icons.twitter className="w-3 h-3" />
</Link>
</div>
</header>
);
}
================================================
FILE: src/components/related-questions.tsx
================================================
import { PlusIcon } from "lucide-react";
export default function RelatedQuestions({
questions,
onSelect,
}: {
questions: string[];
onSelect: (question: string) => void;
}) {
return (
<div className="divide-y border-t mt-2">
{questions.map((question, index) => (
<div
key={`question-${index}`}
className="flex cursor-pointer items-center py-2 font-medium justify-between "
onClick={() => onSelect(question)}
>
<span>{question.toLowerCase()}</span>
<PlusIcon className="text-tint mr-2" size={20} />
</div>
))}
</div>
);
}
================================================
FILE: src/components/search-results.tsx
================================================
/* eslint-disable @next/next/no-img-element */
"use client";
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "./ui/skeleton";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { SearchResult } from "@/schema/chat";
export const SearchResultsSkeleton = () => {
return (
<>
<div className="flex flex-wrap w-full">
{[...Array(4)].map((_, index) => (
<div className="w-1/2 md:w-1/4 p-1" key={`skeleton-${index}`}>
<Skeleton className="rounded-md shadow-none border-none h-[70px] bg-card " />
</div>
))}
</div>
</>
);
};
export const Logo = ({ url }: { url: string }) => {
return (
<div className="rounded-full overflow-hidden relative">
<img
className="block relative"
src={`https://www.google.com/s2/favicons?sz=128&domain=${url}`}
alt="favicon"
width={16}
height={16}
/>
</div>
);
};
export function SearchResults({ results }: { results: SearchResult[] }) {
const [showAll, setShowAll] = useState(false);
const displayedResults = showAll ? results : results.slice(0, 3);
const additionalCount = results.length > 3 ? results.length - 3 : 0;
const additionalResults = results.slice(3, 3 + additionalCount);
return (
<div className="flex flex-wrap w-full ">
{displayedResults.map(({ title, url, content, description }, index) => {
const formattedUrl = new URL(url).hostname.split(".").slice(-2, -1)[0];
return (
<HoverCard key={`source-${index}`}>
<HoverCardTrigger asChild>
<div className="w-1/2 md:w-1/4 p-1">
<a className="" href={url} target="_blank">
<Card className="flex-1 rounded-md flex-col shadow-none border-none h-[70px]">
<CardContent className="p-2 flex flex-col justify-between h-full">
<p className="text-xs line-clamp-2 font-medium text-foreground/80">
{title} | {description}
</p>
<div className="flex space-x-1">
<div className="flex items-center space-x-2">
<div className="rounded-full overflow-hidden relative">
<Logo url={url} />
</div>
<div className="text-xs text-muted-foreground truncate font-medium">
{formattedUrl}
</div>
</div>
<div className="text-xs text-muted-foreground font-medium">
·
</div>
<div className="text-xs text-muted-foreground truncate font-medium">
{index + 1}
</div>
</div>
</CardContent>
</Card>
</a>
</div>
</HoverCardTrigger>
<HoverCardContent className="w-80 py-2">
<div className="flex justify-between space-x-4">
<div className="space-y-1">
<div className="flex items-center space-x-2">
<div className="rounded-full overflow-hidden relative">
<Logo url={url} />
</div>
<div className="text-xs text-muted-foreground truncate font-medium">
{formattedUrl}
</div>
</div>
<p className="text-sm font-medium">{title}</p>
<span className="text-sm line-clamp-3 font-light text-foreground/90">
{content}
</span>
</div>
</div>
</HoverCardContent>
</HoverCard>
);
})}
{!showAll && additionalCount > 0 && (
<div
className="cursor-pointer
w-1/2 md:w-1/4 p-1"
onClick={() => setShowAll(true)}
>
<Card className="flex-1 rounded-md flex-col shadow-none border-none h-[70px]">
<CardContent className="p-2 flex flex-col justify-between h-full">
<div className="flex items-center space-x-2">
{additionalResults.map(({ url }, index) => {
return <Logo url={url} key={`logo-${index}`} />;
})}
</div>
<div className="text-xs text-muted-foreground truncate font-medium">
View {additionalCount} more
</div>
</CardContent>
</Card>
</div>
)}
</div>
);
}
================================================
FILE: src/components/section.tsx
================================================
import { cn } from "@/lib/utils";
import {
BookOpen,
BookOpenIcon,
CameraIcon,
ListOrderedIcon,
ListPlusIcon,
SparkleIcon,
SparklesIcon,
StarIcon,
TextSearchIcon,
} from "lucide-react";
import { motion } from "framer-motion";
export const Section = ({
title,
children,
animate = true,
streaming = false,
}: {
title: "Sources" | "Answer" | "Related" | "Images" | "More Results";
children: React.ReactNode;
animate?: boolean;
streaming?: boolean;
}) => {
const iconMap = {
Sources: BookOpenIcon,
Answer: SparkleIcon,
Related: ListPlusIcon,
Images: CameraIcon,
"More Results": ListOrderedIcon,
};
const IconComponent = iconMap[title] || StarIcon;
return (
<div
className={cn(
"flex flex-col mb-8",
animate ? "animate-in fade-in duration-1000 ease-out" : ""
)}
>
<div className="flex items-center space-x-2">
{title === "Answer" && streaming ? (
<motion.div
animate={{ rotate: [0, 360] }}
transition={{ repeat: Infinity, duration: 1.5, ease: "linear" }}
>
<IconComponent size={22} />
</motion.div>
) : (
<IconComponent size={22} />
)}
<div className="text-lg font-medium">{title}</div>
</div>
<div className="pt-1">{children}</div>
</div>
);
};
================================================
FILE: src/components/shared/icons.tsx
================================================
import {
AlertTriangle,
ArrowRight,
ArrowUpRight,
Check,
ChevronLeft,
ChevronRight,
Copy,
CreditCard,
File,
FileText,
HelpCircle,
Image,
Laptop,
Loader2,
LucideIcon,
LucideProps,
Moon,
MoreVertical,
Plus,
Puzzle,
Search,
Settings,
SunMedium,
Trash,
User,
X,
} from "lucide-react";
export type Icon = LucideIcon;
export const Icons = {
add: Plus,
arrowRight: ArrowRight,
arrowUpRight: ArrowUpRight,
billing: CreditCard,
chevronLeft: ChevronLeft,
chevronRight: ChevronRight,
check: Check,
close: X,
copy: Copy,
ellipsis: MoreVertical,
gitHub: ({ ...props }: LucideProps) => (
<svg
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="github"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 496 512"
{...props}
>
<path
fill="currentColor"
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"
></path>
</svg>
),
google: ({ ...props }: LucideProps) => (
<svg
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="google"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
{...props}
>
<path
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"
fill="currentColor"
/>
</svg>
),
nextjs: ({ ...props }: LucideProps) => (
<svg
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="nextjs"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 15 15"
{...props}
>
<path
fill="currentColor"
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"
></path>
</svg>
),
help: HelpCircle,
laptop: Laptop,
logo: Puzzle,
media: Image,
moon: Moon,
page: File,
post: FileText,
search: Search,
settings: Settings,
spinner: Loader2,
sun: SunMedium,
trash: Trash,
twitter: ({ ...props }: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="twitter"
role="img"
{...props}
>
<path
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"
fill="currentColor"
/>
</svg>
),
user: User,
warning: AlertTriangle,
};
================================================
FILE: src/components/shared/max-width-wrapper.tsx
================================================
import { ReactNode } from "react";
import { cn } from "@/lib/utils";
export default function MaxWidthWrapper({
className,
children,
large = false,
}: {
className?: string;
large?: boolean;
children: ReactNode;
}) {
return (
<div
className={cn(
"container",
large ? "max-w-screen-2xl" : "max-w-6xl",
className,
)}
>
{children}
</div>
);
}
================================================
FILE: src/components/starter-questions.tsx
================================================
import { ArrowRight, ArrowUpRight } from "lucide-react";
const starterQuestions = [
"I want to make old photos high definition",
"Tools that can help me market better on Twitter",
"Auto-generated seo friendly blogs from my website",
"I need an AI girlfriend with multiple roles to choose from",
];
export const StarterQuestionsList = ({
handleSend,
}: {
handleSend: (question: string) => void;
}) => {
return (
<ul className="flex flex-col space-y-2 pt-2">
{starterQuestions.map((question) => (
<li key={question} className="flex items-center space-x-2">
<button
onClick={() => handleSend(question)}
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"
>
{question}
<ArrowUpRight size={18} className="text-tint" />
</button>
</li>
))}
</ul>
);
};
================================================
FILE: src/components/theme-provider.tsx
================================================
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
================================================
FILE: src/components/ui/accordion.tsx
================================================
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
================================================
FILE: src/components/ui/alert.tsx
================================================
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"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",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };
================================================
FILE: src/components/ui/avatar.tsx
================================================
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
================================================
FILE: src/components/ui/badge.tsx
================================================
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"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",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
================================================
FILE: src/components/ui/button.tsx
================================================
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"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",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
rounded: {
default: "rounded-md",
sm: "rounded-sm",
lg: "rounded-lg",
xl: "rounded-xl",
"2xl": "rounded-2xl",
full: "rounded-full",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
================================================
FILE: src/components/ui/card.tsx
================================================
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
================================================
FILE: src/components/ui/dropdown-menu.tsx
================================================
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"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",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"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",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"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",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"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",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"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",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
================================================
FILE: src/components/ui/hover-card.tsx
================================================
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
const HoverCard = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Root>
>(({ ...props }, ref) => (
<HoverCardPrimitive.Root openDelay={200} closeDelay={100} {...props} />
));
HoverCard.displayName = HoverCardPrimitive.Root.displayName;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"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",
className,
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };
================================================
FILE: src/components/ui/input.tsx
================================================
import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"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",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };
================================================
FILE: src/components/ui/label.tsx
================================================
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
================================================
FILE: src/components/ui/select.tsx
================================================
"use client";
import * as React from "react";
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"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",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"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",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"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",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
================================================
FILE: src/components/ui/separator.tsx
================================================
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
================================================
FILE: src/components/ui/skeleton.tsx
================================================
import { cn } from "@/lib/utils";
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
);
}
export { Skeleton };
================================================
FILE: src/components/ui/switch.tsx
================================================
"use client";
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"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 ",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"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",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
================================================
FILE: src/components/ui/tabs.tsx
================================================
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"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",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
================================================
FILE: src/components/ui/textarea.tsx
================================================
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"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",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";
export { Textarea };
================================================
FILE: src/components/ui/timeline.tsx
================================================
//github.com/shadcn-ui/ui/pull/3374 + cursor for modifications :)
import React from "react";
import { CheckIcon, CircleIcon, Cross1Icon } from "@radix-ui/react-icons";
import { VariantProps, cva } from "class-variance-authority";
import { cn } from "@/lib/utils";
const timelineVariants = cva("grid", {
variants: {
positions: {
left: "[&>li]:grid-cols-[0_min-content_1fr]",
right: "[&>li]:grid-cols-[1fr_min-content]",
center: "[&>li]:grid-cols-[1fr_min-content_1fr]",
},
},
defaultVariants: {
positions: "left",
},
});
interface TimelineProps
extends React.HTMLAttributes<HTMLUListElement>,
VariantProps<typeof timelineVariants> {}
const Timeline = React.forwardRef<HTMLUListElement, TimelineProps>(
({ children, className, positions, ...props }, ref) => {
return (
<ul
className={cn(timelineVariants({ positions }), className)}
ref={ref}
{...props}
>
{children}
</ul>
);
},
);
Timeline.displayName = "Timeline";
const timelineItemVariants = cva("grid items-start gap-x-2", {
variants: {
status: {
done: "text-primary",
default: "text-muted-foreground",
},
},
defaultVariants: {
status: "default",
},
});
interface TimelineItemProps
extends React.HTMLAttributes<HTMLLIElement>,
VariantProps<typeof timelineItemVariants> {}
const TimelineItem = React.forwardRef<HTMLLIElement, TimelineItemProps>(
({ className, status, ...props }, ref) => (
<li
className={cn(timelineItemVariants({ status }), className)}
ref={ref}
{...props}
/>
),
);
TimelineItem.displayName = "TimelineItem";
const timelineDotVariants = cva(
"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",
{
variants: {
status: {
default: "[&>*]:hidden border-tint border-[0.5px]",
current:
"[&>*:not(.radix-circle)]:hidden [&>.radix-circle]:bg-tint [&>.radix-circle]:fill-tint [&>.radix-circle]:size-3",
done: "bg-tint [&>*:not(.radix-check)]:hidden [&>.radix-check]:text-background [&>.radix-check]:size-2",
error:
"border-destructive bg-destructive [&>*:not(.radix-cross)]:hidden [&>.radix-cross]:text-background [&>.radix-cross]:size-2",
custom:
"[&>*:not(:nth-child(4))]:hidden [&>*:nth-child(4)]:block [&>*:nth-child(4)]:size-2",
},
},
defaultVariants: {
status: "default",
},
},
);
interface TimelineDotProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof timelineDotVariants> {
customIcon?: React.ReactNode;
}
const TimelineDot = React.forwardRef<HTMLDivElement, TimelineDotProps>(
({ className, status, customIcon, ...props }, ref) => (
<div
role="status"
className={cn("timeline-dot", timelineDotVariants({ status }), className)}
ref={ref}
{...props}
>
<div className="radix-circle size-2 rounded-full" />
<CheckIcon className="radix-check size-2" />
<Cross1Icon className="radix-cross size-2" />
{customIcon}
</div>
),
);
TimelineDot.displayName = "TimelineDot";
const timelineContentVariants = cva(
"row-start-1 row-end-2 text-muted-foreground",
{
variants: {
side: {
right: "col-start-3 col-end-4 mr-auto text-left",
left: "col-start-1 col-end-2 ml-auto text-right",
},
},
defaultVariants: {
side: "right",
},
},
);
interface TimelineConentProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof timelineContentVariants> {}
const TimelineContent = React.forwardRef<HTMLDivElement, TimelineConentProps>(
({ className, side, ...props }, ref) => (
<div
className={cn(timelineContentVariants({ side }), className)}
ref={ref}
{...props}
/>
),
);
TimelineContent.displayName = "TimelineContent";
const timelineHeadingVariants = cva(
"row-start-2 row-end-3 line-clamp-1 max-w-full truncate",
{
variants: {
side: {
right: "col-start-3 col-end-4 mr-auto text-left",
left: "col-start-1 col-end-2 ml-auto text-right",
},
variant: {
primary: "text-base font-medium text-primary",
secondary: "text-sm font-light text-muted-foreground",
},
},
defaultVariants: {
side: "right",
variant: "primary",
},
},
);
interface TimelineHeadingProps
extends React.HTMLAttributes<HTMLParagraphElement>,
VariantProps<typeof timelineHeadingVariants> {}
const TimelineHeading = React.forwardRef<
HTMLParagraphElement,
TimelineHeadingProps
>(({ className, side, variant, ...props }, ref) => (
<p
role="heading"
aria-level={variant === "primary" ? 2 : 3}
className={cn(timelineHeadingVariants({ side, variant }), className)}
ref={ref}
{...props}
/>
));
TimelineHeading.displayName = "TimelineHeading";
interface TimelineLineProps extends React.HTMLAttributes<HTMLHRElement> {
done?: boolean;
}
const TimelineLine = React.forwardRef<HTMLHRElement, TimelineLineProps>(
({ className, done = false, ...props }, ref) => {
return (
<hr
role="separator"
aria-orientation="vertical"
className={cn(
"col-start-2 col-end-3 row-start-1 row-end-3 mx-auto flex h-full w-[3px] justify-center rounded-full",
"mt-6 mb-2",
done ? "bg-muted" : "bg-muted",
className,
)}
ref={ref}
{...props}
/>
);
},
);
TimelineLine.displayName = "TimelineLine";
export {
Timeline,
TimelineDot,
TimelineItem,
TimelineContent,
TimelineHeading,
TimelineLine,
};
================================================
FILE: src/components/ui/toast.tsx
================================================
"use client";
import * as React from "react";
import { Cross2Icon } from "@radix-ui/react-icons";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"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]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"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",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"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",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"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",
className,
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};
================================================
FILE: src/components/ui/toaster.tsx
================================================
"use client";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}
================================================
FILE: src/components/ui/toggle.tsx
================================================
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"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",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-3",
sm: "h-8 px-2",
lg: "h-10 px-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };
================================================
FILE: src/components/ui/tooltip.tsx
================================================
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"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",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
================================================
FILE: src/components/ui/typewriter-effect.tsx
================================================
"use client";
import { cn } from "@/lib/utils";
import { motion, stagger, useAnimate, useInView } from "framer-motion";
import { useEffect } from "react";
export const TypewriterEffect = ({
words,
className,
cursorClassName,
}: {
words: {
text: string;
className?: string;
}[];
className?: string;
cursorClassName?: string;
}) => {
// split text inside of words into array of characters
const wordsArray = words.map((word) => {
return {
...word,
text: word.text.split(""),
};
});
const [scope, animate] = useAnimate();
const isInView = useInView(scope);
useEffect(() => {
if (isInView) {
animate(
"span",
{
display: "inline-block",
opacity: 1,
width: "fit-content",
},
{
duration: 0.3,
delay: stagger(0.1),
ease: "easeInOut",
}
);
}
}, [isInView]);
const renderWords = () => {
return (
<motion.div ref={scope} className="inline">
{wordsArray.map((word, idx) => {
return (
<div key={`word-${idx}`} className="inline-block">
{word.text.map((char, index) => (
<motion.span
initial={{}}
key={`char-${index}`}
className={cn(
`dark:text-white text-black opacity-0 hidden`,
word.className
)}
>
{char}
</motion.span>
))}
</div>
);
})}
</motion.div>
);
};
return (
<div
className={cn(
"text-base sm:text-xl md:text-3xl lg:text-5xl font-bold text-center",
className
)}
>
{renderWords()}
<motion.span
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
transition={{
duration: 0.8,
repeat: Infinity,
repeatType: "reverse",
}}
className={cn(
"inline-block rounded-sm w-[4px] h-4 md:h-6 lg:h-10 bg-blue-500",
cursorClassName
)}
></motion.span>
</div>
);
};
export const TypewriterEffectSmooth = ({
words,
className,
cursorClassName,
}: {
words: {
text: string;
className?: string;
}[];
className?: string;
cursorClassName?: string;
}) => {
// split text inside of words into array of characters
const wordsArray = words.map((word) => {
return {
...word,
text: word.text.split(""),
};
});
const renderWords = () => {
return (
<div>
{wordsArray.map((word, idx) => {
return (
<div key={`word-${idx}`} className="inline-block">
{word.text.map((char, index) => (
<span
key={`char-${index}`}
className={cn(`dark:text-white text-black `, word.className)}
>
{char}
</span>
))}
</div>
);
})}
</div>
);
};
return (
<div className={cn("flex space-x-1 my-6", className)}>
<motion.div
className="overflow-hidden pb-2"
initial={{
width: "0%",
}}
whileInView={{
width: "fit-content",
}}
transition={{
duration: 2,
ease: "linear",
delay: 1,
}}
>
<div
className="text-xs sm:text-base md:text-xl lg:text-5xl xl:text-5xl font-bold"
style={{
whiteSpace: "nowrap",
}}
>
{renderWords()}{" "}
</div>{" "}
</motion.div>
<motion.span
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
transition={{
duration: 0.8,
repeat: Infinity,
repeatType: "reverse",
}}
className={cn(
"block rounded-sm w-[4px] h-4 sm:h-6 lg:h-12 bg-tint",
cursorClassName
)}
></motion.span>
</div>
);
};
================================================
FILE: src/components/ui/use-toast.ts
================================================
"use client";
// Inspired by react-hot-toast library
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };
================================================
FILE: src/components/user-message.tsx
================================================
import { ChatMessage } from "@/schema/chat";
export const UserMessageContent = ({ message }: { message: ChatMessage }) => {
return (
<div className="my-4">
<span className="text-3xl">{message.content}</span>
</div>
);
};
================================================
FILE: src/config/sites.ts
================================================
export const SiteConfig = {
name: "DiscovAI",
title: "DiscovAI",
metaTitle: "DiscovAI - Discover top ai tools best match your need",
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",
panel: "Everything about AI",
subPanel: "Discover AI from over 15,349 tools for you need",
};
export const LinkConfig = {
site: "https://discovai.io",
github: "https://github.com/DiscovAI/DiscovAI-search",
twitter: "https://x.com/ruiyanghim",
};
================================================
FILE: src/db/init.sql
================================================
CREATE EXTENSION IF NOT EXISTS "vector";
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- content table
CREATE TABLE aitools (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
title TEXT NOT NULL,
url TEXT NOT NULL,
description TEXT,
content TEXT,
screenshot_url TEXT,
full_content TEXT,
detail TEXT,
cat TEXT,
ext_info JSONB,
total_visits_last_three_months INT,
visits_last_month INT,
bounce_rate DECIMAL,
page_per_visit DECIMAL,
time_on_site DECIMAL,
traffic_detail JSON;
);
-- chunk table
CREATE TABLE aitools_chunk (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
chunk_text TEXT,
metadata JSONB,
tool_id UUID NOT NULL,
embedding vector(768),
FOREIGN KEY (tool_id) REFERENCES aitools(id)
);
-- hnsw index for query performance
create index on aitools_chunk using hnsw (embedding vector_l2_ops);
-- rpc function for supabase client
create or replace function match_embeddings (
query_embedding vector(768),
match_threshold float,
match_count int
)
returns table (
id UUID,
metadata JSONB,
tool_id text,
chunk_text text,
similarity float,
screenshot_url text
)
language sql stable
as $$
select
aitools_chunk.id,
aitools_chunk.metadata,
aitools_chunk.tool_id,
aitools_chunk.chunk_text,
1 - (aitools_chunk.embedding <=> query_embedding) as similarity,
aitools.screenshot_url
from aitools_chunk
join aitools on aitools_chunk.tool_id = aitools.id
where 1 - (aitools_chunk.embedding <=> query_embedding) > match_threshold
order by (aitools_chunk.embedding <=> query_embedding) asc
limit match_count;
$$;
================================================
FILE: src/db/redis.ts
================================================
// lib/redis.ts
import { Redis } from "@upstash/redis";
import { Ratelimit } from "@upstash/ratelimit";
import { env } from "@/env.mjs";
export const embeddingVectorCacheKey = (query: string) =>
`cache:search:${query}`;
export const llmResultCacheKey = (prompt: string) => `cache:llm:${prompt}`;
export const redis = new Redis({
url: env.UPSTASH_REDIS_REST_URL || "",
token: env.UPSTASH_REDIS_REST_TOKEN || "",
});
export const ratelimit = new Ratelimit({
redis: redis,
limiter: Ratelimit.slidingWindow(10, "10 s"),
analytics: true,
prefix: "@upstash/ratelimit",
});
================================================
FILE: src/db/supabase.ts
================================================
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
================================================
FILE: src/env.mjs
================================================
/* eslint-disable no-process-env */
import { createEnv } from "@t3-oss/env-nextjs";
import z from "zod";
export const env = createEnv({
server: {
NEXT_PUBLIC_SUPABASE_URL: z.string(),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string(),
JINA_API_KEY: z.string(),
OPENAI_API_KEY: z.string(),
OPENAI_API_URL: z.string().nullable(),
UPSTASH_REDIS_REST_URL: z.string(),
UPSTASH_REDIS_REST_TOKEN: z.string(),
},
client: {},
runtimeEnv: {
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
JINA_API_KEY: process.env.JINA_API_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
OPENAI_API_URL: process.env.OPENAI_API_URL,
UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
},
});
================================================
FILE: src/hooks/chat.ts
================================================
import { useMutation } from "@tanstack/react-query";
import {
ChatMessage,
ChatRequest,
ChatResponseEvent,
ErrorStream,
Message,
MessageRole,
MoreResultsStream,
RelatedQueriesStream,
SearchResult,
SearchResultStream,
StreamEndStream,
StreamEvent,
TextChunkStream,
} from "@/schema/chat";
import Error from "next/error";
import {
fetchEventSource,
FetchEventSourceInit,
} from "@microsoft/fetch-event-source";
import { useState } from "react";
import { useChatStore } from "@/stores";
import { env } from "../env.mjs";
import { useRouter } from "next/navigation";
const streamChat = async ({
request,
onMessage,
}: {
request: ChatRequest;
onMessage?: FetchEventSourceInit["onmessage"];
}): Promise<void> => {
try {
return await fetchEventSource("api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
keepalive: true,
openWhenHidden: true,
body: JSON.stringify({ ...request }),
// credentials: "include", // if need cookie
onmessage: onMessage,
onerror: (error) => {
console.error(error);
},
});
} catch (error) {
console.error(error);
}
};
const convertToChatRequest = (query: string, history: ChatMessage[]) => {
const newHistory: Message[] = history.map((message) => ({
role:
message.role === MessageRole.USER
? MessageRole.USER
: MessageRole.ASSISTANT,
content: message.content,
}));
return { query, history: newHistory };
};
export const useChat = () => {
const { addMessage, messages, threadId, setThreadId } = useChatStore();
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(
null
);
const [isStreamingProSearch, setIsStreamingProSearch] = useState(false);
const [isStreamingMessage, setIsStreamingMessage] = useState(false);
const handleEvent = (eventItem: ChatResponseEvent, state: ChatMessage) => {
switch (eventItem.event) {
case StreamEvent.BEGIN_STREAM:
setIsStreamingMessage(true);
setStreamingMessage({
...state,
role: MessageRole.ASSISTANT,
content: "",
related_queries: [],
sources: [],
images: [],
});
break;
case StreamEvent.SEARCH_RESULTS:
const data = eventItem.data as SearchResultStream;
state.sources = data.results ?? [];
state.images = data.images ?? [];
break;
case StreamEvent.TEXT_CHUNK:
state.content += (eventItem.data as TextChunkStream).text;
break;
case StreamEvent.RELATED_QUERIES:
state.related_queries =
(eventItem.data as RelatedQueriesStream).related_queries ?? [];
break;
case StreamEvent.MORE_RESULTS:
state.more_results =
(eventItem.data as MoreResultsStream).more_results ?? [];
break;
case StreamEvent.STREAM_END:
const endData = eventItem.data as StreamEndStream;
addMessage({ ...state });
setStreamingMessage(null);
setIsStreamingMessage(false);
setIsStreamingProSearch(false);
// Only if the backend is using the DB
if (endData.thread_id) {
setThreadId(endData.thread_id);
window.history.pushState({}, "", `/search/${endData.thread_id}`);
}
return;
case StreamEvent.ERROR:
const errorData = eventItem.data as ErrorStream;
addMessage({
role: MessageRole.ASSISTANT,
content: errorData.detail,
related_queries: [],
sources: [],
images: [],
is_error_message: true,
});
setStreamingMessage(null);
setIsStreamingMessage(false);
setIsStreamingProSearch(false);
return;
}
setStreamingMessage({
role: MessageRole.ASSISTANT,
content: state.content,
related_queries: state.related_queries,
sources: state.sources,
images: state.images,
more_results: state.more_results,
});
};
const { mutateAsync: chat } = useMutation<void, Error, ChatRequest>({
retry: false,
mutationFn: async (request) => {
const state: ChatMessage = {
role: MessageRole.ASSISTANT,
content: "",
sources: [],
related_queries: [],
images: [],
more_results: [],
};
addMessage({ role: MessageRole.USER, content: request.query });
const req = {
...request,
thread_id: threadId,
};
await streamChat({
request: req,
onMessage: (event) => {
if (!event.data) return;
const eventItem: ChatResponseEvent = JSON.parse(event.data);
handleEvent(eventItem, state);
},
});
},
});
const handleSend = async (query: string) => {
await chat(convertToChatRequest(query, messages));
};
return {
handleSend,
streamingMessage,
isStreamingMessage,
isStreamingProSearch,
};
};
================================================
FILE: src/lib/chat/embedding.ts
================================================
import { JinaEmbeddings } from "@langchain/community/embeddings/jina";
import { env } from "@/env.mjs";
const embeddings = new JinaEmbeddings({
model: "jina-embeddings-v2-base-en",
apiKey: env.JINA_API_KEY,
});
export async function generateDocEmbedding(contents: string[]) {
const documentEmbeddings = await embeddings.embedDocuments(contents);
return documentEmbeddings;
}
export async function generateQueyEmbedding(query: string) {
const embedding = await embeddings.embedQuery(query);
return embedding;
}
================================================
FILE: src/lib/chat/llm.ts
================================================
import { CHAT_PROMPT, RELATED_QUESTION_PROMPT, TRANSLATE } from "./prompts";
// import { OpenAI } from "@langchain/openai";
// import type { AIMessageChunk } from "@langchain/core/messages";
// import { concat } from "@langchain/core/utils/stream";
import { streamText, generateText, generateObject } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import z from "zod";
import { containsChinese } from "../utils";
import { env } from "@/env.mjs";
const openai = createOpenAI({
apiKey: env.OPENAI_API_KEY,
baseURL: env.OPENAI_API_URL,
});
function documentToStr(doc) {
const { metadata, chunk_text, screenshot_url } = doc;
return `Title: ${metadata.title}\nURL: ${metadata.url}\nScreenshotUrl: ${screenshot_url}\nSummary: ${chunk_text}`;
}
function formatContext(searchResults) {
return searchResults
.map((result, index) => `Citation ${index + 1}. ${documentToStr(result)}`)
.join("\n\n");
}
export const formatePrompt = (contexts, query) =>
CHAT_PROMPT(formatContext(contexts), query);
export async function genLLMTextChunk({ query, contexts }) {
const prompt = formatePrompt(contexts, query);
const model = openai("gpt-4o-mini");
const result = await streamText({
model: model,
prompt: prompt,
});
return result;
}
export async function genRelatedQuery({ query, contexts }) {
const prompt = RELATED_QUESTION_PROMPT(
JSON.stringify(contexts).slice(0, 4000),
query
);
const model = openai("gpt-4o-mini");
const result = await generateObject({
model,
prompt,
schema: z.object({
items: z.array(z.string()).length(3),
}),
});
return result.object.items;
}
export async function translate({ query }) {
if (!containsChinese(query)) {
return query;
}
const prompt = TRANSLATE(query);
const model = openai("gpt-4o-mini");
const { text } = await generateText({
model,
prompt,
});
return text;
}
================================================
FILE: src/lib/chat/prompts.ts
================================================
export const CHAT_PROMPT = (contexts: string, query: string) => `\
As a professional AI tool search expert. Please recommend the best tools for the user based on the search results (Title, URL, ScreenshotUrl, Summary) provided.
You must only use the information in the search results provided.Use a professional tone.
You must introduce each tool in context.
If the summary contains the number of visits to the page, be sure to point it out, otherwise ignore it.
You must cite the answer using [number] notation. You must cite sentences with their relevant citation number. Cite every part of the answer.
Place citations at the end of the sentence. You can do multiple citations in a row with the format [number1][number2].
Only 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.
ONLY cite inline.
DO NOT include a reference section, DO NOT include URLs.
DO NOT repeat the question.
You can use markdown formatting. You should include bullets to list the information in your answer.
For each item, you must add the image of screenshotUrl below like this:
<a href={URL} target="_blank"><img src={ScreenshotUrl} alt={title} style="border-radius:8px;" /></a>
<context>
${contexts}
</context>
---------------------
Make sure to match the language of the user's question.
Question: ${query}
Answer (in the language of the user's question): \
`;
export const RELATED_QUESTION_PROMPT = (context: string, query: string) => `
Given a question and search result context, generate 3 follow-up questions the user might ask. Use the original question and context.
Instructions:
- Generate exactly 3 questions.
- These questions should be concise, and simple.
- Ensure the follow-up questions are relevant to the original question and context.
Make sure to match the language of the user's question.
Original Question: ${query}
<context>
${context}
</context>
Output:
related_questions: A list of EXACTLY three concise, simple follow-up questions
`;
export const TRANSLATE = (query: string) => `
Directly translate it to english, no other words.
Question: ${query}
`;
================================================
FILE: src/lib/utils.ts
================================================
import { ChatResponseEvent } from "@/schema/chat";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function nFormatter(num: number, digits?: number) {
if (!num) return "0";
const lookup = [
{ value: 1, symbol: "" },
{ value: 1e3, symbol: "K" },
{ value: 1e6, symbol: "M" },
{ value: 1e9, symbol: "G" },
{ value: 1e12, symbol: "T" },
{ value: 1e15, symbol: "P" },
{ value: 1e18, symbol: "E" },
];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
var item = lookup
.slice()
.reverse()
.find(function (item) {
return num >= item.value;
});
return item
? (num / item.value).toFixed(digits || 1).replace(rx, "$1") + item.symbol
: "0";
}
export const genStream = (o: ChatResponseEvent) => {
const encoder = new TextEncoder();
return encoder.encode(`data: ${JSON.stringify(o)}\n\n`);
};
export function containsChinese(str: string) {
const chineseRegex = /[\u4e00-\u9fa5]/;
return chineseRegex.test(str);
}
function addQueryParams(url, params) {
const urlObj = new URL(url);
Object.keys(params).forEach((key) =>
urlObj.searchParams.append(key, params[key])
);
return urlObj.toString();
}
const params = {
ref: "discovai-io",
utm_source: "discovai-io",
utm_medium: "referral",
};
export function addRefToUrl(url) {
return addQueryParams(url, params);
}
export const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
================================================
FILE: src/middleware.ts
================================================
import { NextFetchEvent, NextRequest, NextResponse } from "next/server";
import { ratelimit } from "./db/redis";
export default async function middleware(
request: NextRequest,
event: NextFetchEvent
): Promise<Response | undefined> {
const ip = request.ip ?? "127.0.0.1";
const { success, pending, limit, reset, remaining } =
await ratelimit.limit(ip);
return success
? NextResponse.next()
: NextResponse.redirect(new URL("/blocked", request.url));
}
export const config = {
matcher: "/",
};
================================================
FILE: src/providers.tsx
================================================
"use client";
import React, { useMemo } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
================================================
FILE: src/schema/chat.ts
================================================
export type BeginStream = {
event_type?: StreamEvent;
query: string;
};
export type ChatHistoryResponse = {
snapshots?: Array<ChatSnapshot>;
};
export type ChatMessage = {
content: string;
role: MessageRole;
related_queries?: Array<string> | null;
sources?: Array<SearchResult> | null;
images?: Array<string> | null;
is_error_message?: boolean;
more_results?: Array<{
title: string;
url: string;
screenshot_url: string;
}> | null;
};
export type ChatRequest = {
thread_id?: number | null;
query: string;
history?: Array<Message>;
pro_search?: boolean;
};
export type ChatResponseEvent = {
event: StreamEvent;
data:
| BeginStream
| SearchResultStream
| TextChunkStream
| RelatedQueriesStream
| StreamEndStream
| FinalResponseStream
| ErrorStream
| MoreResultsStream;
};
export type ChatSnapshot = {
id: number;
title: string;
date: string;
preview: string;
model_name: string;
};
export type ErrorStream = {
event_type?: StreamEvent;
detail: string;
};
export type FinalResponseStream = {
event_type?: StreamEvent;
message: string;
};
export type HTTPValidationError = {
detail?: Array<ValidationError>;
};
export type Message = {
content: string;
role: MessageRole;
};
export enum MessageRole {
USER = "user",
ASSISTANT = "assistant",
}
export type RelatedQueriesStream = {
event_type?: StreamEvent;
related_queries?: Array<string>;
};
export type MoreResultsStream = {
event_type?: StreamEvent;
more_results?: Array<any>;
};
export type SearchResult = {
title: string;
url: string;
content: string;
description: string;
};
export type SearchResultStream = {
event_type?: StreamEvent;
results?: Array<SearchResult>;
images?: Array<string>;
};
export type StreamEndStream = {
event_type?: StreamEvent;
thread_id?: number | null;
};
export enum StreamEvent {
BEGIN_STREAM = "begin-stream",
SEARCH_RESULTS = "search-results",
TEXT_CHUNK = "text-chunk",
RELATED_QUERIES = "related-queries",
MORE_RESULTS = "more-results",
STREAM_END = "stream-end",
FINAL_RESPONSE = "final-response",
ERROR = "error",
}
export type TextChunkStream = {
event_type?: StreamEvent;
text: string;
};
export type ThreadResponse = {
thread_id: number;
messages?: Array<ChatMessage>;
};
export type ValidationError = {
loc: Array<string | number>;
msg: string;
type: string;
};
================================================
FILE: src/stores/index.ts
================================================
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { createMessageSlice, ChatStore } from "./slices/messageSlice";
type StoreState = ChatStore;
const useStore = create<StoreState>()(
persist(
(...a) => ({
...createMessageSlice(...a),
}),
{
name: "store",
partialize: (state) => ({
// messages: state.messages, TODO
}),
}
)
);
export const useChatStore = () =>
useStore((state) => ({
messages: state.messages,
addMessage: state.addMessage,
setMessages: state.setMessages,
threadId: state.threadId,
setThreadId: state.setThreadId,
}));
================================================
FILE: src/stores/slices/messageSlice.ts
================================================
import { create, StateCreator } from "zustand";
import { ChatMessage } from "@/schema/chat";
type State = {
threadId: number | null;
messages: ChatMessage[];
};
type Actions = {
addMessage: (message: ChatMessage) => void;
setThreadId: (threadId: number | null) => void;
setMessages: (messages: ChatMessage[]) => void;
};
export type ChatStore = State & Actions;
export const createMessageSlice: StateCreator<ChatStore, [], [], ChatStore> = (
set
) => ({
threadId: null,
messages: [],
addMessage: (message: ChatMessage) =>
set((state) => ({ messages: [...state.messages, message] })),
setThreadId: (threadId: number | null) => set((state) => ({ threadId })),
setMessages: (messages: ChatMessage[]) => set((state) => ({ messages })),
});
================================================
FILE: tailwind.config.ts
================================================
import type { Config } from "tailwindcss";
const {
default: flattenColorPalette,
} = require("tailwindcss/lib/util/flattenColorPalette");
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
typography: (theme: any) => ({
custom: {
css: {
"--tw-prose-body": theme("colors.primary.foreground"),
"--tw-prose-invert-body": theme("colors.primary.DEFAULT"),
},
},
}),
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
tint: {
DEFAULT: "hsl(var(--tint))",
foreground: "hsl(var(--tint-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [
require("tailwindcss-animate"),
require("@tailwindcss/typography"),
addVariablesForColors,
],
} as Config;
function addVariablesForColors({ addBase, theme }: any) {
let allColors = flattenColorPalette(theme("colors"));
let newVars = Object.fromEntries(
Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
);
addBase({
":root": newVars,
});
}
export default config;
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"target": "ES2020",
"strict": false,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
gitextract_sui8u3lf/ ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ ├── chat/ │ │ │ │ └── route.ts │ │ │ └── join-wait/ │ │ │ └── route.ts │ │ ├── blocked/ │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── robots.txt │ │ └── waitlist/ │ │ └── page.tsx │ ├── components/ │ │ ├── ask-input.tsx │ │ ├── assistant-message.tsx │ │ ├── chat-panel.tsx │ │ ├── image-section.tsx │ │ ├── landing/ │ │ │ ├── hero.tsx │ │ │ ├── preview-landing.tsx │ │ │ ├── typing-title.tsx │ │ │ └── wailtlist.tsx │ │ ├── magicui/ │ │ │ └── typing-animation.tsx │ │ ├── markdown.tsx │ │ ├── message.tsx │ │ ├── messages-list.tsx │ │ ├── mode-toggle.tsx │ │ ├── more-results.tsx │ │ ├── nav.tsx │ │ ├── related-questions.tsx │ │ ├── search-results.tsx │ │ ├── section.tsx │ │ ├── shared/ │ │ │ ├── icons.tsx │ │ │ └── max-width-wrapper.tsx │ │ ├── starter-questions.tsx │ │ ├── theme-provider.tsx │ │ ├── ui/ │ │ │ ├── accordion.tsx │ │ │ ├── alert.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── switch.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── timeline.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── typewriter-effect.tsx │ │ │ └── use-toast.ts │ │ └── user-message.tsx │ ├── config/ │ │ └── sites.ts │ ├── db/ │ │ ├── init.sql │ │ ├── redis.ts │ │ └── supabase.ts │ ├── env.mjs │ ├── hooks/ │ │ └── chat.ts │ ├── lib/ │ │ ├── chat/ │ │ │ ├── embedding.ts │ │ │ ├── llm.ts │ │ │ └── prompts.ts │ │ └── utils.ts │ ├── middleware.ts │ ├── providers.tsx │ ├── schema/ │ │ └── chat.ts │ └── stores/ │ ├── index.ts │ └── slices/ │ └── messageSlice.ts ├── tailwind.config.ts └── tsconfig.json
SYMBOL INDEX (94 symbols across 41 files)
FILE: src/app/api/chat/route.ts
function POST (line 12) | async function POST(req: NextRequest) {
FILE: src/app/api/join-wait/route.ts
function postHandler (line 4) | async function postHandler(req: NextRequest) {
constant POST (line 50) | const POST = postHandler;
FILE: src/app/blocked/page.tsx
function Blocked (line 1) | function Blocked() {
FILE: src/app/layout.tsx
function RootLayout (line 52) | function RootLayout({
FILE: src/app/page.tsx
function Home (line 4) | function Home() {
FILE: src/app/waitlist/page.tsx
function Page (line 3) | function Page() {
FILE: src/components/assistant-message.tsx
function ErrorMessage (line 10) | function ErrorMessage({ content }: { content: string }) {
FILE: src/components/image-section.tsx
function ImagePreload (line 19) | function ImagePreload({ images }: { images: string[] }) {
FILE: src/components/landing/hero.tsx
function HeroLanding (line 8) | async function HeroLanding() {
FILE: src/components/landing/preview-landing.tsx
function PreviewLanding (line 5) | function PreviewLanding() {
FILE: src/components/landing/typing-title.tsx
function TypingTitle (line 30) | function TypingTitle() {
FILE: src/components/landing/wailtlist.tsx
function WailtList (line 12) | function WailtList() {
FILE: src/components/magicui/typing-animation.tsx
type TypingAnimationProps (line 7) | interface TypingAnimationProps {
function TypingAnimation (line 13) | function TypingAnimation({
FILE: src/components/message.tsx
function chunkString (line 10) | function chunkString(str: string): string[] {
type MessageProps (line 16) | interface MessageProps {
FILE: src/components/mode-toggle.tsx
function ModeToggle (line 15) | function ModeToggle() {
FILE: src/components/more-results.tsx
function MoreResults (line 9) | function MoreResults({
FILE: src/components/nav.tsx
function Navbar (line 31) | function Navbar() {
FILE: src/components/related-questions.tsx
function RelatedQuestions (line 3) | function RelatedQuestions({
FILE: src/components/search-results.tsx
function SearchResults (line 41) | function SearchResults({ results }: { results: SearchResult[] }) {
FILE: src/components/shared/icons.tsx
type Icon (line 30) | type Icon = LucideIcon;
FILE: src/components/shared/max-width-wrapper.tsx
function MaxWidthWrapper (line 5) | function MaxWidthWrapper({
FILE: src/components/theme-provider.tsx
function ThemeProvider (line 7) | function ThemeProvider({ children, ...props }: ThemeProviderProps) {
FILE: src/components/ui/badge.tsx
type BadgeProps (line 26) | interface BadgeProps
function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: src/components/ui/button.tsx
type ButtonProps (line 45) | interface ButtonProps
FILE: src/components/ui/input.tsx
type InputProps (line 5) | interface InputProps
FILE: src/components/ui/skeleton.tsx
function Skeleton (line 3) | function Skeleton({
FILE: src/components/ui/textarea.tsx
type TextareaProps (line 5) | interface TextareaProps
FILE: src/components/ui/timeline.tsx
type TimelineProps (line 21) | interface TimelineProps
type TimelineItemProps (line 52) | interface TimelineItemProps
type TimelineDotProps (line 88) | interface TimelineDotProps
type TimelineConentProps (line 126) | interface TimelineConentProps
type TimelineHeadingProps (line 161) | interface TimelineHeadingProps
type TimelineLineProps (line 179) | interface TimelineLineProps extends React.HTMLAttributes<HTMLHRElement> {
FILE: src/components/ui/toast.tsx
type ToastProps (line 115) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement (line 117) | type ToastActionElement = React.ReactElement<typeof ToastAction>;
FILE: src/components/ui/toaster.tsx
function Toaster (line 13) | function Toaster() {
FILE: src/components/ui/use-toast.ts
constant TOAST_LIMIT (line 8) | const TOAST_LIMIT = 1;
constant TOAST_REMOVE_DELAY (line 9) | const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast (line 11) | type ToasterToast = ToastProps & {
function genId (line 27) | function genId() {
type ActionType (line 32) | type ActionType = typeof actionTypes;
type Action (line 34) | type Action =
type State (line 52) | interface State {
function dispatch (line 133) | function dispatch(action: Action) {
type Toast (line 140) | type Toast = Omit<ToasterToast, "id">;
function toast (line 142) | function toast({ ...props }: Toast) {
function useToast (line 171) | function useToast() {
FILE: src/db/init.sql
type aitools (line 5) | CREATE TABLE aitools (
type aitools_chunk (line 25) | CREATE TABLE aitools_chunk (
type aitools_chunk (line 36) | create index on aitools_chunk using hnsw (embedding vector_l2_ops)
function match_embeddings (line 39) | create or replace function match_embeddings (
FILE: src/lib/chat/embedding.ts
function generateDocEmbedding (line 9) | async function generateDocEmbedding(contents: string[]) {
function generateQueyEmbedding (line 13) | async function generateQueyEmbedding(query: string) {
FILE: src/lib/chat/llm.ts
function documentToStr (line 16) | function documentToStr(doc) {
function formatContext (line 21) | function formatContext(searchResults) {
function genLLMTextChunk (line 30) | async function genLLMTextChunk({ query, contexts }) {
function genRelatedQuery (line 40) | async function genRelatedQuery({ query, contexts }) {
function translate (line 56) | async function translate({ query }) {
FILE: src/lib/utils.ts
function cn (line 5) | function cn(...inputs: ClassValue[]) {
function nFormatter (line 9) | function nFormatter(num: number, digits?: number) {
function containsChinese (line 37) | function containsChinese(str: string) {
function addQueryParams (line 42) | function addQueryParams(url, params) {
function addRefToUrl (line 55) | function addRefToUrl(url) {
FILE: src/middleware.ts
function middleware (line 4) | async function middleware(
FILE: src/providers.tsx
function Providers (line 6) | function Providers({ children }: { children: React.ReactNode }) {
FILE: src/schema/chat.ts
type BeginStream (line 1) | type BeginStream = {
type ChatHistoryResponse (line 6) | type ChatHistoryResponse = {
type ChatMessage (line 10) | type ChatMessage = {
type ChatRequest (line 24) | type ChatRequest = {
type ChatResponseEvent (line 31) | type ChatResponseEvent = {
type ChatSnapshot (line 44) | type ChatSnapshot = {
type ErrorStream (line 52) | type ErrorStream = {
type FinalResponseStream (line 57) | type FinalResponseStream = {
type HTTPValidationError (line 62) | type HTTPValidationError = {
type Message (line 66) | type Message = {
type MessageRole (line 71) | enum MessageRole {
type RelatedQueriesStream (line 76) | type RelatedQueriesStream = {
type MoreResultsStream (line 81) | type MoreResultsStream = {
type SearchResult (line 86) | type SearchResult = {
type SearchResultStream (line 93) | type SearchResultStream = {
type StreamEndStream (line 99) | type StreamEndStream = {
type StreamEvent (line 104) | enum StreamEvent {
type TextChunkStream (line 115) | type TextChunkStream = {
type ThreadResponse (line 120) | type ThreadResponse = {
type ValidationError (line 125) | type ValidationError = {
FILE: src/stores/index.ts
type StoreState (line 5) | type StoreState = ChatStore;
FILE: src/stores/slices/messageSlice.ts
type State (line 4) | type State = {
type Actions (line 9) | type Actions = {
type ChatStore (line 15) | type ChatStore = State & Actions;
FILE: tailwind.config.ts
function addVariablesForColors (line 99) | function addVariablesForColors({ addBase, theme }: any) {
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (159K chars).
[
{
"path": ".eslintrc.json",
"chars": 40,
"preview": "{\n \"extends\": \"next/core-web-vitals\"\n}\n"
},
{
"path": ".gitignore",
"chars": 429,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "LICENSE",
"chars": 10933,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 3629,
"preview": "# DiscovAI\n\nAn AI-powered search engine for AI tools, or your own data.\n\nhttps://github.com/user-attachments/assets/2cdc"
},
{
"path": "components.json",
"chars": 414,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {"
},
{
"path": "next.config.mjs",
"chars": 330,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n images: {\n remotePatterns: [\n {\n protocol"
},
{
"path": "package.json",
"chars": 2031,
"preview": "{\n \"name\": \"discovai-search\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build"
},
{
"path": "postcss.config.mjs",
"chars": 135,
"preview": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n plugins: {\n tailwindcss: {},\n },\n};\n\nexport d"
},
{
"path": "src/app/api/chat/route.ts",
"chars": 6137,
"preview": "// app/api/stream/route.ts\n\nimport { embeddingVectorCacheKey, llmResultCacheKey, redis } from \"@/db/redis\";\nimport { sup"
},
{
"path": "src/app/api/join-wait/route.ts",
"chars": 1223,
"preview": "import { NextRequest } from \"next/server\";\nimport { supabase } from \"@/db/supabase\";\n\nasync function postHandler(req: Ne"
},
{
"path": "src/app/blocked/page.tsx",
"chars": 135,
"preview": "export default function Blocked() {\n return (\n <div>\n <main>\n <h3>Access blocked.</h3>\n </main>\n "
},
{
"path": "src/app/globals.css",
"chars": 1658,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 60 30% 98%;\n --"
},
{
"path": "src/app/layout.tsx",
"chars": 1953,
"preview": "import { Navbar } from \"@/components/nav\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { LinkCon"
},
{
"path": "src/app/page.tsx",
"chars": 364,
"preview": "import { ChatPanel } from \"@/components/chat-panel\";\nimport { Suspense } from \"react\";\n\nexport default function Home() {"
},
{
"path": "src/app/robots.txt",
"chars": 38,
"preview": "User-Agent: *\nAllow: /\nDisallow: /auth"
},
{
"path": "src/app/waitlist/page.tsx",
"chars": 233,
"preview": "import HeroLanding from \"@/components/landing/hero\";\nimport PreviewLanding from \"@/components/landing/preview-landing\";\n"
},
{
"path": "src/components/ask-input.tsx",
"chars": 3043,
"preview": "import TextareaAutosize from \"react-textarea-autosize\";\nimport { useState } from \"react\";\nimport { Button } from \"./ui/b"
},
{
"path": "src/components/assistant-message.tsx",
"chars": 1949,
"preview": "import { MessageComponent, MessageComponentSkeleton } from \"./message\";\nimport { SearchResultsSkeleton, SearchResults } "
},
{
"path": "src/components/chat-panel.tsx",
"chars": 3738,
"preview": "\"use client\";\nimport { useChat } from \"@/hooks/chat\";\nimport { useChatStore } from \"@/stores\";\nimport { useSearchParams "
},
{
"path": "src/components/image-section.tsx",
"chars": 839,
"preview": "/* eslint-disable @next/next/no-img-element */\n\"use client\";\n\nimport { Skeleton } from \"./ui/skeleton\";\nexport const Ima"
},
{
"path": "src/components/landing/hero.tsx",
"chars": 1443,
"preview": "import { Icons } from \"@/components/shared/icons\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { cn "
},
{
"path": "src/components/landing/preview-landing.tsx",
"chars": 754,
"preview": "import Image from \"next/image\";\n\nimport MaxWidthWrapper from \"@/components/shared/max-width-wrapper\";\n\nexport default fu"
},
{
"path": "src/components/landing/typing-title.tsx",
"chars": 1240,
"preview": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport {\n TypewriterEffect,\n TypewriterEffectSmooth,\n} fro"
},
{
"path": "src/components/landing/wailtlist.tsx",
"chars": 3180,
"preview": "\"use client\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { cn, nFormatter } from \"@/lib/utils\";\nimport { But"
},
{
"path": "src/components/magicui/typing-animation.tsx",
"chars": 1184,
"preview": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface TypingAnimatio"
},
{
"path": "src/components/markdown.tsx",
"chars": 294,
"preview": "import { FC, memo } from \"react\";\nimport ReactMarkdown, { Options } from \"react-markdown\";\n\nexport const MemoizedReactMa"
},
{
"path": "src/components/message.tsx",
"chars": 4649,
"preview": "import React, { FC, memo, useEffect, useMemo, useState } from \"react\";\nimport { MemoizedReactMarkdown } from \"./markdown"
},
{
"path": "src/components/messages-list.tsx",
"chars": 1320,
"preview": "import { AssistantMessageContent } from \"./assistant-message\";\nimport { Separator } from \"./ui/separator\";\nimport { User"
},
{
"path": "src/components/mode-toggle.tsx",
"chars": 1407,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { ComputerIcon, Moon, Sun } from \"lucide-react\";\nimport { useTheme"
},
{
"path": "src/components/more-results.tsx",
"chars": 1070,
"preview": "import { PlusIcon } from \"lucide-react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n"
},
{
"path": "src/components/nav.tsx",
"chars": 2040,
"preview": "\"use client\";\n\nimport Link from \"next/link\";\nimport { ModeToggle } from \"./mode-toggle\";\nimport { useTheme } from \"next-"
},
{
"path": "src/components/related-questions.tsx",
"chars": 627,
"preview": "import { PlusIcon } from \"lucide-react\";\n\nexport default function RelatedQuestions({\n questions,\n onSelect,\n}: {\n que"
},
{
"path": "src/components/search-results.tsx",
"chars": 4794,
"preview": "/* eslint-disable @next/next/no-img-element */\n\"use client\";\nimport { useState } from \"react\";\nimport { Card, CardConten"
},
{
"path": "src/components/section.tsx",
"chars": 1375,
"preview": "import { cn } from \"@/lib/utils\";\nimport {\n BookOpen,\n BookOpenIcon,\n CameraIcon,\n ListOrderedIcon,\n ListPlusIcon,\n"
},
{
"path": "src/components/shared/icons.tsx",
"chars": 4276,
"preview": "import {\n AlertTriangle,\n ArrowRight,\n ArrowUpRight,\n Check,\n ChevronLeft,\n ChevronRight,\n Copy,\n CreditCard,\n "
},
{
"path": "src/components/shared/max-width-wrapper.tsx",
"chars": 410,
"preview": "import { ReactNode } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport default function MaxWidthWrapper({\n class"
},
{
"path": "src/components/starter-questions.tsx",
"chars": 1028,
"preview": "import { ArrowRight, ArrowUpRight } from \"lucide-react\";\n\nconst starterQuestions = [\n \"I want to make old photos high d"
},
{
"path": "src/components/theme-provider.tsx",
"chars": 332,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport"
},
{
"path": "src/components/ui/accordion.tsx",
"chars": 2036,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport {"
},
{
"path": "src/components/ui/alert.tsx",
"chars": 1610,
"preview": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \""
},
{
"path": "src/components/ui/avatar.tsx",
"chars": 1419,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } fr"
},
{
"path": "src/components/ui/badge.tsx",
"chars": 1147,
"preview": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \""
},
{
"path": "src/components/ui/button.tsx",
"chars": 2042,
"preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"cla"
},
{
"path": "src/components/ui/card.tsx",
"chars": 1876,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Card = React.forwardRef<\n HTMLDivElement,\n R"
},
{
"path": "src/components/ui/dropdown-menu.tsx",
"chars": 7407,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\ni"
},
{
"path": "src/components/ui/hover-card.tsx",
"chars": 1486,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\";\n\nimport"
},
{
"path": "src/components/ui/input.tsx",
"chars": 809,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface InputProps\n extends React.InputHTM"
},
{
"path": "src/components/ui/label.tsx",
"chars": 734,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, ty"
},
{
"path": "src/components/ui/select.tsx",
"chars": 5680,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n CaretSortIcon,\n CheckIcon,\n ChevronDownIcon,\n ChevronUpIcon"
},
{
"path": "src/components/ui/separator.tsx",
"chars": 780,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport "
},
{
"path": "src/components/ui/skeleton.tsx",
"chars": 269,
"preview": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) "
},
{
"path": "src/components/ui/switch.tsx",
"chars": 1143,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn "
},
{
"path": "src/components/ui/tabs.tsx",
"chars": 1906,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } fro"
},
{
"path": "src/components/ui/textarea.tsx",
"chars": 740,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface TextareaProps\n extends React.Texta"
},
{
"path": "src/components/ui/timeline.tsx",
"chars": 5688,
"preview": "//github.com/shadcn-ui/ui/pull/3374 + cursor for modifications :)\nimport React from \"react\";\nimport { CheckIcon, CircleI"
},
{
"path": "src/components/ui/toast.tsx",
"chars": 4887,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { Cross2Icon } from \"@radix-ui/react-icons\";\nimport * as ToastPrim"
},
{
"path": "src/components/ui/toaster.tsx",
"chars": 800,
"preview": "\"use client\";\n\nimport {\n Toast,\n ToastClose,\n ToastDescription,\n ToastProvider,\n ToastTitle,\n ToastViewport,\n} fro"
},
{
"path": "src/components/ui/toggle.tsx",
"chars": 1415,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { cva, "
},
{
"path": "src/components/ui/tooltip.tsx",
"chars": 1153,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn"
},
{
"path": "src/components/ui/typewriter-effect.tsx",
"chars": 4170,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, stagger, useAnimate, useInView } from \"framer-motion\";"
},
{
"path": "src/components/ui/use-toast.ts",
"chars": 4007,
"preview": "\"use client\";\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\";\n\nimport type { ToastActionElement,"
},
{
"path": "src/components/user-message.tsx",
"chars": 240,
"preview": "import { ChatMessage } from \"@/schema/chat\";\n\nexport const UserMessageContent = ({ message }: { message: ChatMessage }) "
},
{
"path": "src/config/sites.ts",
"chars": 548,
"preview": "export const SiteConfig = {\n name: \"DiscovAI\",\n title: \"DiscovAI\",\n metaTitle: \"DiscovAI - Discover top ai tools best"
},
{
"path": "src/db/init.sql",
"chars": 1702,
"preview": "CREATE EXTENSION IF NOT EXISTS \"vector\";\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\n\n-- content table\nCREATE TABLE aito"
},
{
"path": "src/db/redis.ts",
"chars": 584,
"preview": "// lib/redis.ts\nimport { Redis } from \"@upstash/redis\";\nimport { Ratelimit } from \"@upstash/ratelimit\";\nimport { env } f"
},
{
"path": "src/db/supabase.ts",
"chars": 182,
"preview": "import { createClient } from \"@supabase/supabase-js\";\n\nexport const supabase = createClient(\n process.env.NEXT_PUBLIC_S"
},
{
"path": "src/env.mjs",
"chars": 888,
"preview": "/* eslint-disable no-process-env */\n\nimport { createEnv } from \"@t3-oss/env-nextjs\";\nimport z from \"zod\";\n\nexport const "
},
{
"path": "src/hooks/chat.ts",
"chars": 5002,
"preview": "import { useMutation } from \"@tanstack/react-query\";\nimport {\n ChatMessage,\n ChatRequest,\n ChatResponseEvent,\n Error"
},
{
"path": "src/lib/chat/embedding.ts",
"chars": 524,
"preview": "import { JinaEmbeddings } from \"@langchain/community/embeddings/jina\";\nimport { env } from \"@/env.mjs\";\n\nconst embedding"
},
{
"path": "src/lib/chat/llm.ts",
"chars": 1910,
"preview": "import { CHAT_PROMPT, RELATED_QUESTION_PROMPT, TRANSLATE } from \"./prompts\";\n// import { OpenAI } from \"@langchain/opena"
},
{
"path": "src/lib/chat/prompts.ts",
"chars": 2195,
"preview": "export const CHAT_PROMPT = (contexts: string, query: string) => `\\\nAs a professional AI tool search expert. Please recom"
},
{
"path": "src/lib/utils.ts",
"chars": 1547,
"preview": "import { ChatResponseEvent } from \"@/schema/chat\";\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from"
},
{
"path": "src/middleware.ts",
"chars": 518,
"preview": "import { NextFetchEvent, NextRequest, NextResponse } from \"next/server\";\nimport { ratelimit } from \"./db/redis\";\n\nexport"
},
{
"path": "src/providers.tsx",
"chars": 374,
"preview": "\"use client\";\n\nimport React, { useMemo } from \"react\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react"
},
{
"path": "src/schema/chat.ts",
"chars": 2430,
"preview": "export type BeginStream = {\n event_type?: StreamEvent;\n query: string;\n};\n\nexport type ChatHistoryResponse = {\n snaps"
},
{
"path": "src/stores/index.ts",
"chars": 649,
"preview": "import { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { createMessageSlice, ChatStore }"
},
{
"path": "src/stores/slices/messageSlice.ts",
"chars": 766,
"preview": "import { create, StateCreator } from \"zustand\";\nimport { ChatMessage } from \"@/schema/chat\";\n\ntype State = {\n threadId:"
},
{
"path": "tailwind.config.ts",
"chars": 2981,
"preview": "import type { Config } from \"tailwindcss\";\nconst {\n default: flattenColorPalette,\n} = require(\"tailwindcss/lib/util/fla"
},
{
"path": "tsconfig.json",
"chars": 603,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n "
}
]
About this extraction
This page contains the full source code of the DiscovAI/DiscovAI-search GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (144.1 KB), approximately 39.7k tokens, and a symbol index with 94 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.