├── .gitignore
├── LICENSE
├── README.md
├── app
├── api
│ └── gemini
│ │ └── route.ts
├── globals.css
├── layout.tsx
└── page.tsx
├── components.json
├── components
├── api-key-config.tsx
├── footer.tsx
├── rag-eval-demo.tsx
├── response-details-modal.tsx
├── theme-provider.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
│ ├── chart.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
│ ├── sidebar.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-mobile.tsx
│ └── use-toast.ts
├── hooks
├── use-mobile.tsx
└── use-toast.ts
├── lib
└── utils.ts
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── public
├── placeholder-logo.png
├── placeholder-logo.svg
├── placeholder-user.jpg
├── placeholder.jpg
└── placeholder.svg
├── rag-evals-app.tsx
├── styles
└── globals.css
├── tailwind.config.ts
└── tsconfig.json
/.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 | # .env.production
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
40 | .trigger
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Anubhav Singh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RAG Evals App
2 |
3 | A Next.js application for evaluating and visualizing RAG (Retrieval-Augmented Generation) systems. This application provides a user-friendly interface to test and analyze the performance of RAG implementations through various metrics and visualizations.
4 |
5 | This repo goes along with the Python Notebooks at https://github.com/xprilion/gemini-as-a-judge-for-rag-evals
6 |
7 | ## Features
8 |
9 | - Interactive chat interface for testing RAG systems
10 | - Real-time visualization of vector embeddings
11 | - Cosine similarity scoring and ranking
12 | - Comprehensive evaluation tables for:
13 | - Sentence analysis
14 | - Vector representations
15 | - Query information
16 | - Similarity scores
17 |
18 | ## Tech Stack
19 |
20 | - **Framework**: Next.js 15.2.4
21 | - **Language**: TypeScript
22 | - **UI Components**: Radix UI
23 | - **Styling**: Tailwind CSS
24 | - **State Management**: React Hooks
25 | - **Form Handling**: React Hook Form
26 | - **Data Visualization**: Recharts
27 | - **Date Handling**: date-fns
28 | - **AI Integration**: Gemini 2.0
29 |
30 | ## Getting Started
31 |
32 | ### Prerequisites
33 |
34 | - Node.js (v18 or higher)
35 | - pnpm (recommended package manager)
36 |
37 | ### Installation
38 |
39 | 1. Clone the repository:
40 |
41 | ```bash
42 | git clone https://github.com/xprilion/gemini-rag-evals-visualizer-app.git
43 | cd gemini-rag-evals-visualizer-app
44 | ```
45 |
46 | 2. Install dependencies:
47 |
48 | ```bash
49 | pnpm install
50 | ```
51 |
52 | 3. Start the development server:
53 |
54 | ```bash
55 | pnpm dev
56 | ```
57 |
58 | The application will be available at `http://localhost:3000`.
59 |
60 | ## Project Structure
61 |
62 | ```
63 | gemini-rag-evals-visualizer-app/
64 | ├── app/ # Next.js app directory
65 | │ ├── api/ # API routes
66 | │ ├── page.tsx # Main page component
67 | │ └── layout.tsx # Root layout
68 | ├── components/ # Reusable UI components
69 | ├── hooks/ # Custom React hooks
70 | ├── lib/ # Utility functions and configurations
71 | ├── public/ # Static assets
72 | └── styles/ # Global styles
73 | ```
74 |
75 | ## Development
76 |
77 | - `pnpm dev` - Start development server
78 | - `pnpm build` - Build for production
79 | - `pnpm start` - Start production server
80 | - `pnpm lint` - Run ESLint
81 |
82 | ## Contributing
83 |
84 | Contributions are welcome! Please feel free to submit a Pull Request.
85 |
86 | ## License
87 |
88 | This project is licensed under the MIT License - see the LICENSE file for details.
89 |
--------------------------------------------------------------------------------
/app/api/gemini/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest, NextResponse } from "next/server";
2 |
3 | export async function POST(req: NextRequest) {
4 | try {
5 | const { prompt, context, apiKey } = await req.json();
6 |
7 | if (!apiKey) {
8 | return NextResponse.json(
9 | { error: "API key is required" },
10 | { status: 400 }
11 | );
12 | }
13 |
14 | // Prepare the API request
15 | const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
16 |
17 | // Create the request payload
18 | const payload = {
19 | contents: [
20 | {
21 | parts: [
22 | {
23 | text: `Context: ${context}\n\nUser query: ${prompt}`,
24 | },
25 | ],
26 | },
27 | ],
28 | generationConfig: {
29 | temperature: 0.7,
30 | maxOutputTokens: 800,
31 | },
32 | };
33 |
34 | // Make the direct API call
35 | const response = await fetch(url, {
36 | method: "POST",
37 | headers: {
38 | "Content-Type": "application/json",
39 | },
40 | body: JSON.stringify(payload),
41 | });
42 |
43 | if (!response.ok) {
44 | throw new Error(`API request failed with status ${response.status}`);
45 | }
46 |
47 | const data = await response.json();
48 | const text = data.candidates[0].content.parts[0].text;
49 |
50 | return NextResponse.json({ text });
51 | } catch (error) {
52 | console.error("Error in Gemini API:", error);
53 | return NextResponse.json(
54 | { error: "Failed to generate response" },
55 | { status: 500 }
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/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: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 221.2 83.2% 53.3%;
14 | --primary-foreground: 210 40% 98%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 221.2 83.2% 53.3%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark {
30 | --background: 222.2 84% 4.9%;
31 | --foreground: 210 40% 98%;
32 | --card: 222.2 84% 4.9%;
33 | --card-foreground: 210 40% 98%;
34 | --popover: 222.2 84% 4.9%;
35 | --popover-foreground: 210 40% 98%;
36 | --primary: 217.2 91.2% 59.8%;
37 | --primary-foreground: 222.2 47.4% 11.2%;
38 | --secondary: 217.2 32.6% 17.5%;
39 | --secondary-foreground: 210 40% 98%;
40 | --muted: 217.2 32.6% 17.5%;
41 | --muted-foreground: 215 20.2% 65.1%;
42 | --accent: 217.2 32.6% 17.5%;
43 | --accent-foreground: 210 40% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 210 40% 98%;
46 | --border: 217.2 32.6% 17.5%;
47 | --input: 217.2 32.6% 17.5%;
48 | --ring: 224.3 76.3% 48%;
49 | }
50 | }
51 |
52 | @layer base {
53 | * {
54 | @apply border-border;
55 | }
56 | body {
57 | @apply bg-background text-foreground;
58 | }
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react"
2 | import "@/app/globals.css"
3 | import { Inter } from "next/font/google"
4 | import { ThemeProvider } from "@/components/theme-provider"
5 |
6 | const inter = Inter({ subsets: ["latin"] })
7 |
8 | export const metadata = {
9 | title: "RAG Evaluation Demo",
10 | description: "Interactive tool for demonstrating Retrieval Augmented Generation",
11 | generator: 'v0.dev'
12 | }
13 |
14 | export default function RootLayout({
15 | children,
16 | }: {
17 | children: React.ReactNode
18 | }) {
19 | return (
20 |
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 | )
28 | }
29 |
30 |
31 |
32 | import './globals.css'
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import RagEvalDemo from "@/components/rag-eval-demo";
2 | import { ThemeToggle } from "@/components/theme-provider";
3 | import { Footer } from "@/components/footer";
4 |
5 | export default function Home() {
6 | return (
7 |
8 |
9 |
10 |
RAG Evals Demo
11 |
12 |
13 |
14 | This interactive tool demonstrates how Retrieval Augmented Generation
15 | (RAG) works by visualizing the process of vectorization, similarity
16 | scoring, and response generation.
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/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 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/components/api-key-config.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { Button } from "@/components/ui/button";
3 | import { Input } from "@/components/ui/input";
4 | import { Label } from "@/components/ui/label";
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogHeader,
9 | DialogTitle,
10 | DialogTrigger,
11 | } from "@/components/ui/dialog";
12 | import { Settings2 } from "lucide-react";
13 |
14 | interface ApiKeyConfigProps {
15 | children?: React.ReactNode;
16 | }
17 |
18 | export function ApiKeyConfig({ children }: ApiKeyConfigProps) {
19 | const [apiKey, setApiKey] = useState("");
20 | const [isOpen, setIsOpen] = useState(false);
21 |
22 | useEffect(() => {
23 | const savedKey = localStorage.getItem("geminiApiKey");
24 | if (savedKey) {
25 | setApiKey(savedKey);
26 | }
27 | }, []);
28 |
29 | const handleSave = () => {
30 | localStorage.setItem("geminiApiKey", apiKey);
31 | setIsOpen(false);
32 | };
33 |
34 | return (
35 |
36 |
37 | {children || (
38 |
39 |
40 |
41 | )}
42 |
43 |
44 |
45 | Configure Gemini API Key
46 |
47 |
48 |
49 | API Key
50 | setApiKey(e.target.value)}
55 | placeholder="Enter your Gemini API key"
56 | />
57 |
58 |
59 | Save
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Github } from "lucide-react";
2 | import Link from "next/link";
3 |
4 | export function Footer() {
5 | return (
6 |
7 |
8 |
9 |
10 | Built by{" "}
11 |
17 | xprilion
18 |
19 | . The source code is available on{" "}
20 |
26 | GitHub
27 |
28 | .
29 |
30 |
31 |
32 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/components/response-details-modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogContent,
4 | DialogHeader,
5 | DialogTitle,
6 | } from "@/components/ui/dialog";
7 | import { ScrollArea } from "@/components/ui/scroll-area";
8 | import { Badge } from "@/components/ui/badge";
9 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
10 |
11 | interface ResponseDetails {
12 | userQuery: string;
13 | matchedSentences: string[];
14 | matchedIndexes: number[];
15 | finalPrompt: string;
16 | }
17 |
18 | interface EvaluationComparison {
19 | trueIndexes: number[];
20 | predictedIndexes: number[];
21 | intersection: number[];
22 | precision: number;
23 | recall: number;
24 | f1: number;
25 | }
26 |
27 | interface ResponseDetailsModalProps {
28 | isOpen: boolean;
29 | onClose: () => void;
30 | details: ResponseDetails;
31 | evaluationComparison?: EvaluationComparison;
32 | }
33 |
34 | export function ResponseDetailsModal({
35 | isOpen,
36 | onClose,
37 | details,
38 | evaluationComparison,
39 | }: ResponseDetailsModalProps) {
40 | return (
41 |
42 |
43 |
44 | Response Details
45 |
46 |
47 |
48 |
49 |
50 | User Query
51 |
52 |
53 | {details.userQuery}
54 |
55 |
56 |
57 |
58 |
59 | Matched Sentences
60 |
61 |
62 |
63 | {details.matchedSentences.map((sentence, index) => (
64 |
65 |
66 | {details.matchedIndexes[index] + 1}
67 |
68 |
{sentence}
69 |
70 | ))}
71 |
72 |
73 |
74 |
75 | {evaluationComparison && (
76 |
77 |
78 | Evaluation Results
79 |
80 |
81 |
82 |
83 |
True Matches
84 |
85 | {evaluationComparison.trueIndexes.map((index) => (
86 |
94 | {index}
95 |
96 | ))}
97 |
98 |
99 |
100 |
Predicted Matches
101 |
102 | {evaluationComparison.predictedIndexes.map((index) => (
103 |
111 | {index + 1}
112 |
113 | ))}
114 |
115 |
116 |
117 |
118 |
Precision
119 |
120 | {(evaluationComparison.precision * 100).toFixed(1)}%
121 |
122 |
123 |
124 |
Recall
125 |
126 | {(evaluationComparison.recall * 100).toFixed(1)}%
127 |
128 |
129 |
130 |
F1 Score
131 |
132 | {(evaluationComparison.f1 * 100).toFixed(1)}%
133 |
134 |
135 |
136 |
137 |
138 |
139 | )}
140 |
141 |
142 |
143 | Final Prompt
144 |
145 |
146 |
147 | {details.finalPrompt}
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import {
5 | ThemeProvider as NextThemesProvider,
6 | type ThemeProviderProps,
7 | } from "next-themes";
8 | import { Moon, Sun } from "lucide-react";
9 | import { Button } from "@/components/ui/button";
10 | import { useTheme } from "next-themes";
11 |
12 | export function ThemeToggle() {
13 | const { theme, setTheme } = useTheme();
14 |
15 | return (
16 | setTheme(theme === "light" ? "dark" : "light")}
20 | >
21 |
22 |
23 | Toggle theme
24 |
25 | );
26 | }
27 |
28 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
29 | return {children} ;
30 | }
31 |
--------------------------------------------------------------------------------
/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 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
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 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
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]:w-3.5 [&>svg]:h-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 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
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 border-input bg-background hover:bg-accent hover:text-accent-foreground",
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: "h-10 w-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 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLDivElement,
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 |
219 |
220 | Previous slide
221 |
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 |
248 |
249 | Next slide
250 |
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/chart.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RechartsPrimitive from "recharts"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | // Format: { THEME_NAME: CSS_SELECTOR }
9 | const THEMES = { light: "", dark: ".dark" } as const
10 |
11 | export type ChartConfig = {
12 | [k in string]: {
13 | label?: React.ReactNode
14 | icon?: React.ComponentType
15 | } & (
16 | | { color?: string; theme?: never }
17 | | { color?: never; theme: Record }
18 | )
19 | }
20 |
21 | type ChartContextProps = {
22 | config: ChartConfig
23 | }
24 |
25 | const ChartContext = React.createContext(null)
26 |
27 | function useChart() {
28 | const context = React.useContext(ChartContext)
29 |
30 | if (!context) {
31 | throw new Error("useChart must be used within a ")
32 | }
33 |
34 | return context
35 | }
36 |
37 | const ChartContainer = React.forwardRef<
38 | HTMLDivElement,
39 | React.ComponentProps<"div"> & {
40 | config: ChartConfig
41 | children: React.ComponentProps<
42 | typeof RechartsPrimitive.ResponsiveContainer
43 | >["children"]
44 | }
45 | >(({ id, className, children, config, ...props }, ref) => {
46 | const uniqueId = React.useId()
47 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
48 |
49 | return (
50 |
51 |
60 |
61 |
62 | {children}
63 |
64 |
65 |
66 | )
67 | })
68 | ChartContainer.displayName = "Chart"
69 |
70 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71 | const colorConfig = Object.entries(config).filter(
72 | ([_, config]) => config.theme || config.color
73 | )
74 |
75 | if (!colorConfig.length) {
76 | return null
77 | }
78 |
79 | return (
80 |