├── frontend ├── .gitignore ├── postcss.config.js ├── src │ ├── assets │ │ ├── laion-social-graph.png │ │ ├── benchmark-dark-theme.webp │ │ ├── benchmark-light-theme.webp │ │ └── logos │ │ │ ├── Laion-dark.svg │ │ │ └── Laion-light.svg │ ├── ui │ │ ├── lib │ │ │ └── utils.ts │ │ ├── components │ │ │ ├── custom │ │ │ │ ├── GradientCard.tsx │ │ │ │ ├── LoadingScreen.tsx │ │ │ │ ├── Col.tsx │ │ │ │ ├── Grid.tsx │ │ │ │ ├── Row.tsx │ │ │ │ ├── Centered.tsx │ │ │ │ ├── JsonComponent.tsx │ │ │ │ ├── ResponsiveRow.tsx │ │ │ │ ├── FakeH1.tsx │ │ │ │ ├── HomePageBackdrop.tsx │ │ │ │ ├── TooltipContentComponent.tsx │ │ │ │ ├── AlertInfo.tsx │ │ │ │ ├── AlertWarning.tsx │ │ │ │ ├── HeaderComponents.tsx │ │ │ │ ├── SearchInput.tsx │ │ │ │ ├── FeatureCard.tsx │ │ │ │ ├── CommandBlock.tsx │ │ │ │ ├── ThemeToggle.tsx │ │ │ │ ├── StakingDashboardBanner.tsx │ │ │ │ ├── ScoreBadge.tsx │ │ │ │ ├── SelectableCard.tsx │ │ │ │ ├── Code.tsx │ │ │ │ ├── InferenceIcon.tsx │ │ │ │ ├── WorkerLogsTerminal.tsx │ │ │ │ └── CodeBlock.tsx │ │ │ └── ui │ │ │ │ ├── Skeleton.tsx │ │ │ │ ├── Spinner.tsx │ │ │ │ ├── SeparatorBorder.tsx │ │ │ │ ├── Separator.tsx │ │ │ │ ├── Label.tsx │ │ │ │ ├── Slider.tsx │ │ │ │ ├── Popover.tsx │ │ │ │ ├── ScaleLoader.tsx │ │ │ │ ├── Avatar.tsx │ │ │ │ ├── Badge.tsx │ │ │ │ ├── Textarea.tsx │ │ │ │ ├── Tooltip.tsx │ │ │ │ ├── Input.tsx │ │ │ │ ├── Checkbox.tsx │ │ │ │ ├── Toaster.tsx │ │ │ │ ├── Switch.tsx │ │ │ │ ├── Tabs.tsx │ │ │ │ ├── Accordion.tsx │ │ │ │ ├── Calendar.tsx │ │ │ │ ├── Alert.tsx │ │ │ │ ├── Breadcrumb.tsx │ │ │ │ ├── Table.tsx │ │ │ │ ├── Card.tsx │ │ │ │ ├── Button.tsx │ │ │ │ ├── AlertDialog.tsx │ │ │ │ ├── Sheet.tsx │ │ │ │ └── Toast.tsx │ │ ├── hooks │ │ │ ├── useHasMounted.hook.ts │ │ │ ├── useBreakpoints.hook.ts │ │ │ └── useToast.hook.ts │ │ ├── utils │ │ │ └── getThemeColor.ts │ │ ├── index.tsx │ │ └── providers │ │ │ └── ThemeProvider.tsx │ ├── vite-env.d.ts │ ├── lib │ │ ├── models.ts │ │ ├── ui-shared.tsx │ │ └── ui-client-utils.ts │ ├── state │ │ └── chartDataCache.ts │ ├── types │ │ ├── assets.d.ts │ │ └── index.ts │ ├── components │ │ ├── LearnMoreContent.tsx │ │ ├── LaionHotKeys.tsx │ │ ├── DistributionChart.tsx │ │ └── LearnMoreSheet.tsx │ ├── main.tsx │ ├── index.css │ ├── utils │ │ ├── routeMapping.ts │ │ └── api.ts │ └── styles │ │ └── fonts │ │ └── inter.css ├── .prettierignore ├── prettier.config.cjs ├── vite.config.ts ├── tsconfig.json ├── index.html ├── eslint.config.js └── package.json ├── backend ├── .gitignore ├── .dev.vars.example ├── download_db.sh ├── wrangler.toml ├── pyproject.toml ├── import-to-local-d1.sh └── src │ ├── models.py │ └── cache_generator.py ├── .github └── workflows │ └── pr-checks.yml ├── LICENSE ├── .vscode └── settings.json ├── .gitignore ├── README.md └── Taskfile.yml /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | api/cache/ 3 | 4 | .env.production -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .venv-workers 3 | node_modules 4 | python_modules 5 | 6 | 7 | data/ 8 | .wrangler/ -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/src/assets/laion-social-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/context-labs/aella-data-explorer/HEAD/frontend/src/assets/laion-social-graph.png -------------------------------------------------------------------------------- /frontend/src/assets/benchmark-dark-theme.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/context-labs/aella-data-explorer/HEAD/frontend/src/assets/benchmark-dark-theme.webp -------------------------------------------------------------------------------- /frontend/src/assets/benchmark-light-theme.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/context-labs/aella-data-explorer/HEAD/frontend/src/assets/benchmark-light-theme.webp -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | **/build 2 | **/public 3 | **/dist 4 | **/.output 5 | **/.vercel 6 | **/.vinxi 7 | .DS_Store 8 | **/inter.css 9 | **/jetbrains.css 10 | **/helmfile.yaml 11 | .claude -------------------------------------------------------------------------------- /frontend/src/ui/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from "clsx"; 2 | import { clsx } from "clsx"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_API_URL?: string; 5 | // Add other env variables as needed 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/GradientCard.tsx: -------------------------------------------------------------------------------- 1 | export function GradientCard() { 2 | return ( 3 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/ui/hooks/useHasMounted.hook.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useHasMounted() { 4 | const [hasMounted, setHasMounted] = useState(false); 5 | 6 | useEffect(() => { 7 | setHasMounted(true); 8 | }, []); 9 | 10 | return hasMounted; 11 | } 12 | -------------------------------------------------------------------------------- /backend/.dev.vars.example: -------------------------------------------------------------------------------- 1 | # Local development environment variables for Wrangler 2 | # Copy this file to .dev.vars and update with your local paths 3 | # These are only used in local dev mode (wrangler dev) 4 | 5 | # Absolute path to the local SQLite database 6 | # Example: /Users/yourname/Documents/laion-data-explorer/backend/data/db.sqlite 7 | LOCAL_DB_PATH= 8 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /frontend/src/ui/utils/getThemeColor.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeColorName } from "../constants/ColorSystem"; 2 | import { ColorSystem } from "../constants/ColorSystem"; 3 | 4 | export function getThemeColor( 5 | resolvedTheme: "dark" | "light", 6 | themeColorName: ThemeColorName, 7 | ) { 8 | return ColorSystem.TailwindThemeColors[resolvedTheme][themeColorName]; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import { Centered } from "~/ui/components/custom/Centered"; 2 | import { ScaleLoader } from "~/ui/components/ui/ScaleLoader"; 3 | 4 | export function LoadingScreen() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | module.exports = { 3 | semi: true, 4 | trailingComma: "all", 5 | singleQuote: false, 6 | printWidth: 80, 7 | tabWidth: 2, 8 | endOfLine: "auto", 9 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 10 | embeddedLanguageFormatting: "off", 11 | importOrderParserPlugins: ["typescript", "jsx"], 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | import { Loader2Icon } from "lucide-react"; 3 | 4 | function Spinner({ className, ...props }: React.ComponentProps<"svg">) { 5 | return ( 6 | 12 | ); 13 | } 14 | 15 | export { Spinner }; 16 | -------------------------------------------------------------------------------- /backend/download_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Download database from R2 5 | R2_URL="https://laion-data-assets.inference.net/db.sqlite" 6 | DB_PATH="data/db.sqlite" 7 | 8 | echo "==> Downloading database from R2..." 9 | echo " Source: ${R2_URL}" 10 | echo " Target: ${DB_PATH}" 11 | 12 | # Create data directory if it doesn't exist 13 | mkdir -p data 14 | 15 | # Download the database 16 | curl -L -o "${DB_PATH}" "${R2_URL}" 17 | 18 | echo "==> Database downloaded successfully!" 19 | echo " Location: ${DB_PATH}" 20 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/Col.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | import React from "react"; 3 | 4 | type ColProps = React.HtmlHTMLAttributes & { 5 | children: React.ReactNode; 6 | }; 7 | 8 | export const Col = React.forwardRef( 9 | ({ children, className, ...rest }, ref) => { 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | }, 16 | ); 17 | 18 | Col.displayName = "Col"; 19 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/Grid.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | import React from "react"; 3 | 4 | type GridProps = React.HtmlHTMLAttributes & { 5 | children: React.ReactNode; 6 | }; 7 | 8 | export const Grid = React.forwardRef( 9 | ({ children, className, ...rest }, ref) => { 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | }, 16 | ); 17 | 18 | Grid.displayName = "Grid"; 19 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/Row.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | import React from "react"; 3 | 4 | type RowProps = React.HtmlHTMLAttributes & { 5 | children: React.ReactNode; 6 | }; 7 | 8 | export const Row = React.forwardRef( 9 | ({ children, className, ...rest }, ref) => { 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | }, 16 | ); 17 | 18 | Row.displayName = "Row"; 19 | -------------------------------------------------------------------------------- /frontend/src/lib/models.ts: -------------------------------------------------------------------------------- 1 | // Stub for @kuzco/models 2 | // This file contains minimal stubs to replace imports from the monorepo @kuzco/models package 3 | 4 | export enum LogLevel { 5 | Debug = 0, 6 | Info = 1, 7 | Warn = 2, 8 | Error = 3, 9 | Fatal = 4, 10 | } 11 | 12 | export interface LogMessage { 13 | timestamp: string; 14 | level: LogLevel; 15 | message: string; 16 | header: string; 17 | } 18 | 19 | export const LINKS = { 20 | INFERENCE_DEVNET_STAKING_PROTOCOL_DOCUMENTATION: 21 | "https://docs.example.com/staking", 22 | // Add other links as needed 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/Centered.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | import React from "react"; 3 | 4 | type CenteredProps = React.HtmlHTMLAttributes & { 5 | children: React.ReactNode; 6 | }; 7 | 8 | export const Centered = React.forwardRef( 9 | ({ children, className, ...rest }, ref) => { 10 | return ( 11 |
16 | {children} 17 |
18 | ); 19 | }, 20 | ); 21 | 22 | Centered.displayName = "Centered"; 23 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/SeparatorBorder.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 2 | import { cn } from "~/ui/lib/utils"; 3 | import * as React from "react"; 4 | 5 | const SeparatorBorder = React.forwardRef< 6 | React.ComponentRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 14 | )); 15 | SeparatorBorder.displayName = "SeparatorBorder"; 16 | 17 | export { SeparatorBorder }; 18 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/JsonComponent.tsx: -------------------------------------------------------------------------------- 1 | import JsonView from "@uiw/react-json-view"; 2 | import { lightTheme } from "@uiw/react-json-view/light"; 3 | import { vscodeTheme } from "@uiw/react-json-view/vscode"; 4 | import { useTheme } from "~/ui/providers/ThemeProvider"; 5 | 6 | type JsonComponentProps = { 7 | data: object | null | undefined; 8 | }; 9 | 10 | export function JsonComponent({ data }: JsonComponentProps) { 11 | const { isDarkTheme } = useTheme(); 12 | if (data == null) { 13 | return

No data present.

; 14 | } 15 | return ( 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/state/chartDataCache.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import { atomFamily } from "jotai/utils"; 3 | import type { ClusterInfo, ClusterTemporalData } from "../types"; 4 | 5 | // Cache for cluster distribution data 6 | export const clustersDataAtom = atom(null); 7 | 8 | // Cache family for temporal data by year range 9 | // Key format: "minYear-maxYear" 10 | export const temporalDataAtomFamily = atomFamily((_yearRange: string) => 11 | atom(null), 12 | ); 13 | 14 | // Helper to create year range key 15 | export function createYearRangeKey(minYear: number, maxYear: number): string { 16 | return `${minYear}-${maxYear}`; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/ResponsiveRow.tsx: -------------------------------------------------------------------------------- 1 | import { Col } from "~/ui/components/custom/Col"; 2 | import { cn } from "~/ui/lib/utils"; 3 | import React from "react"; 4 | 5 | type ResponsiveRowProps = React.HtmlHTMLAttributes & { 6 | children: React.ReactNode; 7 | }; 8 | 9 | export const ResponsiveRow = React.forwardRef< 10 | HTMLDivElement, 11 | ResponsiveRowProps 12 | >(({ children, className, ...rest }, ref) => { 13 | return ( 14 | 26 | {children} 27 | 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | import { nodePolyfills } from "vite-plugin-node-polyfills"; 4 | import tsConfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | nodePolyfills({ 9 | include: ["buffer", "process"], 10 | }), 11 | tsConfigPaths({ 12 | projects: ["./tsconfig.json"], 13 | }), 14 | react(), 15 | ], 16 | server: { 17 | host: true, // Expose on local network 18 | port: 5173, 19 | allowedHosts: ["localhost", "127.0.0.1", "0.0.0.0"], 20 | proxy: { 21 | "/api": { 22 | target: "http://localhost:8787", 23 | changeOrigin: true, 24 | }, 25 | }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /backend/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "laion-api" 2 | main = "src/worker.py" 3 | compatibility_date = "2025-10-09" 4 | compatibility_flags = ["python_workers", "python_dedicated_snapshot"] 5 | 6 | [vars] 7 | R2_ASSETS_URL = "https://laion-data-assets.inference.net" 8 | 9 | [env.production.vars] 10 | R2_ASSETS_URL = "https://laion-data-assets.inference.net" 11 | 12 | # D1 database binding 13 | # For local dev, this creates a local SQLite database in .wrangler/state/ 14 | # For production, this points to the remote D1 database 15 | [[d1_databases]] 16 | binding = "LAION_DB" 17 | database_name = "laion-data-exploration" 18 | database_id = "48ad4777-ba9c-418b-be05-904dca96a7bf" 19 | 20 | [observability] 21 | [observability.logs] 22 | enabled = true 23 | head_sampling_rate = 1 24 | invocation_logs = true 25 | persist = true 26 | -------------------------------------------------------------------------------- /frontend/src/types/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*.png" { 7 | const content: string; 8 | export default content; 9 | } 10 | 11 | declare module "*.jpg" { 12 | const content: string; 13 | export default content; 14 | } 15 | 16 | declare module "*.jpeg" { 17 | const content: string; 18 | export default content; 19 | } 20 | 21 | declare module "*.gif" { 22 | const content: string; 23 | export default content; 24 | } 25 | 26 | declare module "*.webp" { 27 | const content: string; 28 | export default content; 29 | } 30 | 31 | declare module "*.json" { 32 | const value: unknown; 33 | export default value; 34 | } 35 | 36 | declare module "*.css" { 37 | const content: Record; 38 | export default content; 39 | } 40 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "laion-api" 3 | version = "1.0.0" 4 | description = "LAION Paper Visualizer API - Python Worker for Cloudflare" 5 | requires-python = ">=3.11" 6 | 7 | dependencies = ["fastapi>=0.110.0", "pydantic>=2.6.0", "webtypy>=0.1.7"] 8 | 9 | [dependency-groups] 10 | dev = ["ruff>=0.14.1", "workers-py"] 11 | 12 | [tool.ruff] 13 | line-length = 200 14 | target-version = "py311" 15 | 16 | [tool.ruff.lint] 17 | select = [ 18 | "E", # pycodestyle errors 19 | "W", # pycodestyle warnings 20 | "F", # pyflakes 21 | "I", # isort 22 | "N", # pep8-naming 23 | "UP", # pyupgrade 24 | "B", # flake8-bugbear 25 | "C4", # flake8-comprehensions 26 | "SIM", # flake8-simplify 27 | ] 28 | ignore = [] 29 | 30 | [tool.ruff.format] 31 | quote-style = "double" 32 | indent-style = "space" 33 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/FakeH1.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | 3 | // This className has custom CSS applied to it in our global stylesheet. 4 | const FAKE_H1_CLASS_NAME = "fake-h1"; 5 | 6 | type FakeH1Props = { 7 | children: React.ReactNode; 8 | style?: React.CSSProperties; 9 | className?: string; 10 | }; 11 | 12 | /** 13 | * This is an H2 tag which specific styling overrides to match our H1 tag 14 | * styles. This is to avoid rendering multiple H1 tags on a single page 15 | * (which is problematic for HTML semantics/SEO), while allowing us to 16 | * still have multiple heading titles which appear like H1s. 17 | */ 18 | export function FakeH1({ children, className, style }: FakeH1Props) { 19 | return ( 20 |

21 | {children} 22 |

23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "composite": true, 9 | "emitDeclarationOnly": true, 10 | "baseUrl": ".", 11 | "outDir": "dist", 12 | "paths": { 13 | "~/ui": ["./src/ui/index.tsx"], 14 | "~/ui/*": ["./src/ui/*"], 15 | "~/*": ["src/*"] 16 | }, 17 | "moduleResolution": "bundler", 18 | "allowImportingTsExtensions": true, 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true 27 | }, 28 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json", "vite.config.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/HomePageBackdrop.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "~/ui/providers/ThemeProvider"; 2 | 3 | export function HomePageBackdrop() { 4 | const { resolvedTheme: theme } = useTheme(); 5 | 6 | if (theme !== "dark") { 7 | return null; 8 | } 9 | 10 | return ( 11 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 2 | import { cn } from "~/ui/lib/utils"; 3 | import * as React from "react"; 4 | 5 | const Separator = React.forwardRef< 6 | React.ComponentRef, 7 | React.ComponentPropsWithoutRef 8 | >( 9 | ( 10 | { className, decorative = true, orientation = "horizontal", ...props }, 11 | ref, 12 | ) => ( 13 | 24 | ), 25 | ); 26 | Separator.displayName = SeparatorPrimitive.Root.displayName; 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from "@radix-ui/react-label"; 2 | import { cn } from "~/ui/lib/utils"; 3 | import type { VariantProps } from "class-variance-authority"; 4 | import { cva } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | const labelVariants = cva( 8 | ` 9 | text-sm font-medium leading-none 10 | 11 | peer-disabled:cursor-not-allowed peer-disabled:opacity-70 12 | `, 13 | ); 14 | 15 | const Label = React.forwardRef< 16 | React.ComponentRef, 17 | React.ComponentPropsWithoutRef & 18 | VariantProps 19 | >(({ className, ...props }, ref) => ( 20 | 25 | )); 26 | Label.displayName = LabelPrimitive.Root.displayName; 27 | 28 | export { Label }; 29 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install Task 17 | uses: arduino/setup-task@v2 18 | with: 19 | version: 3.x 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.11' 25 | 26 | - name: Install uv 27 | uses: astral-sh/setup-uv@v4 28 | with: 29 | version: "latest" 30 | 31 | - name: Setup Bun 32 | uses: oven-sh/setup-bun@v2 33 | with: 34 | bun-version: latest 35 | 36 | - name: Install backend dependencies 37 | run: task backend:setup 38 | 39 | - name: Install frontend dependencies 40 | run: task frontend:setup 41 | 42 | - name: Run checks (formatting, linting, TypeScript) 43 | run: task check 44 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/TooltipContentComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Col } from "~/ui/components/custom/Col"; 2 | import { cn } from "~/ui/lib/utils"; 3 | import type React from "react"; 4 | 5 | type TooltipContentComponentProps = { 6 | content: React.ReactNode | string[]; 7 | title: React.ReactNode; 8 | className?: string; 9 | }; 10 | 11 | export function TooltipContentComponent({ 12 | className, 13 | content, 14 | title, 15 | }: TooltipContentComponentProps) { 16 | return ( 17 | 18 |

{title}

19 | {Array.isArray(content) ? ( 20 | content.map((item, index) => ( 21 |

22 | {item} 23 |

24 | )) 25 | ) : typeof content === "string" ? ( 26 |

{content}

27 | ) : ( 28 | content 29 | )} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/AlertInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "~/ui/components/ui/Alert"; 2 | import { InfoIcon } from "lucide-react"; 3 | 4 | type AlertInfoProps = { 5 | title: string; 6 | content?: string | React.ReactNode; 7 | className?: string; 8 | }; 9 | 10 | export function AlertInfo({ className, content, title }: AlertInfoProps) { 11 | return ( 12 | 13 |
14 |
15 | 16 |
17 |
18 |
{title}
19 | {content && ( 20 |
21 | {typeof content === "string" ? ( 22 |

{content}

23 | ) : ( 24 | content 25 | )} 26 |
27 | )} 28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/AlertWarning.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "~/ui/components/ui/Alert"; 2 | import { AlertTriangleIcon } from "lucide-react"; 3 | 4 | type AlertWarningProps = { 5 | title: string; 6 | content?: string | React.ReactNode; 7 | className?: string; 8 | }; 9 | 10 | export function AlertWarning({ className, content, title }: AlertWarningProps) { 11 | return ( 12 | 13 |
14 |
15 | 16 |
17 |
18 |
{title}
19 | {content && ( 20 |
21 | {typeof content === "string" ? ( 22 |

{content}

23 | ) : ( 24 | content 25 | )} 26 |
27 | )} 28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Inference.net 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 | -------------------------------------------------------------------------------- /frontend/src/lib/ui-shared.tsx: -------------------------------------------------------------------------------- 1 | // Stub for @kuzco/ui-shared 2 | // This file contains minimal stubs to replace imports from the monorepo @kuzco/ui-shared package 3 | 4 | import { useCallback, useState } from "react"; 5 | import type { TouchEvent } from "react"; 6 | 7 | export const ICONS = { 8 | // Add icon definitions as needed 9 | // Example: HOME: "home-icon" 10 | }; 11 | 12 | export function useSwipeRightDetector(callback: (open: boolean) => void) { 13 | const [touchStartX, setTouchStartX] = useState(0); 14 | 15 | const onTouchStart = useCallback((e: TouchEvent) => { 16 | setTouchStartX(e.touches[0].clientX); 17 | }, []); 18 | 19 | const onTouchMove = useCallback((_e: TouchEvent) => { 20 | // Can be used to provide visual feedback during swipe 21 | }, []); 22 | 23 | const onTouchEnd = useCallback( 24 | (e: TouchEvent) => { 25 | const touchEndX = e.changedTouches[0].clientX; 26 | if (touchEndX - touchStartX > 50) { 27 | callback(false); 28 | } 29 | }, 30 | [touchStartX, callback], 31 | ); 32 | 33 | return { 34 | onTouchStart, 35 | onTouchMove, 36 | onTouchEnd, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/components/LearnMoreContent.tsx: -------------------------------------------------------------------------------- 1 | export const LearnMoreLinks = () => { 2 | return ( 3 |

4 | This dataset was built using a specialized small model, fine-tuned by{" "} 5 | 15 | Inference.net 16 | 17 | , in collaboration with{" "} 18 | 28 | LAION 29 | 30 | . 31 |

32 | ); 33 | }; 34 | 35 | export function LearnMoreContent() { 36 | return ( 37 |
38 | 39 |

40 | This is a small 100,000 sample preview of the full ~50m sample dataset. 41 | Our fine-tuned model extracts structured summaries from original, 42 | arbitrary text data. 43 |

44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/HeaderComponents.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | 3 | type HeaderComponentProps = { 4 | children: React.ReactNode; 5 | className?: string; 6 | }; 7 | 8 | export function HeaderComponent({ children, className }: HeaderComponentProps) { 9 | return ( 10 |
21 | {children} 22 |
23 | ); 24 | } 25 | 26 | type HeaderHoverLinkContainerProps = { 27 | children: React.ReactNode; 28 | className?: string; 29 | }; 30 | 31 | export function HeaderHoverLinkContainer({ 32 | children, 33 | className, 34 | }: HeaderHoverLinkContainerProps) { 35 | return ( 36 |
47 | {children} 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createLocalStorage, LOCAL_STORAGE_KEYS } from "~/lib/ui-client-utils"; 2 | import { ThemeProvider } from "~/ui"; 3 | import { createStore } from "jotai"; 4 | import { Provider as JotaiProvider } from "jotai/react"; 5 | import { PostHogProvider } from "posthog-js/react"; 6 | import React from "react"; 7 | import ReactDOM from "react-dom/client"; 8 | import { Router } from "wouter"; 9 | import { LaionHotKeys } from "./components/LaionHotKeys"; 10 | import LaionApp from "./LaionApp"; 11 | import "./index.css"; 12 | 13 | const jotaiStore = createStore(); 14 | 15 | const options = { 16 | api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, 17 | }; 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 20 | ReactDOM.createRoot(document.getElementById("root")!).render( 21 | 22 | 23 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | , 39 | ); 40 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Slider.tsx: -------------------------------------------------------------------------------- 1 | import * as SliderPrimitive from "@radix-ui/react-slider"; 2 | import { cn } from "~/ui/lib/utils"; 3 | import * as React from "react"; 4 | 5 | const Slider = React.forwardRef< 6 | React.ComponentRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | 23 | 24 | 25 | 38 | 39 | )); 40 | Slider.displayName = SliderPrimitive.Root.displayName; 41 | 42 | export { Slider }; 43 | -------------------------------------------------------------------------------- /frontend/src/ui/components/custom/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { Row } from "~/ui/components/custom/Row"; 2 | import { Input } from "~/ui/components/ui/Input"; 3 | import { cn } from "~/ui/lib/utils"; 4 | import { SearchIcon } from "lucide-react"; 5 | import { useEffect, useState } from "react"; 6 | 7 | type SearchInputProps = { 8 | className?: string; 9 | onChange: (value: string) => void; 10 | placeholder?: string; 11 | defaultValue?: string; 12 | debounceMs?: number; 13 | }; 14 | 15 | export function SearchInput({ 16 | className, 17 | debounceMs = 300, 18 | defaultValue = "", 19 | onChange, 20 | placeholder = "Search...", 21 | }: SearchInputProps) { 22 | const [value, setValue] = useState(defaultValue); 23 | 24 | useEffect(() => { 25 | const timer = setTimeout(() => { 26 | onChange(value); 27 | }, debounceMs); 28 | 29 | return () => clearTimeout(timer); 30 | }, [value, onChange, debounceMs]); 31 | 32 | const handleChange = (e: React.ChangeEvent) => { 33 | const newValue = e.target.value; 34 | setValue(newValue); 35 | }; 36 | 37 | return ( 38 | 39 | 45 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/lib/ui-client-utils.ts: -------------------------------------------------------------------------------- 1 | // Stub for @kuzco/ui-client-utils 2 | // This file contains minimal stubs to replace imports from the monorepo @kuzco/ui-client-utils package 3 | 4 | export const LOCAL_STORAGE_KEYS = { 5 | THEME: "theme", 6 | // Add other keys as needed 7 | }; 8 | 9 | export function createLocalStorage(): Storage { 10 | if (typeof window === "undefined") { 11 | // Return a mock storage for SSR 12 | const mockStorage: Record = {}; 13 | return { 14 | getItem: (key: string) => mockStorage[key] ?? null, 15 | setItem: (key: string, value: string) => { 16 | mockStorage[key] = value; 17 | }, 18 | removeItem: (key: string) => { 19 | delete mockStorage[key]; 20 | }, 21 | clear: () => { 22 | Object.keys(mockStorage).forEach((key) => delete mockStorage[key]); 23 | }, 24 | key: (index: number) => Object.keys(mockStorage)[index] ?? null, 25 | length: Object.keys(mockStorage).length, 26 | }; 27 | } 28 | return window.localStorage; 29 | } 30 | 31 | export function isTypingInputElementFocused(): boolean { 32 | if (typeof document === "undefined") return false; 33 | 34 | const activeElement = document.activeElement; 35 | if (!activeElement) return false; 36 | 37 | const tagName = activeElement.tagName.toLowerCase(); 38 | const isInput = tagName === "input" || tagName === "textarea"; 39 | const isContentEditable = 40 | activeElement.getAttribute("contenteditable") === "true"; 41 | 42 | return isInput || isContentEditable; 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Popover.tsx: -------------------------------------------------------------------------------- 1 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 2 | import { cn } from "~/ui/lib/utils"; 3 | import * as React from "react"; 4 | 5 | const Popover = PopoverPrimitive.Root; 6 | 7 | const PopoverTrigger = PopoverPrimitive.Trigger; 8 | 9 | const PopoverContent = React.forwardRef< 10 | React.ComponentRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ align = "center", className, sideOffset = 4, ...props }, ref) => ( 13 | 14 | 41 | 42 | )); 43 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 44 | 45 | export { Popover, PopoverTrigger, PopoverContent }; 46 | -------------------------------------------------------------------------------- /backend/import-to-local-d1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to set up local D1 database for development 3 | # This copies your existing SQLite database to where Wrangler expects it 4 | 5 | set -e 6 | 7 | echo "Setting up local D1 database for development..." 8 | echo "" 9 | 10 | # Find the D1 database path (Wrangler stores it in .wrangler/state/v3/d1/miniflare-D1DatabaseObject/) 11 | D1_DIR=".wrangler/state/v3/d1/miniflare-D1DatabaseObject" 12 | 13 | if [ ! -d "$D1_DIR" ]; then 14 | echo "Error: D1 directory not found at $D1_DIR" 15 | echo "Please run 'task dev' first to initialize the D1 database, then run this script." 16 | exit 1 17 | fi 18 | 19 | # Find the actual .sqlite file in the directory 20 | D1_DB_FILE=$(find "$D1_DIR" -name "*.sqlite" -type f | head -n 1) 21 | 22 | if [ -z "$D1_DB_FILE" ]; then 23 | echo "Error: No .sqlite file found in $D1_DIR" 24 | echo "Please run 'task dev' first to initialize the D1 database, then run this script." 25 | exit 1 26 | fi 27 | 28 | echo "Found local D1 database at: $D1_DB_FILE" 29 | echo "Copying data/db.sqlite to replace it..." 30 | echo "" 31 | 32 | # Backup the existing database first 33 | if [ -f "$D1_DB_FILE" ]; then 34 | echo "Creating backup: ${D1_DB_FILE}.backup" 35 | cp "$D1_DB_FILE" "${D1_DB_FILE}.backup" 36 | fi 37 | 38 | # Copy our database 39 | cp data/db.sqlite "$D1_DB_FILE" 40 | 41 | echo "" 42 | echo "✓ Database copied successfully!" 43 | echo "" 44 | echo "You can now run: task dev" 45 | echo "" 46 | echo "Note: The local D1 database is stored at: $D1_DB_FILE" 47 | echo "If you need to reset it, just delete that file and run 'task dev' again to recreate it." 48 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | /* Import fonts */ 2 | @import "./styles/fonts/inter.css"; 3 | 4 | /* Import the base Tailwind styles from ui-library */ 5 | @import "./ui/styles/global.css"; 6 | 7 | :root { 8 | font-family: "Inter", "JetBrains Mono", sans-serif; 9 | line-height: 1.5; 10 | font-weight: 400; 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | * { 18 | box-sizing: border-box; 19 | margin: 0; 20 | padding: 0; 21 | } 22 | 23 | /* Light mode autofill styles */ 24 | input:-webkit-autofill { 25 | -webkit-text-fill-color: rgb(25, 25, 25); 26 | } 27 | 28 | /* Dark mode autofill styles */ 29 | .dark input:-webkit-autofill { 30 | -webkit-text-fill-color: rgb(250, 250, 250); 31 | box-shadow: 0 0 0px 1000px rgb(41, 41, 41) inset; 32 | transition: background-color 5000s ease-in-out 0s; 33 | } 34 | 35 | body { 36 | margin: 0; 37 | display: flex; 38 | place-items: center; 39 | min-width: 320px; 40 | min-height: 100vh; 41 | } 42 | 43 | #root { 44 | width: 100%; 45 | height: 100vh; 46 | } 47 | 48 | /* Loading ellipsis animation */ 49 | @keyframes ellipsis { 50 | 0%, 51 | 20% { 52 | opacity: 0; 53 | } 54 | 40% { 55 | opacity: 1; 56 | } 57 | 60%, 58 | 100% { 59 | opacity: 1; 60 | } 61 | } 62 | 63 | .loading-dots span { 64 | animation: ellipsis 1.5s infinite; 65 | } 66 | 67 | .loading-dots span:nth-child(2) { 68 | animation-delay: 0.2s; 69 | } 70 | 71 | .loading-dots span:nth-child(3) { 72 | animation-delay: 0.4s; 73 | } 74 | 75 | a { 76 | text-decoration: underline; 77 | } 78 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable format on save 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": null, 5 | "editor.codeActionsOnSave": { 6 | "editor.formatOnSave": "always", 7 | "source.fixAll.eslint": "always" 8 | }, 9 | 10 | // Frontend (Prettier) 11 | "[typescript]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[typescriptreact]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[javascript]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[javascriptreact]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[json]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode" 25 | }, 26 | "[jsonc]": { 27 | "editor.defaultFormatter": "esbenp.prettier-vscode" 28 | }, 29 | "[css]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode" 31 | }, 32 | "[html]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | 36 | "eslint.validate": [ 37 | "javascript", 38 | "javascriptreact", 39 | "typescript", 40 | "typescriptreact" 41 | ], 42 | 43 | // Backend (Ruff) 44 | "[python]": { 45 | "editor.defaultFormatter": "charliermarsh.ruff", 46 | "editor.codeActionsOnSave": { 47 | "source.fixAll.ruff": "explicit", 48 | "source.organizeImports.ruff": "explicit" 49 | } 50 | }, 51 | 52 | // Ruff configuration 53 | "ruff.configuration": "./backend/pyproject.toml", 54 | 55 | // Python interpreter (UV virtual environment) 56 | "python.defaultInterpreterPath": "${workspaceFolder}/backend/.venv/bin/python" 57 | } 58 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LAION Dataset Explorer 🔬 8 | 9 | 10 | 11 | 12 | 13 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 33 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/ScaleLoader.tsx: -------------------------------------------------------------------------------- 1 | import type { ThemeColorName } from "../../constants/ColorSystem"; 2 | import { useTheme } from "../../providers/ThemeProvider"; 3 | import { getThemeColor } from "../../utils/getThemeColor"; 4 | 5 | type ScaleLoaderProps = { 6 | className?: string; 7 | height?: number; 8 | width?: number; 9 | animationColor?: ThemeColorName; 10 | }; 11 | 12 | export function ScaleLoader({ 13 | animationColor = "foreground", 14 | className, 15 | height = 18, 16 | width = 3, 17 | }: ScaleLoaderProps) { 18 | const { darkOrLightTheme } = useTheme(); 19 | const color = getThemeColor(darkOrLightTheme, animationColor); 20 | 21 | return ( 22 |
29 | {[0, 1, 2, 3, 4].map((index) => ( 30 |
43 | ))} 44 | 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/components/LaionHotKeys.tsx: -------------------------------------------------------------------------------- 1 | import { isTypingInputElementFocused } from "~/lib/ui-client-utils"; 2 | import { useTheme } from "~/ui"; 3 | import { useCallback, useEffect } from "react"; 4 | 5 | const HOT_KEY_MAP = { 6 | TOGGLE_THEME: "t", 7 | }; 8 | 9 | type HotKey = keyof typeof HOT_KEY_MAP; 10 | 11 | export function LaionHotKeys() { 12 | const { setTheme, theme } = useTheme(); 13 | 14 | const toggleTheme = useCallback(() => { 15 | setTheme(theme === "light" ? "dark" : "light"); 16 | }, [setTheme, theme]); 17 | 18 | useEffect(() => { 19 | function handleKeyDown(event: KeyboardEvent) { 20 | if (isTypingInputElementFocused()) { 21 | return; 22 | } 23 | 24 | const matchedKey = Object.entries(HOT_KEY_MAP).find( 25 | ([_, value]) => value === event.key, 26 | ); 27 | if (matchedKey == null) { 28 | return; 29 | } 30 | 31 | const metaKeysPressed = event.metaKey || event.ctrlKey; 32 | if (metaKeysPressed) { 33 | return; 34 | } 35 | 36 | // This prevents the closest active element from being focused. 37 | event.preventDefault(); 38 | document.body.focus(); 39 | 40 | const hotKey = matchedKey[0] as HotKey; 41 | switch (hotKey) { 42 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 43 | case "TOGGLE_THEME": { 44 | toggleTheme(); 45 | break; 46 | } 47 | } 48 | } 49 | 50 | document.addEventListener("keydown", handleKeyDown); 51 | return function cleanup() { 52 | document.removeEventListener("keydown", handleKeyDown); 53 | }; 54 | }, [toggleTheme]); 55 | 56 | return null; 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 2 | import { cn } from "~/ui/lib/utils"; 3 | import * as React from "react"; 4 | 5 | const Avatar = React.forwardRef< 6 | React.ComponentRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | )); 18 | Avatar.displayName = AvatarPrimitive.Root.displayName; 19 | 20 | const AvatarImage = React.forwardRef< 21 | React.ComponentRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, ...props }, ref) => ( 24 | 29 | )); 30 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 31 | 32 | const AvatarFallback = React.forwardRef< 33 | React.ComponentRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, ...props }, ref) => ( 36 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Badge.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | import type { VariantProps } from "class-variance-authority"; 3 | import { cva } from "class-variance-authority"; 4 | import * as React from "react"; 5 | 6 | const badgeVariants = cva( 7 | ` 8 | inline-flex items-center rounded-full border px-3 py-1.5 text-xs 9 | font-semibold transition-colors 10 | 11 | focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 12 | `, 13 | { 14 | defaultVariants: { 15 | variant: "default", 16 | }, 17 | variants: { 18 | variant: { 19 | default: ` 20 | border-transparent bg-primary text-primary-foreground 21 | 22 | hover:bg-primary/80 23 | `, 24 | destructive: ` 25 | border-transparent bg-destructive text-destructive-foreground 26 | 27 | hover:bg-destructive/80 28 | `, 29 | failure: ` 30 | border-transparent bg-detail-failure text-detail-failure-foreground 31 | 32 | hover:bg-detail-failure/80 33 | `, 34 | outline: "text-foreground", 35 | secondary: ` 36 | border-transparent bg-secondary text-secondary-foreground 37 | 38 | hover:bg-secondary/80 39 | `, 40 | success: "border bg-background font-medium text-green-500", 41 | }, 42 | }, 43 | }, 44 | ); 45 | 46 | export type BadgeProps = React.HTMLAttributes & 47 | VariantProps; 48 | 49 | function Badge({ className, variant, ...props }: BadgeProps) { 50 | return ( 51 |
52 | ); 53 | } 54 | 55 | export { Badge, badgeVariants }; 56 | -------------------------------------------------------------------------------- /frontend/src/ui/components/ui/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/ui/lib/utils"; 2 | import * as React from "react"; 3 | 4 | export type TextareaProps = React.ComponentProps<"textarea"> & { 5 | error?: string | null; 6 | hasError?: boolean; 7 | hint?: string; 8 | label?: string; 9 | }; 10 | 11 | const Textarea = React.forwardRef( 12 | ({ className, error, hasError, hint, label, ...props }, ref) => { 13 | return ( 14 |
15 | {label && ( 16 | 17 | )} 18 |