├── .env.example
├── .eslintrc.json
├── .github
└── funding.yaml
├── .gitignore
├── .husky
└── pre-commit
├── README.md
├── actions
├── chats.ts
├── messages.ts
├── not-diamond
│ ├── create-preference-id.ts
│ ├── report-regeneration.ts
│ ├── select-not-diamond-model.ts
│ ├── submit-arena-choice.ts
│ └── submit-feedback.ts
└── stream-message.tsx
├── app
├── [chatid]
│ └── page.tsx
├── global-error.tsx
├── globals.css
├── layout.tsx
└── page.tsx
├── components.json
├── components
├── arena-mode-indicator.tsx
├── chats-bar
│ └── chats-bar.tsx
├── chats
│ ├── chat-list-item.tsx
│ ├── chat.tsx
│ ├── chats-list.tsx
│ └── create-chat-button.tsx
├── dashboard.tsx
├── messages
│ ├── assistant-message.tsx
│ ├── copy-message.tsx
│ ├── message-action-button.tsx
│ ├── message-feedback.tsx
│ ├── message-markdown-memoized.tsx
│ ├── message-markdown.tsx
│ ├── message-preference.tsx
│ ├── message-stats.tsx
│ ├── messsage-codeblock.tsx
│ ├── regenerate-message-from-model.tsx
│ ├── regenerate-message.tsx
│ └── user-message.tsx
├── onboarding-message.tsx
├── router-progress-bar.tsx
├── settings-bar
│ └── settings-bar.tsx
├── ui
│ ├── accordion.tsx
│ ├── alert-dialog.tsx
│ ├── alert.tsx
│ ├── aspect-ratio.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── breadcrumb.tsx
│ ├── button.tsx
│ ├── calendar.tsx
│ ├── card.tsx
│ ├── carousel.tsx
│ ├── checkbox.tsx
│ ├── collapsible.tsx
│ ├── command.tsx
│ ├── context-menu.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.tsx
│ ├── form.tsx
│ ├── hover-card.tsx
│ ├── input-otp.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── menubar.tsx
│ ├── navigation-menu.tsx
│ ├── pagination.tsx
│ ├── popover.tsx
│ ├── progress.tsx
│ ├── radio-group.tsx
│ ├── resizable.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── skeleton.tsx
│ ├── slider.tsx
│ ├── sonner.tsx
│ ├── switch.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ ├── toggle-group.tsx
│ ├── toggle.tsx
│ ├── tooltip.tsx
│ └── use-toast.ts
└── utility
│ ├── providers.tsx
│ └── wait-for-hydration.tsx
├── db
├── db.ts
├── migrations
│ ├── 0000_lying_violations.sql
│ └── meta
│ │ ├── 0000_snapshot.json
│ │ └── _journal.json
├── queries
│ ├── chats.ts
│ ├── messages.ts
│ └── profiles.ts
└── schema
│ ├── chats.ts
│ ├── index.ts
│ ├── messages.ts
│ └── profiles.ts
├── drizzle.config.ts
├── lib
├── context
│ └── app-context.tsx
├── hooks
│ └── use-copy-to-clipboard.tsx
├── not-diamond
│ ├── not-diamond-config.ts
│ └── select-random-model.ts
├── utils.ts
└── utils
│ ├── get-cost.ts
│ ├── handle-fetch.ts
│ └── local-storage.ts
├── license
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prettier.config.cjs
├── public
└── readme.png
├── tailwind.config.ts
├── tsconfig.json
└── types
└── chat-data.ts
/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=
2 | NOT_DIAMOND_API_KEY=
3 | OPENAI_API_KEY=
4 | ANTHROPIC_API_KEY=
5 | GOOGLE_GENERATIVE_AI_API_KEY=
6 | GROQ_API_KEY=
7 | PERPLEXITY_API_KEY=
8 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss"],
10 | "rules": {
11 | "@next/next/no-img-element": "off",
12 | "jsx-a11y/alt-text": "off",
13 | "react-hooks/exhaustive-deps": "off",
14 | "tailwindcss/enforces-negative-arbitrary-values": "off",
15 | "tailwindcss/no-contradicting-classname": "off",
16 | "tailwindcss/no-custom-classname": "off",
17 | "tailwindcss/no-unnecessary-arbitrary-value": "off"
18 | },
19 | "settings": {
20 | "tailwindcss": {
21 | "callees": ["cn", "cva"],
22 | "config": "tailwind.config.js"
23 | }
24 | },
25 | "overrides": [
26 | {
27 | "files": ["*.ts", "*.tsx"],
28 | "parser": "@typescript-eslint/parser"
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.github/funding.yaml:
--------------------------------------------------------------------------------
1 | # If you find my open-source work helpful, please consider sponsoring me!
2 |
3 | github: mckaywrigley
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | . "$(dirname -- "$0")/_/husky.sh"
4 |
5 | npm run lint:fix && npm run format:write && git add .
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI Router Chat
2 |
3 | An AI chat app that utilizes advanced LLM model routing powered by [Not Diamond](https://notdiamond.readme.io/).
4 |
5 | Includes an "Arena mode" where you can dynamically improve your model router.
6 |
7 | 
8 |
9 | ## Demo
10 |
11 | See the latest demo [here](https://x.com/mckaywrigley/status/1818717706037977182).
12 |
13 | ## Sponsor
14 |
15 | If you find AI Router Chat useful, please consider [sponsoring](https://github.com/sponsors/mckaywrigley) us to support my open-source work :)
16 |
17 | ## Requirements
18 |
19 | Get a Not Diamond API key from [Not Diamond](https://app.notdiamond.ai/keys).
20 |
21 | You'll also need API keys from each LLM provider (OpenAI, Anthropic, Groq, Perplexity, Google) that you'd like to use.
22 |
23 | ## Running Locally
24 |
25 | **1. Clone Repo**
26 |
27 | ```bash
28 | git clone https://github.com/mckaywrigley/ai-router-chat.git
29 | ```
30 |
31 | **2. Provide API Keys**
32 |
33 | Create a .env file in the root of the repo with these values:
34 |
35 | ```bash
36 | cp .env.example .env
37 | ```
38 |
39 | ```bash
40 | DATABASE_URL=
41 | NOT_DIAMOND_API_KEY=
42 | OPENAI_API_KEY=
43 | ANTHROPIC_API_KEY=
44 | GOOGLE_GENERATIVE_AI_API_KEY=
45 | GROQ_API_KEY=
46 | PERPLEXITY_API_KEY=
47 | ```
48 |
49 | **3. Install Dependencies**
50 |
51 | ```bash
52 | npm i
53 | ```
54 |
55 | **4. Setup DB**
56 |
57 | [Drizzle w/ Supabase Example](https://orm.drizzle.team/learn/tutorials/drizzle-with-supabase)
58 |
59 | Set up a new project on [Supabase](https://supabase.com/).
60 |
61 | Copy the connection string (DB URL) from the Supabase output, and paste it into the above .env file.
62 |
63 | Once you have the DB URL, run the following command to create the database and tables:
64 |
65 | ```bash
66 | npm run migrate
67 | ```
68 |
69 | **5. Run App**
70 |
71 | ```bash
72 | npm run dev
73 | ```
74 |
75 | ## Deploying
76 |
77 | Deploy to Vercel in 1 click:
78 |
79 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmckaywrigley%2Fai-router-chat.git&env=DATABASE_URL,NOT_DIAMOND_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEY,GOOGLE_GENERATIVE_AI_API_KEY,GROQ_API_KEY,PERPLEXITY_API_KEY)
80 |
--------------------------------------------------------------------------------
/actions/chats.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { createChat, deleteChat } from "@/db/queries/chats"
4 | import { InsertChat } from "@/db/schema/chats"
5 | import { revalidatePath } from "next/cache"
6 | import { redirect } from "next/navigation"
7 |
8 | export const createChatAction = async (chatData: InsertChat) => {
9 | const chat = await createChat(chatData)
10 | revalidatePath("/")
11 | return chat
12 | }
13 |
14 | export const deleteChatAction = async (
15 | chatId: string,
16 | deletingCurrent: boolean
17 | ) => {
18 | await deleteChat(chatId)
19 | revalidatePath("/")
20 | if (deletingCurrent) {
21 | return redirect("/")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/actions/messages.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import {
4 | createMessage,
5 | deleteMessagesIncludingAndAfterTurn,
6 | deleteRegeneratedMessages,
7 | getMessagesByChatIdAndTurn,
8 | updateMessage,
9 | updateTurnPreferences
10 | } from "@/db/queries/messages"
11 | import { InsertMessage, SelectMessage } from "@/db/schema/messages"
12 | import { revalidatePath } from "next/cache"
13 | import { submitFeedback } from "./not-diamond/submit-feedback"
14 |
15 | export const createMessageAction = async (data: InsertMessage) => {
16 | const newMessage = await createMessage(data)
17 | revalidatePath("/")
18 | return newMessage
19 | }
20 |
21 | export const updateMessageAction = async (
22 | messageId: string,
23 | updateData: Partial
24 | ) => {
25 | const updatedMessage = await updateMessage(messageId, updateData)
26 | revalidatePath("/")
27 | return updatedMessage
28 | }
29 |
30 | export const updateTurnPreferenceAction = async (
31 | chatId: string,
32 | messageId: string,
33 | turn: number
34 | ) => {
35 | const messages = await getMessagesByChatIdAndTurn(chatId, turn)
36 | await updateTurnPreferences(
37 | chatId,
38 | turn,
39 | messages.map(msg => ({ id: msg.id, isPreferred: msg.id === messageId }))
40 | )
41 | revalidatePath("/")
42 | }
43 |
44 | export const deleteMessagesIncludingAndAfterTurnAction = async (
45 | chatId: string,
46 | turn: number
47 | ) => {
48 | await deleteMessagesIncludingAndAfterTurn(chatId, turn)
49 | revalidatePath("/")
50 | }
51 |
52 | export const deleteRegeneratedMessagesAction = async (
53 | chatId: string,
54 | turn: number
55 | ) => {
56 | await deleteRegeneratedMessages(chatId, turn)
57 | revalidatePath("/")
58 | }
59 |
60 | export const handleBothResponsesAreBad = async ({
61 | messages,
62 | sessionId
63 | }: {
64 | messages: SelectMessage[]
65 | sessionId: string | null
66 | }) => {
67 | const promises = messages.map(async message => {
68 | if (sessionId) {
69 | await submitFeedback(sessionId, -1, {
70 | provider: message.provider,
71 | model: message.model
72 | })
73 | }
74 |
75 | await updateMessageAction(message.id, {
76 | thumbsUp: false
77 | })
78 |
79 | await updateTurnPreferences(
80 | message.chatId,
81 | message.turn,
82 | messages.map(msg => ({ id: msg.id, isPreferred: false }))
83 | )
84 | })
85 |
86 | await Promise.all(promises)
87 | }
88 |
89 | export const handleBothResponsesAreGood = async ({
90 | messages
91 | }: {
92 | messages: SelectMessage[]
93 | }) => {
94 | const promises = messages.map(async message => {
95 | await updateMessageAction(message.id, {
96 | thumbsUp: false
97 | })
98 |
99 | await updateTurnPreferences(
100 | message.chatId,
101 | message.turn,
102 | messages.map(msg => ({ id: msg.id, isPreferred: true }))
103 | )
104 | })
105 |
106 | await Promise.all(promises)
107 | }
108 |
--------------------------------------------------------------------------------
/actions/not-diamond/create-preference-id.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { ndBaseUrl, ndHeaders } from "../../lib/not-diamond/not-diamond-config"
4 | import { handleFetch } from "../../lib/utils/handle-fetch"
5 |
6 | export const createPreferenceId = async (name: string) => {
7 | const json = await handleFetch(
8 | `${ndBaseUrl}/v2/chat/preferences/preferenceCreate`,
9 | {
10 | method: "POST",
11 | headers: ndHeaders,
12 | body: JSON.stringify({
13 | name
14 | })
15 | }
16 | )
17 |
18 | return json.preference_id
19 | }
20 |
--------------------------------------------------------------------------------
/actions/not-diamond/report-regeneration.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { ndBaseUrl, ndHeaders } from "../../lib/not-diamond/not-diamond-config"
4 | import { NDLLMProvider } from "../../lib/not-diamond/select-random-model"
5 | import { handleFetch } from "../../lib/utils/handle-fetch"
6 |
7 | export const reportRegeneration = async (
8 | session_id: string,
9 | provider: NDLLMProvider
10 | ) => {
11 | await handleFetch(`${ndBaseUrl}/v2/chat/report/regenerated`, {
12 | method: "POST",
13 | headers: ndHeaders,
14 | body: JSON.stringify({
15 | session_id,
16 | provider,
17 | regenerated: true
18 | })
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/actions/not-diamond/select-not-diamond-model.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import console from "console"
4 | import {
5 | fallbackModels,
6 | ndBaseUrl,
7 | ndHeaders,
8 | ndLLMProviders
9 | } from "../../lib/not-diamond/not-diamond-config"
10 | import { NDLLMProvider } from "../../lib/not-diamond/select-random-model"
11 | import { handleFetch } from "../../lib/utils/handle-fetch"
12 |
13 | export async function selectNdModel(
14 | preferenceId: string,
15 | messages: { role: string; content: string }[],
16 | activeModels: NDLLMProvider[],
17 | previousSession: string | null
18 | ) {
19 | try {
20 | const response = await handleFetch(`${ndBaseUrl}/v2/chat/modelSelect`, {
21 | method: "POST",
22 | headers: ndHeaders,
23 | body: JSON.stringify({
24 | messages,
25 | llm_providers: activeModels.filter(model => model.provider !== ""),
26 | preference_id: preferenceId,
27 | previous_session: previousSession
28 | })
29 | })
30 |
31 | const {
32 | session_id,
33 | provider: { model }
34 | } = response
35 |
36 | const selectedModel = ndLLMProviders.find(
37 | item => item.ndModelId === model
38 | )?.providerModelId
39 | if (!selectedModel) {
40 | throw new Error("No model selected")
41 | }
42 |
43 | return { session_id, model: selectedModel }
44 | } catch (error: any) {
45 | console.error(`Error selecting model: ${error.message}`)
46 |
47 | // Fallback mechanism
48 | for (const fallbackModel of fallbackModels) {
49 | const fallbackProvider = ndLLMProviders.find(
50 | provider => provider.providerModelId === fallbackModel
51 | )
52 | if (
53 | fallbackProvider &&
54 | activeModels.some(item => item.model === fallbackModel)
55 | ) {
56 | return { session_id: null, model: fallbackModel }
57 | }
58 | }
59 |
60 | return { session_id: null, model: null }
61 | }
62 | }
63 |
64 | export async function selectNdArenaModels(
65 | preferenceId: string,
66 | messages: { role: string; content: string }[],
67 | activeModels: NDLLMProvider[]
68 | ) {
69 | try {
70 | const response = await handleFetch(
71 | `${ndBaseUrl}/v2/chat/arena/arenaModels`,
72 | {
73 | method: "POST",
74 | headers: ndHeaders,
75 | body: JSON.stringify({
76 | messages,
77 | llm_providers: activeModels.filter(model => model.provider !== ""),
78 | preference_id: preferenceId
79 | })
80 | }
81 | )
82 |
83 | const { session_id, model_1, model_2 } = response
84 |
85 | return {
86 | session_id,
87 | model1: model_1.model,
88 | model2: model_2.model
89 | }
90 | } catch (error: any) {
91 | console.error(`Error selecting arena models: ${error.message}`)
92 |
93 | // Fallback mechanism
94 | let fallbackModel1: string | null = null
95 | let fallbackModel2: string | null = null
96 |
97 | for (const fallbackModel of fallbackModels) {
98 | const fallbackProvider = ndLLMProviders.find(
99 | provider => provider.providerModelId === fallbackModel
100 | )
101 | if (
102 | fallbackProvider &&
103 | activeModels.some(item => item.model === fallbackModel)
104 | ) {
105 | if (!fallbackModel1) {
106 | fallbackModel1 = fallbackModel
107 | } else if (!fallbackModel2) {
108 | fallbackModel2 = fallbackModel
109 | break
110 | }
111 | }
112 | }
113 |
114 | return {
115 | session_id: null,
116 | model_1: fallbackModel1,
117 | model_2: fallbackModel2
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/actions/not-diamond/submit-arena-choice.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { ndBaseUrl, ndHeaders } from "../../lib/not-diamond/not-diamond-config"
4 | import { handleFetch } from "../../lib/utils/handle-fetch"
5 |
6 | export const submitArenaChoice = async (
7 | session_id: string,
8 | preferred: { provider: string; model: string },
9 | rejected: { provider: string; model: string }
10 | ) => {
11 | const payload = {
12 | session_id,
13 | preferred_provider: preferred,
14 | rejected_provider: rejected
15 | }
16 |
17 | await handleFetch(`${ndBaseUrl}/v2/chat/arena/arenaChoice`, {
18 | method: "POST",
19 | headers: ndHeaders,
20 | body: JSON.stringify(payload)
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/actions/not-diamond/submit-feedback.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import {
4 | ndBaseUrl,
5 | ndHeaders,
6 | ndLLMProviders
7 | } from "../../lib/not-diamond/not-diamond-config"
8 | import { NDLLMProvider } from "../../lib/not-diamond/select-random-model"
9 | import { handleFetch } from "../../lib/utils/handle-fetch"
10 |
11 | export const submitFeedback = async (
12 | session_id: string,
13 | thumbs: number,
14 | provider: NDLLMProvider
15 | ) => {
16 | const providerData = ndLLMProviders.find(
17 | currentProvider => currentProvider.providerModelId === provider?.model
18 | )
19 |
20 | await handleFetch(`${ndBaseUrl}/v2/chat/report/thumbsUpDown`, {
21 | method: "POST",
22 | headers: ndHeaders,
23 | body: JSON.stringify({
24 | session_id,
25 | thumbs,
26 | provider: {
27 | model: providerData?.ndModelId,
28 | provider: providerData?.ndProvider
29 | }
30 | })
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/actions/stream-message.tsx:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { StreamMessagePayload } from "@/types/chat-data"
4 | import { CoreMessage, streamText } from "ai"
5 | import { createStreamableValue } from "ai/rsc"
6 | import { ndLLMProviders } from "../lib/not-diamond/not-diamond-config"
7 | import { providers } from "../lib/not-diamond/select-random-model"
8 |
9 | const MAX_RETRIES = ndLLMProviders.length
10 |
11 | export const streamMessage = async (payload: StreamMessagePayload) => {
12 | const stream = createStreamableValue("")
13 |
14 | const handleStreamText = async ({
15 | messages,
16 | temperature,
17 | model,
18 | retryCount = 0
19 | }: StreamMessagePayload & { retryCount?: number }) => {
20 | if (retryCount >= MAX_RETRIES) {
21 | stream.update("\n\nMax retries reached. Unable to complete the request.")
22 | stream.done()
23 | return
24 | }
25 |
26 | const regenerationProvider = ndLLMProviders.find(
27 | item => item.providerModelId === model
28 | )?.cloudProvider
29 | const modelWithProvider =
30 | providers[regenerationProvider as keyof typeof providers](model)
31 |
32 | try {
33 | const result = await streamText({
34 | model: modelWithProvider,
35 | messages: messages as CoreMessage[],
36 | temperature
37 | })
38 |
39 | const { textStream } = result
40 |
41 | for await (const chunk of textStream) {
42 | stream.update(chunk)
43 | }
44 |
45 | stream.done()
46 | } catch (e) {
47 | const models = ndLLMProviders.filter(
48 | item => item.providerModelId !== model
49 | )
50 | const randomModel =
51 | models[Math.floor(Math.random() * models.length)].providerModelId
52 | console.info(
53 | `\n\nEncountered an error. Retrying with a different model (${randomModel})...\n\n`
54 | )
55 |
56 | await handleStreamText({
57 | model: randomModel,
58 | temperature,
59 | messages,
60 | retryCount: retryCount + 1
61 | })
62 | }
63 | }
64 |
65 | handleStreamText(payload)
66 |
67 | return { output: stream.value }
68 | }
69 |
--------------------------------------------------------------------------------
/app/[chatid]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Chat } from "@/components/chats/chat"
2 | import { getChatById } from "@/db/queries/chats"
3 | import { getMessagesByChatId } from "@/db/queries/messages"
4 |
5 | export const dynamic = "force-dynamic"
6 |
7 | export default async function ChatIdPage({
8 | params
9 | }: {
10 | params: { chatid: string }
11 | }) {
12 | const chat = await getChatById(params.chatid)
13 | const messages = await getMessagesByChatId(params.chatid)
14 |
15 | return
16 | }
17 |
--------------------------------------------------------------------------------
/app/global-error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as Sentry from "@sentry/nextjs";
4 | import NextError from "next/error";
5 | import { useEffect } from "react";
6 |
7 | export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
8 | useEffect(() => {
9 | Sentry.captureException(error);
10 | }, [error]);
11 |
12 | return (
13 |
14 |
15 | {/* `NextError` is the default Next.js error page component. Its type
16 | definition requires a `statusCode` prop. However, since the App Router
17 | does not expose status codes for errors, we simply pass 0 to render a
18 | generic error message. */}
19 |
20 |
21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 0 0% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 0 0% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 0 0% 3.9%;
15 |
16 | --primary: 0 0% 9%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 0 0% 96.1%;
20 | --secondary-foreground: 0 0% 9%;
21 |
22 | --muted: 0 0% 96.1%;
23 | --muted-foreground: 0 0% 45.1%;
24 |
25 | --accent: 0 0% 96.1%;
26 | --accent-foreground: 0 0% 9%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 0 0% 89.8%;
32 | --input: 0 0% 89.8%;
33 | --ring: 0 0% 3.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 0 0% 3.9%;
40 | --foreground: 0 0% 98%;
41 |
42 | --card: 0 0% 3.9%;
43 | --card-foreground: 0 0% 98%;
44 |
45 | --popover: 0 0% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 |
48 | --primary: 0 0% 98%;
49 | --primary-foreground: 0 0% 9%;
50 |
51 | --secondary: 0 0% 14.9%;
52 | --secondary-foreground: 0 0% 98%;
53 |
54 | --muted: 0 0% 14.9%;
55 | --muted-foreground: 0 0% 63.9%;
56 |
57 | --accent: 0 0% 14.9%;
58 | --accent-foreground: 0 0% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 0 0% 98%;
62 |
63 | --border: 0 0% 14.9%;
64 | --input: 0 0% 14.9%;
65 | --ring: 0 0% 83.1%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { createPreferenceId } from "@/actions/not-diamond/create-preference-id"
2 | import { Dashboard } from "@/components/dashboard"
3 | import { OnboardingMessage } from "@/components/onboarding-message"
4 | import { Providers } from "@/components/utility/providers"
5 | import { getAllChatsByProfileId } from "@/db/queries/chats"
6 | import { createProfile, getProfile } from "@/db/queries/profiles"
7 | import { ndLLMProviders } from "@/lib/not-diamond/not-diamond-config"
8 | import type { Metadata } from "next"
9 | import { Inter } from "next/font/google"
10 | import "./globals.css"
11 |
12 | const inter = Inter({ subsets: ["latin"] })
13 |
14 | export const metadata: Metadata = {
15 | title: "AI Router Chat",
16 | description:
17 | "A template for building an AI chat app that utilizes advanced LLM model routing powered by Not Diamond."
18 | }
19 |
20 | export const dynamic = "force-dynamic"
21 |
22 | export default async function RootLayout({
23 | children
24 | }: Readonly<{
25 | children: React.ReactNode
26 | }>) {
27 | let profile = await getProfile()
28 |
29 | if (!profile) {
30 | const preferenceId = await createPreferenceId("not-diamond-template")
31 | profile = await createProfile({
32 | preferenceId,
33 | activeModels: [...ndLLMProviders.map(model => model.providerModelId)]
34 | })
35 | }
36 |
37 | const chats = await getAllChatsByProfileId(profile.id)
38 |
39 | return (
40 |
41 |
42 |
43 |
44 | {children}
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Chat } from "@/components/chats/chat"
2 |
3 | export const dynamic = "force-dynamic"
4 |
5 | export default async function Home() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/arena-mode-indicator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AppContext } from "@/lib/context/app-context";
4 | import { Swords } from "lucide-react";
5 | import { FC, useContext } from "react";
6 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
7 |
8 | interface ArenaModeIndicatorProps {}
9 |
10 | export const ArenaModeIndicator: FC = ({}) => {
11 | const { isArenaModeActive } = useContext(AppContext);
12 |
13 | if (isArenaModeActive) {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 | Arena Mode Active
22 |
23 |
24 | );
25 | } else {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 | Arena Mode Inactive
34 |
35 |
36 | );
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/components/chats-bar/chats-bar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SelectChat } from "@/db/schema/chats";
4 | import { cn } from "@/lib/utils";
5 | import { SidebarClose, SidebarOpen } from "lucide-react";
6 | import { HTMLAttributes } from "react";
7 | import { ChatsList } from "../chats/chats-list";
8 | import { CreateChatButton } from "../chats/create-chat-button";
9 | import { Button } from "../ui/button";
10 |
11 | interface ChatsBarProps extends HTMLAttributes {
12 | chats: SelectChat[];
13 | isOpen: boolean;
14 | onOpenChange: (isOpen: boolean) => void;
15 | }
16 |
17 | export const ChatsBar = ({ chats, isOpen, onOpenChange, ...props }: ChatsBarProps) => {
18 | return (
19 | <>
20 |
21 |
22 |
23 |
30 |
31 |
32 |
36 |
37 |
38 | {!isOpen && (
39 |
46 | )}
47 | >
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/components/chats/chat-list-item.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { deleteChatAction } from "@/actions/chats"
4 | import { SelectChat } from "@/db/schema/chats"
5 | import { cn } from "@/lib/utils"
6 | import { Ellipsis, Trash } from "lucide-react"
7 | import Link from "next/link"
8 | import { usePathname } from "next/navigation"
9 | import { FC, HTMLAttributes, useState } from "react"
10 | import { Button } from "../ui/button"
11 | import {
12 | DropdownMenu,
13 | DropdownMenuContent,
14 | DropdownMenuTrigger
15 | } from "../ui/dropdown-menu"
16 |
17 | interface ChatListItemProps extends HTMLAttributes {
18 | chat: SelectChat
19 | }
20 | export const ChatListItem: FC = ({ chat, ...props }) => {
21 | const pathname = usePathname()
22 |
23 | const [isHovered, setIsHovered] = useState(false)
24 | const [isSelected, setIsSelected] = useState(false)
25 |
26 | const handleDeleteChat = async () => {
27 | await deleteChatAction(chat.id, pathname === `/${chat.id}`)
28 | }
29 |
30 | const isCurrentPath = pathname === `/${chat.id}`
31 |
32 | return (
33 | setIsHovered(true)}
41 | onMouseLeave={() => !isSelected && setIsHovered(false)}
42 | >
43 | {chat.name}
44 |
45 | setIsSelected(open)}>
46 |
54 |
55 |
56 |
57 | {
60 | e.stopPropagation()
61 | }}
62 | >
63 |
71 |
72 |
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/components/chats/chats-list.tsx:
--------------------------------------------------------------------------------
1 | import { SelectChat } from "@/db/schema/chats";
2 | import { cn } from "@/lib/utils";
3 | import { FC, HTMLAttributes } from "react";
4 | import { ChatListItem } from "./chat-list-item";
5 |
6 | interface ChatsListProps extends HTMLAttributes {
7 | chats: SelectChat[];
8 | }
9 |
10 | export const ChatsList: FC = ({ chats, ...props }) => {
11 | return (
12 | <>
13 | {chats.length > 0 ? (
14 |
15 | {chats.map((chat) => (
16 |
20 | ))}
21 |
22 | ) : (
23 |
26 | )}
27 | >
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/components/chats/create-chat-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AppContext } from "@/lib/context/app-context";
4 | import { cn } from "@/lib/utils";
5 | import { Plus } from "lucide-react";
6 | import { usePathname, useRouter } from "next/navigation";
7 | import { FC, HTMLAttributes, useContext } from "react";
8 | import { Button } from "../ui/button";
9 |
10 | interface CreateChatButtonProps extends HTMLAttributes {}
11 |
12 | export const CreateChatButton: FC = ({ ...props }) => {
13 | const { setSelectedChatId } = useContext(AppContext);
14 |
15 | const router = useRouter();
16 | const pathname = usePathname();
17 |
18 | return (
19 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/components/dashboard.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { SelectChat } from "@/db/schema/chats"
4 | import { SelectProfile } from "@/db/schema/profiles"
5 | import { AppContext } from "@/lib/context/app-context"
6 | import { cn } from "@/lib/utils"
7 | import { HTMLAttributes, useContext, useEffect, useState } from "react"
8 | import { ArenaModeIndicator } from "./arena-mode-indicator"
9 | import { ChatsBar } from "./chats-bar/chats-bar"
10 | import { SettingsBar } from "./settings-bar/settings-bar"
11 |
12 | interface DashboardProps extends HTMLAttributes {
13 | profile: SelectProfile
14 | chats: SelectChat[]
15 | }
16 |
17 | export const Dashboard = ({ profile, chats, ...props }: DashboardProps) => {
18 | const { setProfile } = useContext(AppContext)
19 |
20 | const [isChatsOpen, setIsChatsOpen] = useState(true)
21 |
22 | useEffect(() => {
23 | setProfile(profile)
24 | }, [profile])
25 |
26 | return (
27 |
28 |
33 |
34 |
35 | {props.children}
36 |
37 |
38 |
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/components/messages/assistant-message.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { updateTurnPreferenceAction } from "@/actions/messages"
4 | import { updateProfile } from "@/db/queries/profiles"
5 | import { SelectMessage } from "@/db/schema/messages"
6 | import { AppContext } from "@/lib/context/app-context"
7 | import { cn } from "@/lib/utils"
8 | import { BotIcon } from "lucide-react"
9 | import { FC, HTMLAttributes, useContext } from "react"
10 | import { CopyMessage } from "./copy-message"
11 | import { MessageFeedback } from "./message-feedback"
12 | import { MessageMarkdown } from "./message-markdown"
13 | import { MessagePreference } from "./message-preference"
14 | import { MessageStats } from "./message-stats"
15 | import { RegenerateMessage } from "./regenerate-message"
16 | import { RegenerateMessageFromModel } from "./regenerate-message-from-model"
17 |
18 | interface AssistantMessageProps extends HTMLAttributes {
19 | message: SelectMessage
20 | isLast: boolean
21 | isGenerating: boolean
22 | turnHasPreferenceSelection: boolean
23 | sessionId: string | null
24 | onFeedbackUpdate: (messageId: string, thumbsUp: boolean) => void
25 | onPreferenceUpdate: (messageId: string) => void
26 | onRegenerate: (modelId: string) => void
27 | }
28 |
29 | export const AssistantMessage: FC = ({
30 | message,
31 | isLast,
32 | isGenerating,
33 | turnHasPreferenceSelection,
34 | sessionId,
35 | onFeedbackUpdate,
36 | onPreferenceUpdate,
37 | onRegenerate,
38 | ...props
39 | }) => {
40 | const isArenaMessage = message.arenaMessage
41 | const isPreferred = message.isPreferred
42 | const needsPreferenceSelection = isArenaMessage && !turnHasPreferenceSelection
43 |
44 | const { profile, setProfile } = useContext(AppContext)
45 |
46 | const handlePreference = async () => {
47 | if (!profile) return
48 |
49 | if (needsPreferenceSelection && profile.routerProgress < 10) {
50 | setProfile({ ...profile, routerProgress: profile.routerProgress + 1 })
51 | await updateProfile(profile.id, {
52 | ...profile,
53 | routerProgress: profile.routerProgress + 1
54 | })
55 | }
56 |
57 | onPreferenceUpdate(message.id)
58 | await updateTurnPreferenceAction(message.chatId, message.id, message.turn)
59 | }
60 |
61 | return (
62 | {
72 | if (needsPreferenceSelection) {
73 | handlePreference()
74 | }
75 | }}
76 | >
77 | {!isArenaMessage && (
78 |
79 | )}
80 |
81 |
82 |
83 |
84 |
85 | {isGenerating || needsPreferenceSelection ? (
86 |
87 | ) : (
88 |
89 |
90 | {isLast && !isArenaMessage && (
91 | onRegenerate(message.model)}
93 | />
94 | )}
95 | {isLast && !isArenaMessage && (
96 |
100 | )}
101 | {!isArenaMessage && (
102 |
112 | )}
113 |
114 | {isArenaMessage && !needsPreferenceSelection && !isPreferred && (
115 |
119 | )}
120 |
121 | )}
122 |
123 |
124 | {(!isGenerating || !isLast) &&
125 | (!isArenaMessage ||
126 | (isArenaMessage && turnHasPreferenceSelection)) && (
127 | <>
128 |
129 |
134 | >
135 | )}
136 |
137 |
138 | )
139 | }
140 |
--------------------------------------------------------------------------------
/components/messages/copy-message.tsx:
--------------------------------------------------------------------------------
1 | import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard";
2 | import { Check, Copy } from "lucide-react";
3 | import { FC, HTMLAttributes, useState } from "react";
4 | import { ACTION_ICON_SIZE, MessageActionButton } from "./message-action-button";
5 |
6 | interface CopyMessageProps extends HTMLAttributes {
7 | content: string;
8 | }
9 |
10 | export const CopyMessage: FC = ({ content, ...props }) => {
11 | const { copyToClipboard } = useCopyToClipboard({});
12 | const [copied, setCopied] = useState(false);
13 |
14 | const handleCopy = () => {
15 | copyToClipboard(content);
16 | setCopied(true);
17 | setTimeout(() => setCopied(false), 2000);
18 | };
19 |
20 | return (
21 | : }
24 | className={props.className}
25 | />
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/components/messages/message-action-button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { motion } from "framer-motion";
3 | import { FC, HTMLAttributes, ReactNode } from "react";
4 | import { Button } from "../ui/button";
5 |
6 | export const ACTION_ICON_SIZE = 4;
7 |
8 | interface MessageActionButtonProps extends HTMLAttributes {
9 | onClick: () => void;
10 | icon: ReactNode;
11 | isActive?: boolean;
12 | }
13 |
14 | export const MessageActionButton: FC = ({ onClick, icon, isActive, ...props }) => {
15 | return (
16 |
21 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/components/messages/message-feedback.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { updateMessageAction } from "@/actions/messages"
4 | import { submitFeedback } from "@/actions/not-diamond/submit-feedback"
5 | import { NDLLMProvider } from "@/lib/not-diamond/select-random-model"
6 | import { cn } from "@/lib/utils"
7 | import { ThumbsDown, ThumbsUp } from "lucide-react"
8 | import { FC, HTMLAttributes } from "react"
9 | import { ACTION_ICON_SIZE, MessageActionButton } from "./message-action-button"
10 |
11 | interface MessageFeedbackProps extends HTMLAttributes {
12 | messageId: string
13 | thumbsUp: boolean | null
14 | sessionId: string | null
15 | provider: NDLLMProvider
16 | onFeedbackUpdate: (messageId: string, thumbsUp: boolean) => void
17 | }
18 |
19 | export const MessageFeedback: FC = ({
20 | messageId,
21 | thumbsUp,
22 | sessionId,
23 | provider,
24 | onFeedbackUpdate,
25 | ...props
26 | }) => {
27 | const handleFeedback = async (thumbsUp: boolean) => {
28 | const thumbs = thumbsUp ? 1 : -1
29 |
30 | onFeedbackUpdate(messageId, thumbsUp)
31 |
32 | if (sessionId) {
33 | await submitFeedback(sessionId, thumbs, provider)
34 | }
35 |
36 | await updateMessageAction(messageId, { thumbsUp })
37 | }
38 |
39 | return (
40 |
41 | handleFeedback(true)}
43 | icon={
44 |
47 | }
48 | isActive={thumbsUp === true}
49 | />
50 | handleFeedback(false)}
52 | icon={
53 |
56 | }
57 | isActive={thumbsUp === false}
58 | />
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/components/messages/message-markdown-memoized.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from "react";
2 | import ReactMarkdown, { Options } from "react-markdown";
3 |
4 | export const MessageMarkdownMemoized: FC = memo(ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.className === nextProps.className);
5 |
--------------------------------------------------------------------------------
/components/messages/message-markdown.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import React, { FC, HTMLAttributes } from "react";
3 | import remarkGfm from "remark-gfm";
4 | import remarkMath from "remark-math";
5 | import { MessageMarkdownMemoized } from "./message-markdown-memoized";
6 | import { MessageCodeBlock } from "./messsage-codeblock";
7 |
8 | interface MessageMarkdownProps extends HTMLAttributes {
9 | content: string;
10 | }
11 |
12 | export const MessageMarkdown: FC = ({ content, ...props }) => {
13 | return (
14 | {children}
;
20 | },
21 | img({ node, ...props }) {
22 | return (
23 |
27 | );
28 | },
29 | code({ node, className, children, ...props }) {
30 | const childArray = React.Children.toArray(children);
31 | const firstChild = childArray[0] as React.ReactElement;
32 | const firstChildAsString = React.isValidElement(firstChild) ? (firstChild as React.ReactElement).props.children : firstChild;
33 |
34 | if (firstChildAsString === "▍") {
35 | return ▍;
36 | }
37 |
38 | if (typeof firstChildAsString === "string") {
39 | childArray[0] = firstChildAsString.replace("`▍`", "▍");
40 | }
41 |
42 | const match = /language-(\w+)/.exec(className || "");
43 |
44 | if (typeof firstChildAsString === "string" && !firstChildAsString.includes("\n")) {
45 | return (
46 |
50 | {childArray}
51 |
52 | );
53 | }
54 |
55 | return (
56 |
62 | );
63 | }
64 | }}
65 | >
66 | {content}
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/components/messages/message-preference.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Star } from "lucide-react";
4 | import { FC, HTMLAttributes } from "react";
5 | import { ACTION_ICON_SIZE, MessageActionButton } from "./message-action-button";
6 |
7 | interface MessagePreferenceProps extends HTMLAttributes {
8 | messageId: string;
9 | onChangePreference: () => void;
10 | }
11 |
12 | export const MessagePreference: FC = ({ messageId, onChangePreference, ...props }) => {
13 | return (
14 | }
17 | className={props.className}
18 | />
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/components/messages/message-stats.tsx:
--------------------------------------------------------------------------------
1 | import { ndLLMProviders } from "@/lib/not-diamond/not-diamond-config"
2 | import { cn } from "@/lib/utils"
3 | import { FC, HTMLAttributes } from "react"
4 |
5 | interface MessageStatsProps extends HTMLAttributes {
6 | modelId: string
7 | latency: number | null
8 | cost: number | null
9 | }
10 |
11 | export const MessageStats: FC = ({
12 | modelId,
13 | latency,
14 | cost,
15 | ...props
16 | }) => {
17 | const stats = [
18 | {
19 | label: "model",
20 | value:
21 | ndLLMProviders.find(item => item.providerModelId === modelId)?.label ||
22 | modelId
23 | },
24 | { label: "latency", value: latency ? `${latency}ms` : "N/A" },
25 | { label: "cost", value: cost ? `$${(cost / 1000000).toFixed(4)}` : "N/A" }
26 | ]
27 |
28 | return (
29 |
30 | {stats.map(({ label, value }) => (
31 |
32 | {label}:{" "}
33 | {value}
34 |
35 | ))}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/messages/messsage-codeblock.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard";
3 | import { cn } from "@/lib/utils";
4 | import { Check, Copy, Download } from "lucide-react";
5 | import { FC, HTMLAttributes, memo } from "react";
6 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
7 | import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
8 |
9 | interface MessageCodeBlockProps extends HTMLAttributes {
10 | language: string;
11 | value: string;
12 | }
13 |
14 | interface languageMap {
15 | [key: string]: string | undefined;
16 | }
17 |
18 | export const programmingLanguages: languageMap = {
19 | javascript: ".js",
20 | python: ".py",
21 | java: ".java",
22 | c: ".c",
23 | cpp: ".cpp",
24 | "c++": ".cpp",
25 | "c#": ".cs",
26 | ruby: ".rb",
27 | php: ".php",
28 | swift: ".swift",
29 | "objective-c": ".m",
30 | kotlin: ".kt",
31 | typescript: ".ts",
32 | go: ".go",
33 | perl: ".pl",
34 | rust: ".rs",
35 | scala: ".scala",
36 | haskell: ".hs",
37 | lua: ".lua",
38 | shell: ".sh",
39 | sql: ".sql",
40 | html: ".html",
41 | css: ".css"
42 | };
43 |
44 | export const generateRandomString = (length: number, lowercase = false) => {
45 | const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
46 | let result = "";
47 | for (let i = 0; i < length; i++) {
48 | result += chars.charAt(Math.floor(Math.random() * chars.length));
49 | }
50 | return lowercase ? result.toLowerCase() : result;
51 | };
52 |
53 | export const MessageCodeBlock: FC = memo(({ language, value, ...props }) => {
54 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
55 |
56 | const downloadAsFile = () => {
57 | if (typeof window === "undefined") {
58 | return;
59 | }
60 | const fileExtension = programmingLanguages[language] || ".file";
61 | const suggestedFileName = `file-${generateRandomString(3, true)}${fileExtension}`;
62 | const fileName = window.prompt("Enter file name" || "", suggestedFileName);
63 |
64 | if (!fileName) {
65 | return;
66 | }
67 |
68 | const blob = new Blob([value], { type: "text/plain" });
69 | const url = URL.createObjectURL(blob);
70 | const link = document.createElement("a");
71 | link.download = fileName;
72 | link.href = url;
73 | link.style.display = "none";
74 | document.body.appendChild(link);
75 | link.click();
76 | document.body.removeChild(link);
77 | URL.revokeObjectURL(url);
78 | };
79 |
80 | const onCopy = () => {
81 | if (isCopied) return;
82 | copyToClipboard(value);
83 | };
84 |
85 | return (
86 |
87 |
88 |
{language}
89 |
90 |
98 |
99 |
107 |
108 |
109 |
125 | {value}
126 |
127 |
128 | );
129 | });
130 |
131 | MessageCodeBlock.displayName = "MessageCodeBlock";
132 |
--------------------------------------------------------------------------------
/components/messages/regenerate-message-from-model.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { AppContext } from "@/lib/context/app-context"
4 | import { ndLLMProviders } from "@/lib/not-diamond/not-diamond-config"
5 | import { Check, ChevronDown, Repeat, Sparkles } from "lucide-react"
6 | import { FC, HTMLAttributes, useContext, useState } from "react"
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger
12 | } from "../ui/dropdown-menu"
13 | import { ACTION_ICON_SIZE, MessageActionButton } from "./message-action-button"
14 |
15 | interface RegenerateMessageFromModelProps
16 | extends HTMLAttributes {
17 | currentModel: string
18 | onRegenerateFromModel: (modelId: string) => void
19 | }
20 |
21 | export const RegenerateMessageFromModel: FC<
22 | RegenerateMessageFromModelProps
23 | > = ({ currentModel, onRegenerateFromModel, ...props }) => {
24 | const { profile } = useContext(AppContext)
25 |
26 | const [hoveredModel, setHoveredModel] = useState(null)
27 |
28 | if (!profile) return null
29 |
30 | // Sort models by provider and then by model
31 | const sortedModels = profile.activeModels
32 | .map(modelId => ndLLMProviders.find(m => m.providerModelId === modelId))
33 | .filter(model => model !== undefined)
34 | .sort((a, b) => {
35 | if (!a || !b) return 0
36 |
37 | if (a.cloudProvider < b.cloudProvider) return -1
38 | if (a.cloudProvider > b.cloudProvider) return 1
39 | if (a.providerModelId < b.providerModelId) return -1
40 | if (a.providerModelId > b.providerModelId) return 1
41 | return 0
42 | })
43 |
44 | return (
45 |
46 |
47 | {}}
49 | icon={
50 |
51 |
52 |
53 |
54 | }
55 | className={props.className}
56 | />
57 |
58 |
59 |
60 |
61 | {sortedModels.map(
62 | item =>
63 | item && (
64 |
onRegenerateFromModel(item.providerModelId)}
68 | onMouseEnter={() => setHoveredModel(item.providerModelId)}
69 | onMouseLeave={() => setHoveredModel(null)}
70 | >
71 |
72 |
{item.label}
73 |
74 | {item.cloudProvider}
75 |
76 |
77 |
78 | {hoveredModel === item.providerModelId && (
79 |
80 | )}
81 | {currentModel === item.providerModelId &&
82 | hoveredModel !== item.providerModelId && (
83 |
84 | )}
85 |
86 | )
87 | )}
88 |
89 |
90 |
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/components/messages/regenerate-message.tsx:
--------------------------------------------------------------------------------
1 | import { Repeat } from "lucide-react";
2 | import { FC, HTMLAttributes } from "react";
3 | import { ACTION_ICON_SIZE, MessageActionButton } from "./message-action-button";
4 |
5 | interface RegenerateMessageProps extends HTMLAttributes {
6 | onRegenerate: () => void;
7 | }
8 |
9 | export const RegenerateMessage: FC = ({ onRegenerate, ...props }) => {
10 | return (
11 | }
14 | className={props.className}
15 | />
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/components/messages/user-message.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SelectMessage } from "@/db/schema/messages";
4 | import { cn } from "@/lib/utils";
5 | import { Pencil } from "lucide-react";
6 | import { FC, HTMLAttributes, useEffect, useRef, useState } from "react";
7 | import ReactTextareaAutosize from "react-textarea-autosize";
8 | import { Button } from "../ui/button";
9 | import { MessageMarkdown } from "./message-markdown";
10 |
11 | interface UserMessageProps extends HTMLAttributes {
12 | message: SelectMessage;
13 | onSubmitEdit: (editedMessage: SelectMessage) => void;
14 | }
15 |
16 | export const UserMessage: FC = ({ message, onSubmitEdit, ...props }) => {
17 | const inputRef = useRef(null);
18 |
19 | const [isHovering, setIsHovering] = useState(false);
20 | const [isEditing, setIsEditing] = useState(false);
21 | const [inputValue, setInputValue] = useState(message.content);
22 |
23 | const handleSubmitEdit = () => {
24 | onSubmitEdit({ ...message, content: inputValue });
25 | setIsEditing(false);
26 | };
27 |
28 | const handleKeyDown = (event: React.KeyboardEvent) => {
29 | if (!event.shiftKey && event.key === "Enter") {
30 | event.preventDefault();
31 | onSubmitEdit({ ...message, content: inputValue });
32 | } else if (event.key === "Escape") {
33 | setIsEditing(false);
34 | }
35 | };
36 |
37 | useEffect(() => {
38 | if (isEditing) {
39 | inputRef.current?.focus();
40 | inputRef.current?.setSelectionRange(inputRef.current.value.length, inputRef.current.value.length);
41 | }
42 | }, [isEditing]);
43 |
44 | return (
45 | setIsHovering(true)}
48 | onMouseLeave={() => setIsHovering(false)}
49 | >
50 | {isHovering && !isEditing && (
51 |
{
54 | setInputValue(message.content);
55 | setIsEditing(true);
56 | }}
57 | />
58 | )}
59 |
60 | {isEditing ? (
61 |
62 |
setInputValue(e.target.value)}
68 | minRows={3}
69 | onKeyDown={handleKeyDown}
70 | />
71 |
72 |
73 |
80 |
81 |
87 |
88 |
89 | ) : (
90 |
91 |
92 |
93 | )}
94 |
95 | );
96 | };
97 |
--------------------------------------------------------------------------------
/components/onboarding-message.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { updateProfile } from "@/db/queries/profiles"
4 | import { AppContext } from "@/lib/context/app-context"
5 | import { FC, useContext } from "react"
6 | import {
7 | Dialog,
8 | DialogContent,
9 | DialogDescription,
10 | DialogHeader,
11 | DialogTitle
12 | } from "./ui/dialog"
13 |
14 | interface OnboardingMessageProps {}
15 |
16 | export const OnboardingMessage: FC = () => {
17 | const { profile, setProfile } = useContext(AppContext)
18 |
19 | const handleOnboarding = async () => {
20 | if (!profile) return
21 | setProfile({ ...profile, hasOnboarded: true })
22 | await updateProfile(profile.id, { hasOnboarded: true })
23 | }
24 |
25 | if (!profile) return null
26 |
27 | return (
28 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/components/router-progress-bar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { AppContext } from "@/lib/context/app-context"
4 | import { cn } from "@/lib/utils"
5 | import { FC, HTMLAttributes, useContext } from "react"
6 | import {
7 | Dialog,
8 | DialogContent,
9 | DialogDescription,
10 | DialogHeader,
11 | DialogTitle
12 | } from "./ui/dialog"
13 | import { Progress } from "./ui/progress"
14 |
15 | export const MAX_ROUTER_PROGRESS = 10
16 |
17 | interface RouterProgressBarProps extends HTMLAttributes {}
18 |
19 | export const RouterProgressBar: FC = ({ ...props }) => {
20 | const { profile, setProfile, setIsArenaModeActive } = useContext(AppContext)
21 |
22 | if (!profile) return null
23 |
24 | return (
25 | <>
26 |
27 | {profile.routerProgress < 10 && (
28 | <>
29 |
41 |
42 |
59 | >
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/components/settings-bar/settings-bar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { updateProfile } from "@/db/queries/profiles"
4 | import { AppContext } from "@/lib/context/app-context"
5 | import { ndLLMProviders } from "@/lib/not-diamond/not-diamond-config"
6 | import { cn } from "@/lib/utils"
7 | import { Settings } from "lucide-react"
8 | import { FC, HTMLAttributes, useContext } from "react"
9 | import { Button } from "../ui/button"
10 | import { Checkbox } from "../ui/checkbox"
11 | import {
12 | Sheet,
13 | SheetContent,
14 | SheetDescription,
15 | SheetHeader,
16 | SheetTitle,
17 | SheetTrigger
18 | } from "../ui/sheet"
19 | import { Switch } from "../ui/switch"
20 | import {
21 | Tooltip,
22 | TooltipContent,
23 | TooltipProvider,
24 | TooltipTrigger
25 | } from "../ui/tooltip"
26 |
27 | interface SettingsBarProps extends HTMLAttributes {}
28 |
29 | export const SettingsBar: FC = ({ ...props }) => {
30 | const { isArenaModeActive, setIsArenaModeActive, profile, setProfile } =
31 | useContext(AppContext)
32 |
33 | const handleArenaModeChange = () => {
34 | setIsArenaModeActive(!isArenaModeActive)
35 | }
36 |
37 | const handleModelChange = async (model: string) => {
38 | if (!profile) return
39 |
40 | const newModels = profile.activeModels.includes(model)
41 | ? profile.activeModels.filter(m => m !== model)
42 | : [...profile.activeModels, model]
43 | setProfile({ ...profile, activeModels: newModels })
44 | await updateProfile(profile.id, {
45 | ...profile,
46 | activeModels: newModels
47 | })
48 | }
49 |
50 | if (!profile) return
51 |
52 | return (
53 |
54 |
55 |
56 |
57 |
58 |
61 |
62 | Open sidepanel
63 |
64 |
65 |
66 |
67 |
68 |
69 | Settings
70 | Customize your experience.
71 |
72 |
73 |
74 |
75 |
79 |
Arena Mode
80 |
81 |
82 |
83 |
Active Models
84 |
85 | {ndLLMProviders.map(item => (
86 |
90 |
93 | handleModelChange(item.providerModelId)
94 | }
95 | />
96 | {item.label}
97 |
98 | ))}
99 |
100 |
101 |
102 |
103 | )
104 | }
105 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
59 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
13 | secondary:
14 | "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
15 | destructive:
16 | "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:size-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border-input bg-background hover:bg-accent hover:text-accent-foreground border",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "size-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeft, ChevronRight } from "lucide-react"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
58 | IconRight: ({ ...props }) => ,
59 | }}
60 | {...props}
61 | />
62 | )
63 | }
64 | Calendar.displayName = "Calendar"
65 |
66 | export { Calendar }
67 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from "embla-carousel-react"
7 | import { ArrowLeft, ArrowRight } from "lucide-react"
8 |
9 | import { cn } from "@/lib/utils"
10 | import { Button } from "@/components/ui/button"
11 |
12 | type CarouselApi = UseEmblaCarouselType[1]
13 | type UseCarouselParameters = Parameters
14 | type CarouselOptions = UseCarouselParameters[0]
15 | type CarouselPlugin = UseCarouselParameters[1]
16 |
17 | type CarouselProps = {
18 | opts?: CarouselOptions
19 | plugins?: CarouselPlugin
20 | orientation?: "horizontal" | "vertical"
21 | setApi?: (api: CarouselApi) => void
22 | }
23 |
24 | type CarouselContextProps = {
25 | carouselRef: ReturnType[0]
26 | api: ReturnType[1]
27 | scrollPrev: () => void
28 | scrollNext: () => void
29 | canScrollPrev: boolean
30 | canScrollNext: boolean
31 | } & CarouselProps
32 |
33 | const CarouselContext = React.createContext(null)
34 |
35 | function useCarousel() {
36 | const context = React.useContext(CarouselContext)
37 |
38 | if (!context) {
39 | throw new Error("useCarousel must be used within a ")
40 | }
41 |
42 | return context
43 | }
44 |
45 | const Carousel = React.forwardRef<
46 | HTMLDivElement,
47 | React.HTMLAttributes & CarouselProps
48 | >(
49 | (
50 | {
51 | orientation = "horizontal",
52 | opts,
53 | setApi,
54 | plugins,
55 | className,
56 | children,
57 | ...props
58 | },
59 | ref
60 | ) => {
61 | const [carouselRef, api] = useEmblaCarousel(
62 | {
63 | ...opts,
64 | axis: orientation === "horizontal" ? "x" : "y",
65 | },
66 | plugins
67 | )
68 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
69 | const [canScrollNext, setCanScrollNext] = React.useState(false)
70 |
71 | const onSelect = React.useCallback((api: CarouselApi) => {
72 | if (!api) {
73 | return
74 | }
75 |
76 | setCanScrollPrev(api.canScrollPrev())
77 | setCanScrollNext(api.canScrollNext())
78 | }, [])
79 |
80 | const scrollPrev = React.useCallback(() => {
81 | api?.scrollPrev()
82 | }, [api])
83 |
84 | const scrollNext = React.useCallback(() => {
85 | api?.scrollNext()
86 | }, [api])
87 |
88 | const handleKeyDown = React.useCallback(
89 | (event: React.KeyboardEvent) => {
90 | if (event.key === "ArrowLeft") {
91 | event.preventDefault()
92 | scrollPrev()
93 | } else if (event.key === "ArrowRight") {
94 | event.preventDefault()
95 | scrollNext()
96 | }
97 | },
98 | [scrollPrev, scrollNext]
99 | )
100 |
101 | React.useEffect(() => {
102 | if (!api || !setApi) {
103 | return
104 | }
105 |
106 | setApi(api)
107 | }, [api, setApi])
108 |
109 | React.useEffect(() => {
110 | if (!api) {
111 | return
112 | }
113 |
114 | onSelect(api)
115 | api.on("reInit", onSelect)
116 | api.on("select", onSelect)
117 |
118 | return () => {
119 | api?.off("select", onSelect)
120 | }
121 | }, [api, onSelect])
122 |
123 | return (
124 |
137 |
145 | {children}
146 |
147 |
148 | )
149 | }
150 | )
151 | Carousel.displayName = "Carousel"
152 |
153 | const CarouselContent = React.forwardRef<
154 | HTMLDivElement,
155 | React.HTMLAttributes
156 | >(({ className, ...props }, ref) => {
157 | const { carouselRef, orientation } = useCarousel()
158 |
159 | return (
160 |
171 | )
172 | })
173 | CarouselContent.displayName = "CarouselContent"
174 |
175 | const CarouselItem = React.forwardRef<
176 | HTMLDivElement,
177 | React.HTMLAttributes
178 | >(({ className, ...props }, ref) => {
179 | const { orientation } = useCarousel()
180 |
181 | return (
182 |
193 | )
194 | })
195 | CarouselItem.displayName = "CarouselItem"
196 |
197 | const CarouselPrevious = React.forwardRef<
198 | HTMLButtonElement,
199 | React.ComponentProps
200 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
201 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
202 |
203 | return (
204 |
222 | )
223 | })
224 | CarouselPrevious.displayName = "CarouselPrevious"
225 |
226 | const CarouselNext = React.forwardRef<
227 | HTMLButtonElement,
228 | React.ComponentProps
229 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
230 | const { orientation, scrollNext, canScrollNext } = useCarousel()
231 |
232 | return (
233 |
251 | )
252 | })
253 | CarouselNext.displayName = "CarouselNext"
254 |
255 | export {
256 | type CarouselApi,
257 | Carousel,
258 | CarouselContent,
259 | CarouselItem,
260 | CarouselPrevious,
261 | CarouselNext,
262 | }
263 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | )
17 | Drawer.displayName = "Drawer"
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal
22 |
23 | const DrawerClose = DrawerPrimitive.Close
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ))
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ))
56 | DrawerContent.displayName = "DrawerContent"
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | )
67 | DrawerHeader.displayName = "DrawerHeader"
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | )
78 | DrawerFooter.displayName = "DrawerFooter"
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ))
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ))
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | }
119 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { OTPInput, OTPInputContext } from "input-otp"
5 | import { Dot } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const InputOTP = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, containerClassName, ...props }, ref) => (
13 |
22 | ))
23 | InputOTP.displayName = "InputOTP"
24 |
25 | const InputOTPGroup = React.forwardRef<
26 | React.ElementRef<"div">,
27 | React.ComponentPropsWithoutRef<"div">
28 | >(({ className, ...props }, ref) => (
29 |
30 | ))
31 | InputOTPGroup.displayName = "InputOTPGroup"
32 |
33 | const InputOTPSlot = React.forwardRef<
34 | React.ElementRef<"div">,
35 | React.ComponentPropsWithoutRef<"div"> & { index: number }
36 | >(({ index, className, ...props }, ref) => {
37 | const inputOTPContext = React.useContext(OTPInputContext)
38 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
39 |
40 | return (
41 |
50 | {char}
51 | {hasFakeCaret && (
52 |
55 | )}
56 |
57 | )
58 | })
59 | InputOTPSlot.displayName = "InputOTPSlot"
60 |
61 | const InputOTPSeparator = React.forwardRef<
62 | React.ElementRef<"div">,
63 | React.ComponentPropsWithoutRef<"div">
64 | >(({ ...props }, ref) => (
65 |
66 |
67 |
68 | ))
69 | InputOTPSeparator.displayName = "InputOTPSeparator"
70 |
71 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
72 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "bg-background hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[active]:bg-accent/50 data-[state=open]:bg-accent/50 group inline-flex h-10 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus:outline-none disabled:pointer-events-none disabled:opacity-50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { ButtonProps, buttonVariants } from "@/components/ui/button"
6 |
7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8 |
14 | )
15 | Pagination.displayName = "Pagination"
16 |
17 | const PaginationContent = React.forwardRef<
18 | HTMLUListElement,
19 | React.ComponentProps<"ul">
20 | >(({ className, ...props }, ref) => (
21 |
26 | ))
27 | PaginationContent.displayName = "PaginationContent"
28 |
29 | const PaginationItem = React.forwardRef<
30 | HTMLLIElement,
31 | React.ComponentProps<"li">
32 | >(({ className, ...props }, ref) => (
33 |
34 | ))
35 | PaginationItem.displayName = "PaginationItem"
36 |
37 | type PaginationLinkProps = {
38 | isActive?: boolean
39 | } & Pick &
40 | React.ComponentProps<"a">
41 |
42 | const PaginationLink = ({
43 | className,
44 | isActive,
45 | size = "icon",
46 | ...props
47 | }: PaginationLinkProps) => (
48 |
59 | )
60 | PaginationLink.displayName = "PaginationLink"
61 |
62 | const PaginationPrevious = ({
63 | className,
64 | ...props
65 | }: React.ComponentProps) => (
66 |
72 |
73 | Previous
74 |
75 | )
76 | PaginationPrevious.displayName = "PaginationPrevious"
77 |
78 | const PaginationNext = ({
79 | className,
80 | ...props
81 | }: React.ComponentProps) => (
82 |
88 | Next
89 |
90 |
91 | )
92 | PaginationNext.displayName = "PaginationNext"
93 |
94 | const PaginationEllipsis = ({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"span">) => (
98 |
103 |
104 | More pages
105 |
106 | )
107 | PaginationEllipsis.displayName = "PaginationEllipsis"
108 |
109 | export {
110 | Pagination,
111 | PaginationContent,
112 | PaginationEllipsis,
113 | PaginationItem,
114 | PaginationLink,
115 | PaginationNext,
116 | PaginationPrevious,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { GripVertical } from "lucide-react"
4 | import * as ResizablePrimitive from "react-resizable-panels"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | )
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | )
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
46 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
39 | bottom:
40 | "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
41 | left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
42 | right:
43 | "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "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 group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 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",
29 | {
30 | variants: {
31 | variant: {
32 | default: "bg-background text-foreground border",
33 | destructive:
34 | "destructive border-destructive bg-destructive text-destructive-foreground group",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { toggleVariants } from "@/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | })
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef &
20 | VariantProps
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 |
27 |
28 | {children}
29 |
30 |
31 | ))
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef &
38 | VariantProps
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext)
41 |
42 | return (
43 |
54 | {children}
55 |
56 | )
57 | })
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60 |
61 | export { ToggleGroup, ToggleGroupItem }
62 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "ring-offset-background hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent",
17 | },
18 | size: {
19 | default: "h-10 px-3",
20 | sm: "h-9 px-2.5",
21 | lg: "h-11 px-5",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/components/utility/providers.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { TooltipProvider } from "@/components/ui/tooltip"
4 | import { AppContextProvider } from "@/lib/context/app-context"
5 | import { ThemeProvider as NextThemesProvider } from "next-themes"
6 | import { ThemeProviderProps } from "next-themes/dist/types"
7 | import { FC } from "react"
8 | import { WaitForHydration } from "./wait-for-hydration"
9 |
10 | export const Providers: FC = ({ children, ...props }) => {
11 | return (
12 |
13 |
14 |
15 | {children}
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/components/utility/wait-for-hydration.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { PropsWithChildren, useEffect, useState } from "react"
4 |
5 | export function WaitForHydration({ children }: PropsWithChildren) {
6 | const [isHydrated, setIsHydrated] = useState(false)
7 |
8 | // Wait till Next.js rehydration completes
9 | useEffect(() => {
10 | setIsHydrated(true)
11 | }, [])
12 |
13 | return <>{isHydrated ? <>{children}> : null}>
14 | }
15 |
--------------------------------------------------------------------------------
/db/db.ts:
--------------------------------------------------------------------------------
1 | import { neon } from "@neondatabase/serverless"
2 | import { config } from "dotenv"
3 | import { drizzle } from "drizzle-orm/neon-http"
4 | import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js"
5 | import postgres from "postgres"
6 | import * as schema from "./schema"
7 |
8 | config({ path: ".env.local" })
9 |
10 | const databaseUrl = process.env.DATABASE_URL
11 |
12 | if (!databaseUrl) {
13 | throw new Error("DATABASE_URL is not set")
14 | }
15 |
16 | const dbSchema = {
17 | profiles: schema.profiles,
18 | chats: schema.chats,
19 | messages: schema.messages
20 | }
21 |
22 | function initializeDb(url: string) {
23 | const isNeon = url.includes("neon")
24 |
25 | if (isNeon) {
26 | const client = neon(url)
27 | return drizzle(client, { schema: dbSchema })
28 | } else {
29 | const client = postgres(url, { prepare: false })
30 | return drizzlePostgres(client, { schema: dbSchema })
31 | }
32 | }
33 |
34 | export const db = initializeDb(databaseUrl)
35 |
--------------------------------------------------------------------------------
/db/migrations/0000_lying_violations.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS "chats" (
2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3 | "profile_id" uuid NOT NULL,
4 | "name" varchar(255) NOT NULL,
5 | "created_at" timestamp DEFAULT now() NOT NULL,
6 | "updated_at" timestamp DEFAULT now() NOT NULL
7 | );
8 | --> statement-breakpoint
9 | CREATE TABLE IF NOT EXISTS "messages" (
10 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
11 | "chat_id" uuid NOT NULL,
12 | "profile_id" uuid NOT NULL,
13 | "content" text NOT NULL,
14 | "role" varchar(256) NOT NULL,
15 | "provider" varchar(256) NOT NULL,
16 | "model" varchar(256) NOT NULL,
17 | "turn" integer NOT NULL,
18 | "thumbs_up" boolean,
19 | "arena_message" boolean DEFAULT false NOT NULL,
20 | "is_preferred" boolean,
21 | "latency" integer,
22 | "cost" integer,
23 | "created_at" timestamp DEFAULT now() NOT NULL,
24 | "updated_at" timestamp DEFAULT now() NOT NULL
25 | );
26 | --> statement-breakpoint
27 | CREATE TABLE IF NOT EXISTS "profiles" (
28 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
29 | "preference_id" text,
30 | "has_onboarded" boolean DEFAULT false NOT NULL,
31 | "router_progress" integer DEFAULT 0 NOT NULL,
32 | "active_models" text[] DEFAULT ARRAY[]::text[] NOT NULL,
33 | "created_at" timestamp DEFAULT now() NOT NULL,
34 | "updated_at" timestamp DEFAULT now() NOT NULL,
35 | CONSTRAINT "profiles_preference_id_unique" UNIQUE("preference_id")
36 | );
37 | --> statement-breakpoint
38 | DO $$ BEGIN
39 | ALTER TABLE "chats" ADD CONSTRAINT "chats_profile_id_profiles_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;
40 | EXCEPTION
41 | WHEN duplicate_object THEN null;
42 | END $$;
43 | --> statement-breakpoint
44 | DO $$ BEGIN
45 | ALTER TABLE "messages" ADD CONSTRAINT "messages_chat_id_chats_id_fk" FOREIGN KEY ("chat_id") REFERENCES "public"."chats"("id") ON DELETE cascade ON UPDATE no action;
46 | EXCEPTION
47 | WHEN duplicate_object THEN null;
48 | END $$;
49 | --> statement-breakpoint
50 | DO $$ BEGIN
51 | ALTER TABLE "messages" ADD CONSTRAINT "messages_profile_id_profiles_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."profiles"("id") ON DELETE cascade ON UPDATE no action;
52 | EXCEPTION
53 | WHEN duplicate_object THEN null;
54 | END $$;
55 |
--------------------------------------------------------------------------------
/db/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "4fbd9f8e-ec75-4726-990c-f466603fc210",
3 | "prevId": "00000000-0000-0000-0000-000000000000",
4 | "version": "6",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.chats": {
8 | "name": "chats",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "uuid",
14 | "primaryKey": true,
15 | "notNull": true,
16 | "default": "gen_random_uuid()"
17 | },
18 | "profile_id": {
19 | "name": "profile_id",
20 | "type": "uuid",
21 | "primaryKey": false,
22 | "notNull": true
23 | },
24 | "name": {
25 | "name": "name",
26 | "type": "varchar(255)",
27 | "primaryKey": false,
28 | "notNull": true
29 | },
30 | "created_at": {
31 | "name": "created_at",
32 | "type": "timestamp",
33 | "primaryKey": false,
34 | "notNull": true,
35 | "default": "now()"
36 | },
37 | "updated_at": {
38 | "name": "updated_at",
39 | "type": "timestamp",
40 | "primaryKey": false,
41 | "notNull": true,
42 | "default": "now()"
43 | }
44 | },
45 | "indexes": {},
46 | "foreignKeys": {
47 | "chats_profile_id_profiles_id_fk": {
48 | "name": "chats_profile_id_profiles_id_fk",
49 | "tableFrom": "chats",
50 | "tableTo": "profiles",
51 | "columnsFrom": [
52 | "profile_id"
53 | ],
54 | "columnsTo": [
55 | "id"
56 | ],
57 | "onDelete": "cascade",
58 | "onUpdate": "no action"
59 | }
60 | },
61 | "compositePrimaryKeys": {},
62 | "uniqueConstraints": {}
63 | },
64 | "public.messages": {
65 | "name": "messages",
66 | "schema": "",
67 | "columns": {
68 | "id": {
69 | "name": "id",
70 | "type": "uuid",
71 | "primaryKey": true,
72 | "notNull": true,
73 | "default": "gen_random_uuid()"
74 | },
75 | "chat_id": {
76 | "name": "chat_id",
77 | "type": "uuid",
78 | "primaryKey": false,
79 | "notNull": true
80 | },
81 | "profile_id": {
82 | "name": "profile_id",
83 | "type": "uuid",
84 | "primaryKey": false,
85 | "notNull": true
86 | },
87 | "content": {
88 | "name": "content",
89 | "type": "text",
90 | "primaryKey": false,
91 | "notNull": true
92 | },
93 | "role": {
94 | "name": "role",
95 | "type": "varchar(256)",
96 | "primaryKey": false,
97 | "notNull": true
98 | },
99 | "provider": {
100 | "name": "provider",
101 | "type": "varchar(256)",
102 | "primaryKey": false,
103 | "notNull": true
104 | },
105 | "model": {
106 | "name": "model",
107 | "type": "varchar(256)",
108 | "primaryKey": false,
109 | "notNull": true
110 | },
111 | "turn": {
112 | "name": "turn",
113 | "type": "integer",
114 | "primaryKey": false,
115 | "notNull": true
116 | },
117 | "thumbs_up": {
118 | "name": "thumbs_up",
119 | "type": "boolean",
120 | "primaryKey": false,
121 | "notNull": false
122 | },
123 | "arena_message": {
124 | "name": "arena_message",
125 | "type": "boolean",
126 | "primaryKey": false,
127 | "notNull": true,
128 | "default": false
129 | },
130 | "is_preferred": {
131 | "name": "is_preferred",
132 | "type": "boolean",
133 | "primaryKey": false,
134 | "notNull": false
135 | },
136 | "latency": {
137 | "name": "latency",
138 | "type": "integer",
139 | "primaryKey": false,
140 | "notNull": false
141 | },
142 | "cost": {
143 | "name": "cost",
144 | "type": "integer",
145 | "primaryKey": false,
146 | "notNull": false
147 | },
148 | "created_at": {
149 | "name": "created_at",
150 | "type": "timestamp",
151 | "primaryKey": false,
152 | "notNull": true,
153 | "default": "now()"
154 | },
155 | "updated_at": {
156 | "name": "updated_at",
157 | "type": "timestamp",
158 | "primaryKey": false,
159 | "notNull": true,
160 | "default": "now()"
161 | }
162 | },
163 | "indexes": {},
164 | "foreignKeys": {
165 | "messages_chat_id_chats_id_fk": {
166 | "name": "messages_chat_id_chats_id_fk",
167 | "tableFrom": "messages",
168 | "tableTo": "chats",
169 | "columnsFrom": [
170 | "chat_id"
171 | ],
172 | "columnsTo": [
173 | "id"
174 | ],
175 | "onDelete": "cascade",
176 | "onUpdate": "no action"
177 | },
178 | "messages_profile_id_profiles_id_fk": {
179 | "name": "messages_profile_id_profiles_id_fk",
180 | "tableFrom": "messages",
181 | "tableTo": "profiles",
182 | "columnsFrom": [
183 | "profile_id"
184 | ],
185 | "columnsTo": [
186 | "id"
187 | ],
188 | "onDelete": "cascade",
189 | "onUpdate": "no action"
190 | }
191 | },
192 | "compositePrimaryKeys": {},
193 | "uniqueConstraints": {}
194 | },
195 | "public.profiles": {
196 | "name": "profiles",
197 | "schema": "",
198 | "columns": {
199 | "id": {
200 | "name": "id",
201 | "type": "uuid",
202 | "primaryKey": true,
203 | "notNull": true,
204 | "default": "gen_random_uuid()"
205 | },
206 | "preference_id": {
207 | "name": "preference_id",
208 | "type": "text",
209 | "primaryKey": false,
210 | "notNull": false
211 | },
212 | "has_onboarded": {
213 | "name": "has_onboarded",
214 | "type": "boolean",
215 | "primaryKey": false,
216 | "notNull": true,
217 | "default": false
218 | },
219 | "router_progress": {
220 | "name": "router_progress",
221 | "type": "integer",
222 | "primaryKey": false,
223 | "notNull": true,
224 | "default": 0
225 | },
226 | "active_models": {
227 | "name": "active_models",
228 | "type": "text[]",
229 | "primaryKey": false,
230 | "notNull": true,
231 | "default": "ARRAY[]::text[]"
232 | },
233 | "created_at": {
234 | "name": "created_at",
235 | "type": "timestamp",
236 | "primaryKey": false,
237 | "notNull": true,
238 | "default": "now()"
239 | },
240 | "updated_at": {
241 | "name": "updated_at",
242 | "type": "timestamp",
243 | "primaryKey": false,
244 | "notNull": true,
245 | "default": "now()"
246 | }
247 | },
248 | "indexes": {},
249 | "foreignKeys": {},
250 | "compositePrimaryKeys": {},
251 | "uniqueConstraints": {
252 | "profiles_preference_id_unique": {
253 | "name": "profiles_preference_id_unique",
254 | "nullsNotDistinct": false,
255 | "columns": [
256 | "preference_id"
257 | ]
258 | }
259 | }
260 | }
261 | },
262 | "enums": {},
263 | "schemas": {},
264 | "_meta": {
265 | "columns": {},
266 | "schemas": {},
267 | "tables": {}
268 | }
269 | }
--------------------------------------------------------------------------------
/db/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "6",
3 | "dialect": "postgresql",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "6",
8 | "when": 1718269963322,
9 | "tag": "0000_lying_violations",
10 | "breakpoints": true
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/db/queries/chats.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { desc, eq } from "drizzle-orm"
4 | import { revalidatePath } from "next/cache"
5 | import { db } from "../db"
6 | import { InsertChat, SelectChat, chats } from "../schema/chats"
7 |
8 | export const getChatById = async (id: string): Promise => {
9 | try {
10 | const result = await db.select().from(chats).where(eq(chats.id, id))
11 | return result[0]
12 | } catch (error) {
13 | console.error(`Error fetching chat with id ${id}:`, error)
14 | throw error
15 | }
16 | }
17 |
18 | export const getAllChatsByProfileId = async (
19 | profileId: string
20 | ): Promise => {
21 | try {
22 | return await db
23 | .select()
24 | .from(chats)
25 | .where(eq(chats.profileId, profileId))
26 | .orderBy(desc(chats.createdAt))
27 | } catch (error) {
28 | console.error("Error fetching all chats:", error)
29 | throw error
30 | }
31 | }
32 |
33 | export const createChat = async (chatData: InsertChat): Promise => {
34 | try {
35 | const result = await db.insert(chats).values(chatData).returning()
36 | revalidatePath("/")
37 | return result[0]
38 | } catch (error) {
39 | console.error("Error creating chat:", error)
40 | throw error
41 | }
42 | }
43 |
44 | export const updateChat = async (
45 | id: string,
46 | updateData: Partial
47 | ): Promise => {
48 | try {
49 | const result = await db
50 | .update(chats)
51 | .set(updateData)
52 | .where(eq(chats.id, id))
53 | .returning()
54 | revalidatePath("/")
55 | return result
56 | } catch (error) {
57 | console.error(`Error updating chat with id ${id}:`, error)
58 | throw error
59 | }
60 | }
61 |
62 | export const deleteChat = async (id: string): Promise => {
63 | try {
64 | const result = await db.delete(chats).where(eq(chats.id, id)).returning()
65 | revalidatePath("/")
66 | return result
67 | } catch (error) {
68 | console.error(`Error deleting chat with id ${id}:`, error)
69 | throw error
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/db/queries/messages.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { and, eq, gte, inArray } from "drizzle-orm"
4 | import { revalidatePath } from "next/cache"
5 | import { db } from "../db"
6 | import { InsertMessage, SelectMessage, messages } from "../schema/messages"
7 |
8 | export const getMessagesByChatId = async (
9 | chatId: string
10 | ): Promise => {
11 | try {
12 | return await db
13 | .select()
14 | .from(messages)
15 | .where(eq(messages.chatId, chatId))
16 | .orderBy(messages.turn)
17 | } catch (error) {
18 | console.error(`Error fetching messages for chatId ${chatId}:`, error)
19 | throw error
20 | }
21 | }
22 |
23 | export const createMessage = async (
24 | messageData: InsertMessage
25 | ): Promise => {
26 | try {
27 | const result = await db.insert(messages).values(messageData).returning()
28 | revalidatePath("/")
29 | return result[0]
30 | } catch (error) {
31 | console.error("Error creating message:", error)
32 | throw error
33 | }
34 | }
35 |
36 | export const updateMessage = async (
37 | id: string,
38 | updateData: Partial