diff --git a/Dockerfile b/Dockerfile index ce7b74e59a5eb9bbe9a381bb228fd65569365dfa..ed056f151a10e4b4aaa6eab849207bb2020bad40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,34 @@ +# Use Python base FROM python:3.10-slim WORKDIR /app -COPY requirements.txt . +# Copy backend & model +COPY main.py model.pt requirements.txt /app/ -RUN pip install --upgrade pip +# Copy frontend +COPY UI/ /app/UI/ + +# System deps +RUN apt-get update && apt-get install -y curl build-essential gnupg && rm -rf /var/lib/apt/lists/* + +# Install Node.js (v20 LTS) + pnpm +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g pnpm + +# Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt -COPY . . +# Install frontend dependencies & build +WORKDIR /app/UI +RUN pnpm install +RUN pnpm build +# Hugging Face expects 7860 as default port for the UI EXPOSE 7860 +EXPOSE 8000 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file +# Run both frontend & backend +WORKDIR /app +CMD ["bash", "-c", "cd UI && pnpm start -- -p 7860 & uvicorn main:app --host 0.0.0.0 --port 8000"] \ No newline at end of file diff --git a/README.md b/README.md index 03f0c146a19f7fdeb970e33c30eec1b9fa3b2745..db697a77e36c0e02d5cbf970a9e34a515efde7fd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ --- title: Numera emoji: πŸ”’ -colorFrom: indigo -colorTo: purple +colorFrom: green +colorTo: teal sdk: docker sdk_version: "1.0" app_file: main.py -pinned: false ---- \ No newline at end of file +pinned: true +--- + diff --git a/UI/.gitignore b/UI/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..2129939708a15c74d2fba770c6929b559f6382b0 --- /dev/null +++ b/UI/.gitignore @@ -0,0 +1,10 @@ +# v0 runtime files +__v0_runtime_loader.js +__v0_devtools.tsx +__v0_jsx-dev-runtime.ts + +# Common ignores +node_modules/ +.next/ +.env*.local +.DS_Store \ No newline at end of file diff --git a/UI/app/api/predict/route.ts b/UI/app/api/predict/route.ts new file mode 100644 index 0000000000000000000000000000000000000000..57256cf5fc70e16acd86bb950888d812163ca834 --- /dev/null +++ b/UI/app/api/predict/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server" + +const FASTAPI_URL = process.env.FASTAPI_URL || "http://localhost:8000" + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData() + const file = formData.get("file") as Blob | null + + if (!file) { + return NextResponse.json( + { error: "No file provided" }, + { status: 400 } + ) + } + + // Forward the file to FastAPI backend + const fastApiForm = new FormData() + fastApiForm.append("file", file, "digit.png") + + const response = await fetch(`${FASTAPI_URL}/predict`, { + method: "POST", + body: fastApiForm, + }) + + if (!response.ok) { + const errorText = await response.text() + console.error("FastAPI error:", errorText) + return NextResponse.json( + { error: "Prediction failed" }, + { status: response.status } + ) + } + + const data = await response.json() + + return NextResponse.json(data) + } catch (error) { + console.error("Prediction error:", error) + return NextResponse.json( + { error: "Failed to connect to prediction server" }, + { status: 500 } + ) + } +} diff --git a/UI/app/globals.css b/UI/app/globals.css new file mode 100644 index 0000000000000000000000000000000000000000..c58c320f1107aef83338ab73c906ec34bed862ba --- /dev/null +++ b/UI/app/globals.css @@ -0,0 +1,92 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: #080909; + --foreground: #e5e7eb; + --card: #111314; + --card-foreground: #e5e7eb; + --popover: #111314; + --popover-foreground: #e5e7eb; + --primary: #a8f0a0; + --primary-foreground: #080909; + --secondary: #1a1d1e; + --secondary-foreground: #e5e7eb; + --muted: #1a1d1e; + --muted-foreground: #9ca3af; + --accent: #a8f0a0; + --accent-foreground: #080909; + --destructive: #ef4444; + --destructive-foreground: #fef2f2; + --border: #2a2d2e; + --input: #1a1d1e; + --ring: #a8f0a0; + --chart-1: #a8f0a0; + --chart-2: #6ee7b7; + --chart-3: #34d399; + --chart-4: #10b981; + --chart-5: #059669; + --radius: 0.75rem; + --sidebar: #111314; + --sidebar-foreground: #e5e7eb; + --sidebar-primary: #a8f0a0; + --sidebar-primary-foreground: #080909; + --sidebar-accent: #1a1d1e; + --sidebar-accent-foreground: #e5e7eb; + --sidebar-border: #2a2d2e; + --sidebar-ring: #a8f0a0; + --neon: #a8f0a0; + --canvas: #0a0b0b; +} + +@theme inline { + --font-sans: 'Geist', 'Geist Fallback'; + --font-mono: 'Geist Mono', 'Geist Mono Fallback'; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/UI/app/layout.tsx b/UI/app/layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..19bbf05940cedff504d586a77609fde6e1c26499 --- /dev/null +++ b/UI/app/layout.tsx @@ -0,0 +1,67 @@ +import type { Metadata, Viewport } from 'next' +import { Instrument_Sans, Instrument_Serif, JetBrains_Mono } from 'next/font/google' +import { Analytics } from '@vercel/analytics/next' +import './globals.css' + +const instrumentSans = Instrument_Sans({ + subsets: ["latin"], + variable: '--font-sans', + display: 'swap', +}) + +const instrumentSerif = Instrument_Serif({ + subsets: ["latin"], + weight: "400", + variable: '--font-serif', + display: 'swap', +}) + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + variable: '--font-mono', + display: 'swap', +}) + +export const metadata: Metadata = { + title: 'Numera Β· Real-time Digit Recognition', + description: 'Draw a digit and watch our PyTorch model recognize it in real-time. Built with MNIST dataset.', + generator: 'v0.app', + icons: { + icon: [ + { + url: '/icon-light-32x32.png', + media: '(prefers-color-scheme: light)', + }, + { + url: '/icon-dark-32x32.png', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/icon.svg', + type: 'image/svg+xml', + }, + ], + apple: '/apple-icon.png', + }, +} + +export const viewport: Viewport = { + themeColor: '#080909', + width: 'device-width', + initialScale: 1, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/UI/app/page.tsx b/UI/app/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f19957c788be245b5c202def68639089529390b5 --- /dev/null +++ b/UI/app/page.tsx @@ -0,0 +1,71 @@ +import { Navigation } from "@/components/numera/navigation" +import { HeroGrid } from "@/components/numera/hero-grid" +import { AppCard } from "@/components/numera/app-card" +import { Footer } from "@/components/numera/footer" + +export default function Home() { + return ( +
+ + +
+ {/* Hero Section */} +
+
+
+ {/* Left - Content */} +
+

+ Real-time digit recognition +

+

+ Draw any digit from 0-9 and watch our neural network classify it in real-time. + Powered by PyTorch and trained on the MNIST dataset. +

+
+
+ 99% + Accuracy +
+
+ 60K + Training samples +
+
+ {"<"}50ms + Inference +
+
+
+ + {/* Right - Hero Grid (desktop only) */} +
+ +
+
+
+
+ + {/* Section Divider */} +
+
+
+ + Live classifier + +
+
+
+ + {/* App Card Section */} +
+
+ +
+
+
+ +
+
+ ) +} diff --git a/UI/components.json b/UI/components.json new file mode 100644 index 0000000000000000000000000000000000000000..4ee62ee10547066d7ee70f94d79c786226c3c12c --- /dev/null +++ b/UI/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/UI/components/numera/app-card.tsx b/UI/components/numera/app-card.tsx new file mode 100644 index 0000000000000000000000000000000000000000..48f4272bc60075236b507ea25a28e02f62068836 --- /dev/null +++ b/UI/components/numera/app-card.tsx @@ -0,0 +1,110 @@ +"use client" + +import { useState, useRef, useCallback } from "react" +import { DrawingCanvas } from "./drawing-canvas" +import { ResultsPanel } from "./results-panel" +import { Loader2 } from "lucide-react" + +interface PredictionResult { + prediction: number + confidence: number + probabilities: Record +} + +export function AppCard() { + const [result, setResult] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [clearTrigger, setClearTrigger] = useState(0) + const canvasRef = useRef(null) + const resultsRef = useRef(null) + + const handleCanvasReady = useCallback((canvas: HTMLCanvasElement) => { + canvasRef.current = canvas + }, []) + + const handleClear = useCallback(() => { + setClearTrigger((prev) => prev + 1) + setResult(null) + }, []) + + const handleRunInference = async () => { + const canvas = canvasRef.current + if (!canvas) return + + setIsLoading(true) + + try { + // Convert canvas to blob + const blob = await new Promise((resolve) => { + canvas.toBlob((b) => resolve(b!), "image/png") + }) + + const formData = new FormData() + formData.append("file", blob, "digit.png") + + const response = await fetch("/api/predict", { + method: "POST", + body: formData, + }) + + if (!response.ok) { + throw new Error("Prediction failed") + } + + const data: PredictionResult = await response.json() + setResult(data) + + // Scroll results into view on mobile + if (window.innerWidth <= 680 && resultsRef.current) { + resultsRef.current.scrollIntoView({ behavior: "smooth", block: "start" }) + } + } catch (error) { + console.error("Inference error:", error) + } finally { + setIsLoading(false) + } + } + + return ( +
+
+ {/* Left Pane - Canvas */} +
+ +
+ + +
+
+ + {/* Right Pane - Results */} +
+ +
+
+
+ ) +} diff --git a/UI/components/numera/drawing-canvas.tsx b/UI/components/numera/drawing-canvas.tsx new file mode 100644 index 0000000000000000000000000000000000000000..544ee37bfc95eaecdb32599c478304b586ad96e4 --- /dev/null +++ b/UI/components/numera/drawing-canvas.tsx @@ -0,0 +1,144 @@ +"use client" + +import { useRef, useEffect, useState, useCallback } from "react" + +interface DrawingCanvasProps { + onCanvasReady: (canvas: HTMLCanvasElement) => void + onClear: () => void + clearTrigger: number +} + +export function DrawingCanvas({ onCanvasReady, clearTrigger }: DrawingCanvasProps) { + const canvasRef = useRef(null) + const containerRef = useRef(null) + const [isDrawing, setIsDrawing] = useState(false) + const [hasDrawn, setHasDrawn] = useState(false) + + const setupCanvas = useCallback(() => { + const canvas = canvasRef.current + const container = containerRef.current + if (!canvas || !container) return + + const rect = container.getBoundingClientRect() + const size = Math.min(rect.width, rect.height) + const dpr = window.devicePixelRatio || 1 + + canvas.width = size * dpr + canvas.height = size * dpr + canvas.style.width = `${size}px` + canvas.style.height = `${size}px` + + const ctx = canvas.getContext("2d") + if (ctx) { + ctx.scale(dpr, dpr) + ctx.fillStyle = "#0a0b0b" + ctx.fillRect(0, 0, size, size) + ctx.strokeStyle = "#ffffff" + ctx.lineWidth = 20 + ctx.lineCap = "round" + ctx.lineJoin = "round" + } + + onCanvasReady(canvas) + }, [onCanvasReady]) + + useEffect(() => { + setupCanvas() + window.addEventListener("resize", setupCanvas) + window.addEventListener("orientationchange", setupCanvas) + return () => { + window.removeEventListener("resize", setupCanvas) + window.removeEventListener("orientationchange", setupCanvas) + } + }, [setupCanvas]) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext("2d") + if (!ctx) return + + const dpr = window.devicePixelRatio || 1 + const size = canvas.width / dpr + ctx.fillStyle = "#0a0b0b" + ctx.fillRect(0, 0, size, size) + setHasDrawn(false) + }, [clearTrigger]) + + const getCoordinates = (e: React.MouseEvent | React.TouchEvent) => { + const canvas = canvasRef.current + if (!canvas) return { x: 0, y: 0 } + + const rect = canvas.getBoundingClientRect() + const clientX = "touches" in e ? e.touches[0]?.clientX ?? 0 : e.clientX + const clientY = "touches" in e ? e.touches[0]?.clientY ?? 0 : e.clientY + + return { + x: clientX - rect.left, + y: clientY - rect.top, + } + } + + const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault() + const canvas = canvasRef.current + const ctx = canvas?.getContext("2d") + if (!ctx) return + + const { x, y } = getCoordinates(e) + ctx.beginPath() + ctx.moveTo(x, y) + ctx.lineTo(x, y) + ctx.stroke() + setIsDrawing(true) + setHasDrawn(true) + } + + const draw = (e: React.MouseEvent | React.TouchEvent) => { + if (!isDrawing) return + e.preventDefault() + + const canvas = canvasRef.current + const ctx = canvas?.getContext("2d") + if (!ctx) return + + const { x, y } = getCoordinates(e) + ctx.lineTo(x, y) + ctx.stroke() + } + + const stopDrawing = () => { + setIsDrawing(false) + } + + return ( +
+
+ Input + Β· + Drawing canvas +
+
+ + {!hasDrawn && ( +
+ Draw a digit (0-9) +
+ )} +
+
+ ) +} diff --git a/UI/components/numera/footer.tsx b/UI/components/numera/footer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b1c96904afd7ca53facf1287edda07a53e23f7a9 --- /dev/null +++ b/UI/components/numera/footer.tsx @@ -0,0 +1,18 @@ +export function Footer() { + return ( +
+
+
+ Numera + Β· + PyTorch + Β· + MNIST +
+
+ Built with β™₯ by Abdul Rafay +
+
+
+ ) +} diff --git a/UI/components/numera/hero-grid.tsx b/UI/components/numera/hero-grid.tsx new file mode 100644 index 0000000000000000000000000000000000000000..90ad7a787e8cb9a12f0a9250574c0772857e5e3e --- /dev/null +++ b/UI/components/numera/hero-grid.tsx @@ -0,0 +1,47 @@ +"use client" + +import { useEffect, useState } from "react" + +export function HeroGrid() { + const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + const [activeIndices, setActiveIndices] = useState([]) + + useEffect(() => { + const animate = () => { + const count = Math.floor(Math.random() * 3) + 3 // 3-5 cells + const indices: number[] = [] + while (indices.length < count) { + const idx = Math.floor(Math.random() * 10) + if (!indices.includes(idx)) { + indices.push(idx) + } + } + setActiveIndices(indices) + } + + animate() + const interval = setInterval(animate, 1800) + return () => clearInterval(interval) + }, []) + + return ( +
+ {digits.map((digit, index) => ( +
+ {digit} +
+ ))} +
+ ) +} diff --git a/UI/components/numera/navigation.tsx b/UI/components/numera/navigation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..edcada28d7961144e1fc6cb90d3f6e92fae85d2a --- /dev/null +++ b/UI/components/numera/navigation.tsx @@ -0,0 +1,28 @@ +"use client" + +import { Github } from "lucide-react" + +export function Navigation() { + return ( + + ) +} diff --git a/UI/components/numera/results-panel.tsx b/UI/components/numera/results-panel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cc11e3d443a4bb171d7c4f97cbe1d6c3a183f3ee --- /dev/null +++ b/UI/components/numera/results-panel.tsx @@ -0,0 +1,93 @@ +"use client" + +import { Loader2 } from "lucide-react" + +interface PredictionResult { + prediction: number + confidence: number + probabilities: Record +} + +interface ResultsPanelProps { + result: PredictionResult | null + isLoading: boolean +} + +export function ResultsPanel({ result, isLoading }: ResultsPanelProps) { + return ( +
+
+ Output + Β· + Prediction & Distribution +
+ +
+ {isLoading ? ( +
+ + Analyzing... +
+ ) : result ? ( +
+ {/* Top Prediction */} +
+ + {result.prediction} + +
+ Confidence: + + {(result.confidence * 100).toFixed(1)}% + +
+ {/* Confidence Bar */} +
+
+
+
+ + {/* Probability Distribution */} +
+ Distribution +
+ {Object.entries(result.probabilities) + .sort(([a], [b]) => parseInt(a) - parseInt(b)) + .map(([digit, prob]) => ( +
+ {digit} +
+
+
+ + {(prob * 100).toFixed(1)}% + +
+ ))} +
+
+
+ ) : ( +
+
+ ? +
+ + Draw a digit and run inference + +
+ )} +
+
+ ) +} diff --git a/UI/components/theme-provider.tsx b/UI/components/theme-provider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..55c2f6eb60b22a313a4c27bd0b2d728063cb8ab9 --- /dev/null +++ b/UI/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/UI/components/ui/accordion.tsx b/UI/components/ui/accordion.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e538a33b9946acb372e2f569a7cb4b22b17cff12 --- /dev/null +++ b/UI/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +'use client' + +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { ChevronDownIcon } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180', + className, + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/UI/components/ui/alert-dialog.tsx b/UI/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9704452664dabbb8cef88fa154c12f81b243120d --- /dev/null +++ b/UI/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +'use client' + +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/UI/components/ui/alert.tsx b/UI/components/ui/alert.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e6751abe6a83bc38bbb422f7fc47a534ec65f459 --- /dev/null +++ b/UI/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/UI/components/ui/aspect-ratio.tsx b/UI/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000000000000000000000000000000000000..40bb1208dbbf471b59cac12b60b992cb3cfd30dd --- /dev/null +++ b/UI/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/UI/components/ui/avatar.tsx b/UI/components/ui/avatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aa98465a30f89336a5205d3298bc5bf836baa1fd --- /dev/null +++ b/UI/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '@/lib/utils' + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/UI/components/ui/badge.tsx b/UI/components/ui/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fc4126b7a6f1f09b1d57018fdf85fea6bd8dd5a3 --- /dev/null +++ b/UI/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/UI/components/ui/breadcrumb.tsx b/UI/components/ui/breadcrumb.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1750ff26a6f95d49cece850873e367f51c0be252 --- /dev/null +++ b/UI/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { ChevronRight, MoreHorizontal } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return