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 (
+
+ )
+}
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
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : 'a'
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/UI/components/ui/button-group.tsx b/UI/components/ui/button-group.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..09d443097b193b4c26cd0f37cb4b1ab2e2c6e47b
--- /dev/null
+++ b/UI/components/ui/button-group.tsx
@@ -0,0 +1,83 @@
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+import { Separator } from '@/components/ui/separator'
+
+const buttonGroupVariants = cva(
+ "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
+ {
+ variants: {
+ orientation: {
+ horizontal:
+ '[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
+ vertical:
+ 'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
+ },
+ },
+ defaultVariants: {
+ orientation: 'horizontal',
+ },
+ },
+)
+
+function ButtonGroup({
+ className,
+ orientation,
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ )
+}
+
+function ButtonGroupText({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'div'> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : 'div'
+
+ return (
+
+ )
+}
+
+function ButtonGroupSeparator({
+ className,
+ orientation = 'vertical',
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ ButtonGroup,
+ ButtonGroupSeparator,
+ ButtonGroupText,
+ buttonGroupVariants,
+}
diff --git a/UI/components/ui/button.tsx b/UI/components/ui/button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f64632d152a02f587eb5d01268c05b26b66e2716
--- /dev/null
+++ b/UI/components/ui/button.tsx
@@ -0,0 +1,60 @@
+import * as React from 'react'
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-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",
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline:
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost:
+ 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9',
+ 'icon-sm': 'size-8',
+ 'icon-lg': 'size-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : 'button'
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/UI/components/ui/calendar.tsx b/UI/components/ui/calendar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eaa373e25f99cd03cf06d7c11bec3fac969ad38d
--- /dev/null
+++ b/UI/components/ui/calendar.tsx
@@ -0,0 +1,213 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from 'lucide-react'
+import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
+
+import { cn } from '@/lib/utils'
+import { Button, buttonVariants } from '@/components/ui/button'
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = 'label',
+ buttonVariant = 'ghost',
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps['variant']
+}) {
+ const defaultClassNames = getDefaultClassNames()
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className,
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString('default', { month: 'short' }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn('w-fit', defaultClassNames.root),
+ months: cn(
+ 'flex gap-4 flex-col md:flex-row relative',
+ defaultClassNames.months,
+ ),
+ month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
+ nav: cn(
+ 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
+ defaultClassNames.nav,
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
+ defaultClassNames.button_previous,
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
+ defaultClassNames.button_next,
+ ),
+ month_caption: cn(
+ 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
+ defaultClassNames.month_caption,
+ ),
+ dropdowns: cn(
+ 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
+ defaultClassNames.dropdowns,
+ ),
+ dropdown_root: cn(
+ 'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
+ defaultClassNames.dropdown_root,
+ ),
+ dropdown: cn(
+ 'absolute bg-popover inset-0 opacity-0',
+ defaultClassNames.dropdown,
+ ),
+ caption_label: cn(
+ 'select-none font-medium',
+ captionLayout === 'label'
+ ? 'text-sm'
+ : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
+ defaultClassNames.caption_label,
+ ),
+ table: 'w-full border-collapse',
+ weekdays: cn('flex', defaultClassNames.weekdays),
+ weekday: cn(
+ 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
+ defaultClassNames.weekday,
+ ),
+ week: cn('flex w-full mt-2', defaultClassNames.week),
+ week_number_header: cn(
+ 'select-none w-(--cell-size)',
+ defaultClassNames.week_number_header,
+ ),
+ week_number: cn(
+ 'text-[0.8rem] select-none text-muted-foreground',
+ defaultClassNames.week_number,
+ ),
+ day: cn(
+ 'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
+ defaultClassNames.day,
+ ),
+ range_start: cn(
+ 'rounded-l-md bg-accent',
+ defaultClassNames.range_start,
+ ),
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
+ range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
+ today: cn(
+ 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
+ defaultClassNames.today,
+ ),
+ outside: cn(
+ 'text-muted-foreground aria-selected:text-muted-foreground',
+ defaultClassNames.outside,
+ ),
+ disabled: cn(
+ 'text-muted-foreground opacity-50',
+ defaultClassNames.disabled,
+ ),
+ hidden: cn('invisible', defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === 'left') {
+ return (
+
+ )
+ }
+
+ if (orientation === 'right') {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+