yassinekolsi commited on
Commit
c5474d5
·
1 Parent(s): 7b233d3

chore: integrate UI snapshot from raed-ui branch

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. ui/.gitignore +41 -0
  2. ui/.vscode/mcp.json +11 -0
  3. ui/.vscode/tasks.json +44 -0
  4. ui/README.md +64 -0
  5. ui/app/api/data/route.ts +7 -0
  6. ui/app/api/discovery/route.ts +19 -0
  7. ui/app/api/explorer/route.ts +17 -0
  8. ui/app/data/data-view.tsx +117 -0
  9. ui/app/data/page.tsx +27 -0
  10. ui/app/discovery/page.tsx +182 -0
  11. ui/app/explorer/chart.tsx +55 -0
  12. ui/app/explorer/components.tsx +99 -0
  13. ui/app/explorer/page.tsx +57 -0
  14. ui/app/favicon.ico +0 -0
  15. ui/app/globals.css +125 -0
  16. ui/app/layout.tsx +55 -0
  17. ui/app/loading.tsx +12 -0
  18. ui/app/page.tsx +169 -0
  19. ui/app/settings/page.tsx +192 -0
  20. ui/components.json +24 -0
  21. ui/components/page-header.tsx +37 -0
  22. ui/components/sidebar.tsx +95 -0
  23. ui/components/ui/badge.tsx +37 -0
  24. ui/components/ui/button.tsx +58 -0
  25. ui/components/ui/card.tsx +77 -0
  26. ui/components/ui/input.tsx +21 -0
  27. ui/components/ui/label.tsx +24 -0
  28. ui/components/ui/select.tsx +190 -0
  29. ui/components/ui/separator.tsx +28 -0
  30. ui/components/ui/skeleton.tsx +13 -0
  31. ui/components/ui/slider.tsx +63 -0
  32. ui/components/ui/switch.tsx +35 -0
  33. ui/components/ui/table.tsx +116 -0
  34. ui/components/ui/tabs.tsx +91 -0
  35. ui/components/ui/textarea.tsx +25 -0
  36. ui/eslint.config.mjs +18 -0
  37. ui/hooks/use-controlled-state.tsx +33 -0
  38. ui/hooks/use-mobile.ts +19 -0
  39. ui/lib/api-utils.ts +5 -0
  40. ui/lib/data-service.ts +19 -0
  41. ui/lib/explorer-service.ts +52 -0
  42. ui/lib/get-strict-context.tsx +36 -0
  43. ui/lib/utils.ts +6 -0
  44. ui/next.config.ts +37 -0
  45. ui/package.json +45 -0
  46. ui/pnpm-lock.yaml +0 -0
  47. ui/pnpm-workspace.yaml +2 -0
  48. ui/postcss.config.mjs +7 -0
  49. ui/public/file.svg +1 -0
  50. ui/public/globe.svg +1 -0
ui/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
ui/.vscode/mcp.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "servers": {
3
+ "shadcn": {
4
+ "command": "npx",
5
+ "args": [
6
+ "shadcn@latest",
7
+ "mcp"
8
+ ]
9
+ }
10
+ }
11
+ }
ui/.vscode/tasks.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "label": "Type Check",
6
+ "type": "shell",
7
+ "command": "pnpm",
8
+ "args": [
9
+ "run",
10
+ "type-check"
11
+ ],
12
+ "problemMatcher": [
13
+ "$tsc"
14
+ ],
15
+ "group": "test"
16
+ },
17
+ {
18
+ "label": "Lint",
19
+ "type": "shell",
20
+ "command": "pnpm",
21
+ "args": [
22
+ "run",
23
+ "lint"
24
+ ],
25
+ "problemMatcher": [
26
+ "$eslint-stylish"
27
+ ],
28
+ "group": "test"
29
+ },
30
+ {
31
+ "label": "Lint",
32
+ "type": "shell",
33
+ "command": "npm",
34
+ "args": [
35
+ "run",
36
+ "lint"
37
+ ],
38
+ "problemMatcher": [
39
+ "$eslint-stylish"
40
+ ],
41
+ "group": "test"
42
+ }
43
+ ]
44
+ }
ui/README.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BioFlow UI (Next.js)
2
+
3
+ This project is a migration of the Streamlit "old_ui" to Next.js 16, Shadcn UI, Framer Motion, and Tailwind CSS v4.
4
+
5
+ ## Getting Started
6
+
7
+ 1. **Install dependencies:**
8
+
9
+ ```bash
10
+ cd ui
11
+ pnpm install
12
+ # or
13
+ npm install
14
+ ```
15
+
16
+ 2. **Run the development server:**
17
+
18
+ ```bash
19
+ pnpm dev
20
+ # or
21
+ npm run dev
22
+ ```
23
+
24
+ 3. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
25
+
26
+ ## Structure
27
+
28
+ - `app/`: App Router pages and layout.
29
+ - `page.tsx`: Home Dashboard.
30
+ - `discovery/`: Drug Discovery Pipeline interface.
31
+ - `explorer/`: Data Explorer with Charts.
32
+ - `data/`: Data Management.
33
+ - `settings/`: Configuration.
34
+ - `api/`: API Route handlers.
35
+ - `components/`: Reusable UI components.
36
+ - `ui/`: Shadcn-like primitive components (Button, Card, etc.).
37
+ - `sidebar.tsx`: Main Navigation.
38
+ - `page-header.tsx`: Standard page headers.
39
+
40
+ ## Tech Stack
41
+
42
+ - **Framework:** Next.js 16 (App Router)
43
+ - **Styling:** Tailwind CSS v4
44
+ - **UI Library:** Custom components inspired by Shadcn UI
45
+ - **Icons:** Lucide React
46
+ - **Charts:** Recharts
47
+ - **Animations:** Framer Motion (basic animations included, Framer Motion ready)
48
+
49
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
50
+
51
+ ## Learn More
52
+
53
+ To learn more about Next.js, take a look at the following resources:
54
+
55
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
56
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
57
+
58
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
59
+
60
+ ## Deploy on Vercel
61
+
62
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
63
+
64
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
ui/app/api/data/route.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { getData } from "@/lib/data-service";
3
+
4
+ export async function GET() {
5
+ const data = await getData();
6
+ return NextResponse.json(data);
7
+ }
ui/app/api/discovery/route.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function POST(request: Request) {
4
+ const body = await request.json();
5
+ const { query } = body;
6
+
7
+ console.log("Starting discovery for:", query);
8
+
9
+ // Here you would typically call your Python backend
10
+ // e.g., await fetch('http://localhost:8000/api/discovery', { ... })
11
+
12
+ // Mock response
13
+ return NextResponse.json({
14
+ success: true,
15
+ jobId: "job_" + Date.now(),
16
+ status: "processing",
17
+ message: "Pipeline started successfully"
18
+ });
19
+ }
ui/app/api/explorer/route.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { getExplorerPoints } from "@/lib/explorer-service";
3
+
4
+ export async function GET(request: Request) {
5
+ const { searchParams } = new URL(request.url);
6
+
7
+ try {
8
+ const data = await getExplorerPoints(
9
+ searchParams.get("dataset") || undefined,
10
+ searchParams.get("view") || undefined,
11
+ searchParams.get("colorBy") || undefined
12
+ );
13
+ return NextResponse.json(data);
14
+ } catch (e) {
15
+ return NextResponse.json({ error: "Invalid parameters" }, { status: 400 });
16
+ }
17
+ }
ui/app/data/data-view.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Card, CardContent } from "@/components/ui/card"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Badge } from "@/components/ui/badge"
6
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
7
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
8
+ import { Folder, FileText, Database, HardDrive, Upload, CloudUpload, Eye, Download, Trash2 } from "lucide-react"
9
+ import { SectionHeader } from "@/components/page-header"
10
+ import { Dataset, Statistics } from "@/types/data"
11
+
12
+ interface DataViewProps {
13
+ datasets: Dataset[];
14
+ stats: Statistics | null;
15
+ }
16
+
17
+ export function DataView({ datasets, stats }: DataViewProps) {
18
+ const statCards = [
19
+ { label: "Datasets", value: stats?.datasets?.toString() ?? "—", icon: Folder, color: "text-blue-500" },
20
+ { label: "Molecules", value: stats?.molecules ?? "—", icon: FileText, color: "text-cyan-500" },
21
+ { label: "Proteins", value: stats?.proteins ?? "—", icon: Database, color: "text-emerald-500" },
22
+ { label: "Storage Used", value: stats?.storage ?? "—", icon: HardDrive, color: "text-amber-500" },
23
+ ]
24
+
25
+ return (
26
+ <div className="space-y-8">
27
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
28
+ {statCards.map((stat, i) => (
29
+ <Card key={i}>
30
+ <CardContent className="p-6">
31
+ <div className="flex justify-between items-start mb-2">
32
+ <div className="text-sm font-medium text-muted-foreground">{stat.label}</div>
33
+ <div className={`text-lg ${stat.color}`}><stat.icon className="h-5 w-5" /></div>
34
+ </div>
35
+ <div className="text-2xl font-bold">{stat.value}</div>
36
+ </CardContent>
37
+ </Card>
38
+ ))}
39
+ </div>
40
+
41
+ <Tabs defaultValue="datasets">
42
+ <TabsList>
43
+ <TabsTrigger value="datasets">Datasets</TabsTrigger>
44
+ <TabsTrigger value="upload">Upload</TabsTrigger>
45
+ <TabsTrigger value="processing">Processing</TabsTrigger>
46
+ </TabsList>
47
+ <TabsContent value="datasets" className="space-y-4">
48
+ <SectionHeader title="Your Datasets" icon={<Folder className="h-5 w-5 text-primary" />} />
49
+ <Card>
50
+ <Table>
51
+ <TableHeader>
52
+ <TableRow>
53
+ <TableHead>Name</TableHead>
54
+ <TableHead>Type</TableHead>
55
+ <TableHead>Items</TableHead>
56
+ <TableHead>Size</TableHead>
57
+ <TableHead>Updated</TableHead>
58
+ <TableHead className="text-right">Actions</TableHead>
59
+ </TableRow>
60
+ </TableHeader>
61
+ <TableBody>
62
+ {datasets.length === 0 && (
63
+ <TableRow>
64
+ <TableCell colSpan={6} className="text-center text-muted-foreground text-sm">No datasets found.</TableCell>
65
+ </TableRow>
66
+ )}
67
+ {datasets.map((ds, i) => (
68
+ <TableRow key={i}>
69
+ <TableCell className="font-medium">{ds.name}</TableCell>
70
+ <TableCell>
71
+ <div className="flex items-center gap-2">
72
+ <Badge variant={ds.type === 'Molecules' ? 'default' : 'secondary'}>{ds.type}</Badge>
73
+ </div>
74
+ </TableCell>
75
+ <TableCell>{ds.count}</TableCell>
76
+ <TableCell>{ds.size}</TableCell>
77
+ <TableCell>{ds.updated}</TableCell>
78
+ <TableCell className="text-right">
79
+ <div className="flex justify-end gap-2">
80
+ <Button size="icon" variant="ghost" className="h-8 w-8"><Eye className="h-4 w-4" /></Button>
81
+ <Button size="icon" variant="ghost" className="h-8 w-8"><Download className="h-4 w-4" /></Button>
82
+ <Button size="icon" variant="ghost" className="h-8 w-8 text-destructive hover:text-destructive"><Trash2 className="h-4 w-4" /></Button>
83
+ </div>
84
+ </TableCell>
85
+ </TableRow>
86
+ ))}
87
+ </TableBody>
88
+ </Table>
89
+ </Card>
90
+ </TabsContent>
91
+ <TabsContent value="upload">
92
+ <SectionHeader title="Upload New Data" icon={<Upload className="h-5 w-5 text-primary" />} />
93
+ <Card>
94
+ <CardContent className="p-12">
95
+ <div className="border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center text-center space-y-4 hover:bg-accent/50 transition-colors cursor-pointer">
96
+ <div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center text-primary">
97
+ <CloudUpload className="h-8 w-8" />
98
+ </div>
99
+ <div>
100
+ <div className="text-lg font-semibold">Click to upload or drag and drop</div>
101
+ <div className="text-sm text-muted-foreground">CSV, SDF, FASTA, or JSON (max 50MB)</div>
102
+ </div>
103
+ </div>
104
+ </CardContent>
105
+ </Card>
106
+ </TabsContent>
107
+ <TabsContent value="processing">
108
+ <Card>
109
+ <CardContent className="p-12 text-center text-muted-foreground">
110
+ No active processing tasks.
111
+ </CardContent>
112
+ </Card>
113
+ </TabsContent>
114
+ </Tabs>
115
+ </div>
116
+ )
117
+ }
ui/app/data/page.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Suspense } from 'react'
2
+ import { getData } from "@/lib/data-service"
3
+ import { DataView } from "./data-view"
4
+ import { PageHeader } from "@/components/page-header"
5
+ import { Database, Loader2 } from "lucide-react"
6
+
7
+ export default async function DataPage() {
8
+ const { datasets, stats } = await getData()
9
+
10
+ return (
11
+ <div className="space-y-8 animate-in fade-in duration-500">
12
+ <PageHeader
13
+ title="Data Management"
14
+ subtitle="Upload, manage, and organize your datasets"
15
+ icon={<Database className="h-8 w-8" />}
16
+ />
17
+
18
+ <Suspense fallback={
19
+ <div className="flex h-[400px] w-full items-center justify-center">
20
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
21
+ </div>
22
+ }>
23
+ <DataView datasets={datasets} stats={stats} />
24
+ </Suspense>
25
+ </div>
26
+ )
27
+ }
ui/app/discovery/page.tsx ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { PageHeader, SectionHeader } from "@/components/page-header"
5
+ import { Card, CardContent } from "@/components/ui/card"
6
+ import { Textarea } from "@/components/ui/textarea"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
10
+ import { Label } from "@/components/ui/label"
11
+ import { Microscope, Search, CheckCircle2, Circle, Loader2, ArrowRight } from "lucide-react"
12
+
13
+ export default function DiscoveryPage() {
14
+ const [query, setQuery] = React.useState("")
15
+ const [isSearching, setIsSearching] = React.useState(false)
16
+ const [step, setStep] = React.useState(0)
17
+
18
+ const handleSearch = () => {
19
+ setIsSearching(true)
20
+ setStep(1)
21
+
22
+ // Simulate pipeline
23
+ setTimeout(() => setStep(2), 1500)
24
+ setTimeout(() => setStep(3), 3000)
25
+ setTimeout(() => {
26
+ setStep(4)
27
+ setIsSearching(false)
28
+ }, 4500)
29
+ }
30
+
31
+ const steps = [
32
+ { name: "Input", status: step > 0 ? "done" : "active" },
33
+ { name: "Encode", status: step > 1 ? "done" : (step === 1 ? "active" : "pending") },
34
+ { name: "Search", status: step > 2 ? "done" : (step === 2 ? "active" : "pending") },
35
+ { name: "Predict", status: step > 3 ? "done" : (step === 3 ? "active" : "pending") },
36
+ { name: "Results", status: step === 4 ? "active" : "pending" },
37
+ ]
38
+
39
+ return (
40
+ <div className="space-y-8 animate-in fade-in duration-500">
41
+ <PageHeader
42
+ title="Drug Discovery"
43
+ subtitle="Search for drug candidates with AI-powered analysis"
44
+ icon={<Microscope className="h-8 w-8" />}
45
+ />
46
+
47
+ <Card>
48
+ <div className="p-4 border-b font-semibold">Search Query</div>
49
+ <CardContent className="p-6">
50
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
51
+ <div className="md:col-span-3">
52
+ <Textarea
53
+ placeholder="Enter a natural language query, SMILES string, or FASTA sequence..."
54
+ className="min-h-[120px] font-mono"
55
+ value={query}
56
+ onChange={(e) => setQuery(e.target.value)}
57
+ />
58
+ </div>
59
+ <div className="space-y-4">
60
+ <div className="space-y-2">
61
+ <Label>Search Type</Label>
62
+ <Select defaultValue="Similarity">
63
+ <SelectTrigger>
64
+ <SelectValue placeholder="Select type" />
65
+ </SelectTrigger>
66
+ <SelectContent>
67
+ <SelectItem value="Similarity">Similarity</SelectItem>
68
+ <SelectItem value="Binding Affinity">Binding Affinity</SelectItem>
69
+ <SelectItem value="Properties">Properties</SelectItem>
70
+ </SelectContent>
71
+ </Select>
72
+ </div>
73
+ <div className="space-y-2">
74
+ <Label>Database</Label>
75
+ <Select defaultValue="All">
76
+ <SelectTrigger>
77
+ <SelectValue placeholder="Select database" />
78
+ </SelectTrigger>
79
+ <SelectContent>
80
+ <SelectItem value="All">All</SelectItem>
81
+ <SelectItem value="DrugBank">DrugBank</SelectItem>
82
+ <SelectItem value="ChEMBL">ChEMBL</SelectItem>
83
+ <SelectItem value="ZINC">ZINC</SelectItem>
84
+ </SelectContent>
85
+ </Select>
86
+ </div>
87
+ <Button
88
+ className="w-full"
89
+ onClick={handleSearch}
90
+ disabled={isSearching || !query}
91
+ >
92
+ {isSearching ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Search className="mr-2 h-4 w-4" />}
93
+ {isSearching ? "Running..." : "Search"}
94
+ </Button>
95
+ </div>
96
+ </div>
97
+ </CardContent>
98
+ </Card>
99
+
100
+ <div className="space-y-4">
101
+ <SectionHeader title="Pipeline Status" icon={<ArrowRight className="h-5 w-5 text-muted-foreground" />} />
102
+
103
+ <div className="relative">
104
+ <div className="absolute left-0 top-1/2 w-full h-0.5 bg-muted -z-10 transform -translate-y-1/2"></div>
105
+ <div className="flex justify-between items-center w-full px-4">
106
+ {steps.map((s, i) => (
107
+ <div key={i} className="flex flex-col items-center gap-2 bg-background px-2">
108
+ <div className={`h-8 w-8 rounded-full flex items-center justify-center border-2 transition-colors ${
109
+ s.status === 'done' ? 'bg-primary border-primary text-primary-foreground' :
110
+ s.status === 'active' ? 'border-primary text-primary animate-pulse' : 'border-muted text-muted-foreground bg-background'
111
+ }`}>
112
+ {s.status === 'done' ? <CheckCircle2 className="h-5 w-5" /> : <Circle className="h-5 w-5" />}
113
+ </div>
114
+ <span className={`text-sm font-medium ${s.status === 'pending' ? 'text-muted-foreground' : 'text-foreground'}`}>
115
+ {s.name}
116
+ </span>
117
+ </div>
118
+ ))}
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ {step === 4 && (
124
+ <div className="space-y-4 animate-in slide-in-from-bottom-4 duration-500">
125
+ <SectionHeader title="Results" icon={<CheckCircle2 className="h-5 w-5 text-green-500" />} />
126
+
127
+ <Tabs defaultValue="candidates">
128
+ <TabsList>
129
+ <TabsTrigger value="candidates">Top Candidates</TabsTrigger>
130
+ <TabsTrigger value="analysis">Property Analysis</TabsTrigger>
131
+ <TabsTrigger value="evidence">Evidence</TabsTrigger>
132
+ </TabsList>
133
+ <TabsContent value="candidates" className="space-y-4">
134
+ {[
135
+ { name: "Candidate A", score: 0.95, mw: "342.4", logp: "2.1" },
136
+ { name: "Candidate B", score: 0.89, mw: "298.3", logp: "1.8" },
137
+ { name: "Candidate C", score: 0.82, mw: "415.5", logp: "3.2" },
138
+ { name: "Candidate D", score: 0.76, mw: "267.3", logp: "1.5" },
139
+ { name: "Candidate E", score: 0.71, mw: "389.4", logp: "2.8" },
140
+ ].map((candidate, i) => (
141
+ <Card key={i}>
142
+ <CardContent className="p-4 flex items-center justify-between">
143
+ <div>
144
+ <div className="font-bold text-lg">{candidate.name}</div>
145
+ <div className="flex gap-4 text-sm text-muted-foreground">
146
+ <span>MW: {candidate.mw}</span>
147
+ <span>LogP: {candidate.logp}</span>
148
+ </div>
149
+ </div>
150
+ <div className="text-right">
151
+ <div className="text-sm text-muted-foreground">Score</div>
152
+ <div className={`text-xl font-bold ${
153
+ candidate.score >= 0.9 ? 'text-green-600' :
154
+ candidate.score >= 0.8 ? 'text-green-500' : 'text-amber-500'
155
+ }`}>
156
+ {candidate.score}
157
+ </div>
158
+ </div>
159
+ </CardContent>
160
+ </Card>
161
+ ))}
162
+ </TabsContent>
163
+ <TabsContent value="analysis">
164
+ <Card>
165
+ <CardContent className="p-12 text-center text-muted-foreground">
166
+ Chart visualization would go here (using Recharts).
167
+ </CardContent>
168
+ </Card>
169
+ </TabsContent>
170
+ <TabsContent value="evidence">
171
+ <Card>
172
+ <CardContent className="p-12 text-center text-muted-foreground">
173
+ Evidence graph visualization would go here.
174
+ </CardContent>
175
+ </Card>
176
+ </TabsContent>
177
+ </Tabs>
178
+ </div>
179
+ )}
180
+ </div>
181
+ )
182
+ }
ui/app/explorer/chart.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { ResponsiveContainer, ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, Cell } from "recharts"
4
+ import { Card, CardContent } from "@/components/ui/card"
5
+ import { AlertCircle } from "lucide-react"
6
+ import { DataPoint } from "@/types/explorer"
7
+
8
+ export function ExplorerChart({ data }: { data: DataPoint[] }) {
9
+ if (!data || data.length === 0) {
10
+ return (
11
+ <div className="flex h-full items-center justify-center text-muted-foreground">
12
+ <AlertCircle className="mr-2 h-4 w-4" />
13
+ No data available for this configuration.
14
+ </div>
15
+ )
16
+ }
17
+
18
+ return (
19
+ <Card className="h-[500px] overflow-hidden bg-gradient-to-br from-card to-secondary/30">
20
+ <CardContent className="p-4 h-full relative">
21
+ <ResponsiveContainer width="100%" height="100%">
22
+ <ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
23
+ <XAxis type="number" dataKey="x" name="PC1" stroke="currentColor" fontSize={12} tickLine={false} axisLine={{ strokeOpacity: 0.2 }} />
24
+ <YAxis type="number" dataKey="y" name="PC2" stroke="currentColor" fontSize={12} tickLine={false} axisLine={{ strokeOpacity: 0.2 }} />
25
+ <ZAxis type="number" dataKey="z" range={[50, 400]} />
26
+ <Tooltip
27
+ cursor={{ strokeDasharray: '3 3' }}
28
+ content={({ active, payload }) => {
29
+ if (active && payload && payload.length) {
30
+ const point = payload[0].payload as DataPoint;
31
+ return (
32
+ <div className="bg-popover border border-border p-3 rounded-lg shadow-xl text-sm z-50">
33
+ <p className="font-bold mb-1 text-primary">{point.name}</p>
34
+ <div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-muted-foreground">
35
+ <span>X:</span> <span className="text-foreground">{Number(point.x).toFixed(2)}</span>
36
+ <span>Y:</span> <span className="text-foreground">{Number(point.y).toFixed(2)}</span>
37
+ <span>Affinity:</span> <span className="text-foreground">{Number(point.affinity).toFixed(2)}</span>
38
+ </div>
39
+ </div>
40
+ )
41
+ }
42
+ return null;
43
+ }}
44
+ />
45
+ <Scatter name="Molecules" data={data} fill="#8884d8" animationDuration={1000}>
46
+ {data.map((entry, index) => (
47
+ <Cell key={`cell-${index}`} fill={entry.color} fillOpacity={0.7} className="hover:opacity-100 transition-opacity duration-200" />
48
+ ))}
49
+ </Scatter>
50
+ </ScatterChart>
51
+ </ResponsiveContainer>
52
+ </CardContent>
53
+ </Card>
54
+ )
55
+ }
ui/app/explorer/components.tsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useRouter, useSearchParams } from "next/navigation"
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
6
+ import { Label } from "@/components/ui/label"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
9
+ import { RefreshCw, SlidersHorizontal } from "lucide-react"
10
+
11
+ export function ExplorerControls() {
12
+ const router = useRouter()
13
+ const searchParams = useSearchParams()
14
+ const [isPending, startTransition] = React.useTransition()
15
+
16
+ const dataset = searchParams.get("dataset") || "DrugBank"
17
+ const visualization = searchParams.get("view") || "UMAP"
18
+ const colorBy = searchParams.get("colorBy") || "Activity"
19
+
20
+ const createQueryString = React.useCallback(
21
+ (name: string, value: string) => {
22
+ const params = new URLSearchParams(searchParams.toString())
23
+ params.set(name, value)
24
+ return params.toString()
25
+ },
26
+ [searchParams]
27
+ )
28
+
29
+ const handleUpdate = (name: string, value: string) => {
30
+ startTransition(() => {
31
+ router.push(`?${createQueryString(name, value)}`, { scroll: false })
32
+ })
33
+ }
34
+
35
+ return (
36
+ <Card className="h-full border-l-4 border-l-primary/50">
37
+ <CardHeader>
38
+ <CardTitle className="flex items-center gap-2 text-lg">
39
+ <SlidersHorizontal className="h-5 w-5" />
40
+ Controls
41
+ </CardTitle>
42
+ </CardHeader>
43
+ <CardContent className="space-y-6">
44
+ <div className="space-y-2">
45
+ <Label htmlFor="dataset">Dataset</Label>
46
+ <Select value={dataset} onValueChange={(v) => handleUpdate("dataset", v)}>
47
+ <SelectTrigger id="dataset">
48
+ <SelectValue placeholder="Select dataset" />
49
+ </SelectTrigger>
50
+ <SelectContent>
51
+ <SelectItem value="DrugBank">DrugBank</SelectItem>
52
+ <SelectItem value="ChEMBL">ChEMBL</SelectItem>
53
+ <SelectItem value="ZINC">ZINC</SelectItem>
54
+ </SelectContent>
55
+ </Select>
56
+ </div>
57
+ <div className="space-y-2">
58
+ <Label htmlFor="visualization">Algorithm</Label>
59
+ <Select value={visualization} onValueChange={(v) => handleUpdate("view", v)}>
60
+ <SelectTrigger id="visualization">
61
+ <SelectValue placeholder="Select algorithm" />
62
+ </SelectTrigger>
63
+ <SelectContent>
64
+ <SelectItem value="UMAP">UMAP</SelectItem>
65
+ <SelectItem value="t-SNE">t-SNE</SelectItem>
66
+ <SelectItem value="PCA">PCA</SelectItem>
67
+ </SelectContent>
68
+ </Select>
69
+ </div>
70
+ <div className="space-y-2">
71
+ <Label htmlFor="colorBy">Color Mapping</Label>
72
+ <Select value={colorBy} onValueChange={(v) => handleUpdate("colorBy", v)}>
73
+ <SelectTrigger id="colorBy">
74
+ <SelectValue placeholder="Select color metric" />
75
+ </SelectTrigger>
76
+ <SelectContent>
77
+ <SelectItem value="Activity">Binding Affinity</SelectItem>
78
+ <SelectItem value="MW">Molecular Weight</SelectItem>
79
+ <SelectItem value="LogP">LogP</SelectItem>
80
+ <SelectItem value="Cluster">Cluster ID</SelectItem>
81
+ </SelectContent>
82
+ </Select>
83
+ </div>
84
+
85
+ <div className="pt-4">
86
+ <Button
87
+ variant="secondary"
88
+ className="w-full"
89
+ disabled={isPending}
90
+ onClick={() => handleUpdate("refresh", Date.now().toString())}
91
+ >
92
+ <RefreshCw className={`mr-2 h-4 w-4 ${isPending ? "animate-spin" : ""}`} />
93
+ {isPending ? "Updating..." : "Regenerate View"}
94
+ </Button>
95
+ </div>
96
+ </CardContent>
97
+ </Card>
98
+ )
99
+ }
ui/app/explorer/page.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Suspense } from "react"
2
+ import { getExplorerPoints } from "@/lib/explorer-service"
3
+ import { ExplorerControls } from "./components"
4
+ import { ExplorerChart } from "./chart"
5
+ import { Loader2 } from "lucide-react"
6
+
7
+ interface ExplorerPageProps {
8
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>
9
+ }
10
+
11
+ function pickParam(v: string | string[] | undefined): string | undefined {
12
+ if (Array.isArray(v)) return v[0]
13
+ return v
14
+ }
15
+
16
+ export default async function ExplorerPage({ searchParams }: ExplorerPageProps) {
17
+ // Await searchParams as required by Next.js 16/15
18
+ const params = await searchParams
19
+
20
+ const dataset = pickParam(params.dataset) || "DrugBank"
21
+ const view = pickParam(params.view) || "UMAP"
22
+ const colorBy = pickParam(params.colorBy) || "Activity"
23
+
24
+ const { points: data } = await getExplorerPoints(dataset, view, colorBy)
25
+
26
+ return (
27
+ <div className="container mx-auto p-6 space-y-6">
28
+ <div className="flex flex-col space-y-2">
29
+ <h1 className="text-3xl font-bold tracking-tight">Data Explorer</h1>
30
+ <p className="text-muted-foreground">
31
+ Visualize binding affinity landscapes and model predictions in 3D space using dimensionality reduction.
32
+ </p>
33
+ </div>
34
+
35
+ <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
36
+ <div className="lg:col-span-1">
37
+ <Suspense fallback={<div>Loading controls...</div>}>
38
+ <ExplorerControls />
39
+ </Suspense>
40
+ </div>
41
+
42
+ <div className="lg:col-span-3">
43
+ <Suspense
44
+ key={`${dataset}-${view}-${colorBy}`}
45
+ fallback={
46
+ <div className="h-[500px] flex items-center justify-center border rounded-lg bg-muted/10">
47
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
48
+ </div>
49
+ }
50
+ >
51
+ <ExplorerChart data={data} />
52
+ </Suspense>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ )
57
+ }
ui/app/favicon.ico ADDED
ui/app/globals.css ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ @theme inline {
7
+ --color-background: var(--background);
8
+ --color-foreground: var(--foreground);
9
+ --font-sans: var(--font-geist-sans);
10
+ --font-mono: var(--font-geist-mono);
11
+ --color-sidebar-ring: var(--sidebar-ring);
12
+ --color-sidebar-border: var(--sidebar-border);
13
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
+ --color-sidebar-accent: var(--sidebar-accent);
15
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
+ --color-sidebar-primary: var(--sidebar-primary);
17
+ --color-sidebar-foreground: var(--sidebar-foreground);
18
+ --color-sidebar: var(--sidebar);
19
+ --color-chart-5: var(--chart-5);
20
+ --color-chart-4: var(--chart-4);
21
+ --color-chart-3: var(--chart-3);
22
+ --color-chart-2: var(--chart-2);
23
+ --color-chart-1: var(--chart-1);
24
+ --color-ring: var(--ring);
25
+ --color-input: var(--input);
26
+ --color-border: var(--border);
27
+ --color-destructive: var(--destructive);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-muted-foreground: var(--muted-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-secondary-foreground: var(--secondary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-popover: var(--popover);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-card: var(--card);
40
+ --radius-sm: calc(var(--radius) - 4px);
41
+ --radius-md: calc(var(--radius) - 2px);
42
+ --radius-lg: var(--radius);
43
+ --radius-xl: calc(var(--radius) + 4px);
44
+ --radius-2xl: calc(var(--radius) + 8px);
45
+ --radius-3xl: calc(var(--radius) + 12px);
46
+ --radius-4xl: calc(var(--radius) + 16px);
47
+ }
48
+
49
+ :root {
50
+ --radius: 0.625rem;
51
+ --background: oklch(0.985 0 0);
52
+ --foreground: oklch(0.145 0.02 260);
53
+ --card: oklch(1 0 0);
54
+ --card-foreground: oklch(0.145 0.02 260);
55
+ --popover: oklch(1 0 0);
56
+ --popover-foreground: oklch(0.145 0.02 260);
57
+ --primary: oklch(0.55 0.2 280);
58
+ --primary-foreground: oklch(0.985 0 0);
59
+ --secondary: oklch(0.97 0.01 260);
60
+ --secondary-foreground: oklch(0.55 0.2 280);
61
+ --muted: oklch(0.97 0.01 260);
62
+ --muted-foreground: oklch(0.556 0.03 260);
63
+ --accent: oklch(0.97 0.01 260);
64
+ --accent-foreground: oklch(0.55 0.2 280);
65
+ --destructive: oklch(0.577 0.245 27.325);
66
+ --border: oklch(0.922 0.01 260);
67
+ --input: oklch(0.922 0.01 260);
68
+ --ring: oklch(0.55 0.2 280);
69
+ --chart-1: oklch(0.646 0.222 41.116);
70
+ --chart-2: oklch(0.6 0.118 184.704);
71
+ --chart-3: oklch(0.398 0.07 227.392);
72
+ --chart-4: oklch(0.828 0.189 84.429);
73
+ --chart-5: oklch(0.769 0.188 70.08);
74
+ --sidebar: oklch(0.985 0 0);
75
+ --sidebar-foreground: oklch(0.145 0 0);
76
+ --sidebar-primary: oklch(0.205 0 0);
77
+ --sidebar-primary-foreground: oklch(0.985 0 0);
78
+ --sidebar-accent: oklch(0.97 0 0);
79
+ --sidebar-accent-foreground: oklch(0.205 0 0);
80
+ --sidebar-border: oklch(0.922 0 0);
81
+ --sidebar-ring: oklch(0.705 0 0);
82
+ }
83
+
84
+ .dark {
85
+ --background: oklch(0.12 0.03 260);
86
+ --foreground: oklch(0.95 0.01 260);
87
+ --card: oklch(0.15 0.03 260);
88
+ --card-foreground: oklch(0.95 0.01 260);
89
+ --popover: oklch(0.15 0.03 260);
90
+ --popover-foreground: oklch(0.95 0.01 260);
91
+ --primary: oklch(0.65 0.2 280);
92
+ --primary-foreground: oklch(0.12 0.03 260);
93
+ --secondary: oklch(0.2 0.04 260);
94
+ --secondary-foreground: oklch(0.95 0.01 260);
95
+ --muted: oklch(0.2 0.04 260);
96
+ --muted-foreground: oklch(0.7 0.04 260);
97
+ --accent: oklch(0.2 0.04 260);
98
+ --accent-foreground: oklch(0.95 0.01 260);
99
+ --destructive: oklch(0.704 0.191 22.216);
100
+ --border: oklch(0.25 0.04 260);
101
+ --input: oklch(0.25 0.04 260);
102
+ --ring: oklch(0.65 0.2 280);
103
+ --chart-1: oklch(0.488 0.243 264.376);
104
+ --chart-2: oklch(0.696 0.17 162.48);
105
+ --chart-3: oklch(0.769 0.188 70.08);
106
+ --chart-4: oklch(0.627 0.265 303.9);
107
+ --chart-5: oklch(0.645 0.246 16.439);
108
+ --sidebar: oklch(0.15 0.03 260);
109
+ --sidebar-foreground: oklch(0.95 0.01 260);
110
+ --sidebar-primary: oklch(0.65 0.2 280);
111
+ --sidebar-primary-foreground: oklch(0.985 0 0);
112
+ --sidebar-accent: oklch(0.2 0.04 260);
113
+ --sidebar-accent-foreground: oklch(0.95 0.01 260);
114
+ --sidebar-border: oklch(0.25 0.04 260);
115
+ --sidebar-ring: oklch(0.65 0.2 280);
116
+ }
117
+
118
+ @layer base {
119
+ * {
120
+ @apply border-border outline-ring/50;
121
+ }
122
+ body {
123
+ @apply bg-background text-foreground;
124
+ }
125
+ }
ui/app/layout.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata, Viewport } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+ import { Sidebar } from "@/components/sidebar";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-geist-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ export const viewport: Viewport = {
17
+ themeColor: [
18
+ { media: "(prefers-color-scheme: light)", color: "white" },
19
+ { media: "(prefers-color-scheme: dark)", color: "#0C0E14" },
20
+ ],
21
+ width: "device-width",
22
+ initialScale: 1,
23
+ };
24
+
25
+ export const metadata: Metadata = {
26
+ title: {
27
+ default: "BioFlow",
28
+ template: "%s | BioFlow",
29
+ },
30
+ description: "AI-Powered Drug Discovery Platform for molecular embedding and binding prediction.",
31
+ keywords: ["Drug Discovery", "AI", "Bioinformatics", "Machine Learning", "Molecular Dynamics"],
32
+ authors: [{ name: "BioFlow Team" }],
33
+ creator: "BioFlow",
34
+ };
35
+
36
+ export default function RootLayout({
37
+ children,
38
+ }: Readonly<{
39
+ children: React.ReactNode;
40
+ }>) {
41
+ return (
42
+ <html lang="en">
43
+ <body
44
+ className={`${geistSans.variable} ${geistMono.variable} antialiased flex h-screen overflow-hidden`}
45
+ >
46
+ <Sidebar />
47
+ <main className="flex-1 overflow-y-auto bg-background p-8">
48
+ <div className="mx-auto max-w-7xl">
49
+ {children}
50
+ </div>
51
+ </main>
52
+ </body>
53
+ </html>
54
+ );
55
+ }
ui/app/loading.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Loader2 } from "lucide-react";
2
+
3
+ export default function Loading() {
4
+ return (
5
+ <div className="flex h-[50vh] w-full items-center justify-center">
6
+ <div className="flex flex-col items-center gap-2">
7
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
8
+ <p className="text-sm text-muted-foreground animate-pulse">Loading BioFlow...</p>
9
+ </div>
10
+ </div>
11
+ );
12
+ }
ui/app/page.tsx ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { Card, CardContent } from "@/components/ui/card"
6
+ import { Separator } from "@/components/ui/separator"
7
+ import { SectionHeader } from "@/components/page-header"
8
+ import { ArrowUp, Zap, Search, Database, FileText, Sparkles } from "lucide-react"
9
+ import Link from "next/link"
10
+
11
+ export default function Home() {
12
+ return (
13
+ <div className="space-y-8 animate-in fade-in duration-500">
14
+ {/* Hero Section */}
15
+ <div className="flex flex-col lg:flex-row gap-6">
16
+ <div className="flex-1 rounded-2xl bg-gradient-to-br from-primary/10 via-background to-background p-8 border">
17
+ <Badge variant="secondary" className="mb-4">New • BioFlow 2.0</Badge>
18
+ <h1 className="text-4xl font-bold tracking-tight mb-4">AI-Powered Drug Discovery</h1>
19
+ <p className="text-lg text-muted-foreground mb-6 max-w-xl">
20
+ Run discovery pipelines, predict binding, and surface evidence in one streamlined workspace.
21
+ </p>
22
+ <div className="flex gap-2 mb-6">
23
+ <Badge variant="outline" className="bg-primary/5 border-primary/20 text-primary">Model-aware search</Badge>
24
+ <Badge variant="outline" className="bg-green-500/10 border-green-500/20 text-green-700 dark:text-green-400">Evidence-linked</Badge>
25
+ <Badge variant="outline" className="bg-amber-500/10 border-amber-500/20 text-amber-700 dark:text-amber-400">Fast iteration</Badge>
26
+ </div>
27
+
28
+ <div className="flex gap-4">
29
+ <Link href="/discovery">
30
+ <Button size="lg" className="font-semibold">
31
+ Start Discovery
32
+ </Button>
33
+ </Link>
34
+ <Link href="/explorer">
35
+ <Button size="lg" variant="outline">
36
+ Explore Data
37
+ </Button>
38
+ </Link>
39
+ </div>
40
+ </div>
41
+
42
+ <div className="lg:w-[350px]">
43
+ <Card className="h-full">
44
+ <CardContent className="p-6 flex flex-col justify-between h-full">
45
+ <div>
46
+ <div className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-2">Today</div>
47
+ <div className="text-4xl font-bold mb-2">156 Discoveries</div>
48
+ <div className="text-sm text-green-600 font-medium flex items-center gap-1">
49
+ <ArrowUp className="h-4 w-4" />
50
+ +12% vs last week
51
+ </div>
52
+ </div>
53
+
54
+ <Separator className="my-4" />
55
+
56
+ <div className="space-y-2">
57
+ <div className="flex items-center justify-between text-sm">
58
+ <span className="flex items-center gap-2">
59
+ <span className="h-2 w-2 rounded-full bg-primary"></span>
60
+ Discovery
61
+ </span>
62
+ <span className="font-mono font-medium">64</span>
63
+ </div>
64
+ <div className="flex items-center justify-between text-sm">
65
+ <span className="flex items-center gap-2">
66
+ <span className="h-2 w-2 rounded-full bg-green-500"></span>
67
+ Prediction
68
+ </span>
69
+ <span className="font-mono font-medium">42</span>
70
+ </div>
71
+ <div className="flex items-center justify-between text-sm">
72
+ <span className="flex items-center gap-2">
73
+ <span className="h-2 w-2 rounded-full bg-amber-500"></span>
74
+ Evidence
75
+ </span>
76
+ <span className="font-mono font-medium">50</span>
77
+ </div>
78
+ </div>
79
+ </CardContent>
80
+ </Card>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Metrics Row */}
85
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
86
+ {[
87
+ { label: "Molecules", value: "12.5M", icon: "🧪", change: "+2.3%", color: "text-blue-500" },
88
+ { label: "Proteins", value: "847K", icon: "🧬", change: "+1.8%", color: "text-cyan-500" },
89
+ { label: "Papers", value: "1.2M", icon: "📚", change: "+5.2%", color: "text-emerald-500" },
90
+ { label: "Discoveries", value: "156", icon: "✨", change: "+12%", color: "text-amber-500" }
91
+ ].map((metric, i) => (
92
+ <Card key={i}>
93
+ <CardContent className="p-6">
94
+ <div className="flex justify-between items-start mb-2">
95
+ <div className="text-sm font-medium text-muted-foreground">{metric.label}</div>
96
+ <div className="text-lg">{metric.icon}</div>
97
+ </div>
98
+ <div className="text-2xl font-bold mb-1">{metric.value}</div>
99
+ <div className="text-xs font-medium flex items-center gap-1 text-green-500">
100
+ <ArrowUp className="h-3 w-3" />
101
+ {metric.change}
102
+ </div>
103
+ </CardContent>
104
+ </Card>
105
+ ))}
106
+ </div>
107
+
108
+ {/* Quick Actions */}
109
+ <div className="pt-4">
110
+ <SectionHeader title="Quick Actions" icon={<Zap className="h-5 w-5 text-amber-500" />} />
111
+
112
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
113
+ <Link href="/discovery" className="block">
114
+ <Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
115
+ <CardContent className="p-6 flex flex-col items-center text-center gap-3">
116
+ <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
117
+ <Search className="h-5 w-5" />
118
+ </div>
119
+ <div>
120
+ <div className="font-semibold">New Discovery</div>
121
+ <div className="text-sm text-muted-foreground">Start a pipeline</div>
122
+ </div>
123
+ </CardContent>
124
+ </Card>
125
+ </Link>
126
+ <Link href="/explorer" className="block">
127
+ <Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
128
+ <CardContent className="p-6 flex flex-col items-center text-center gap-3">
129
+ <div className="h-10 w-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500">
130
+ <Database className="h-5 w-5" />
131
+ </div>
132
+ <div>
133
+ <div className="font-semibold">Browse Data</div>
134
+ <div className="text-sm text-muted-foreground">Explore datasets</div>
135
+ </div>
136
+ </CardContent>
137
+ </Card>
138
+ </Link>
139
+ <Link href="/data" className="block">
140
+ <Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
141
+ <CardContent className="p-6 flex flex-col items-center text-center gap-3">
142
+ <div className="h-10 w-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500">
143
+ <FileText className="h-5 w-5" />
144
+ </div>
145
+ <div>
146
+ <div className="font-semibold">Training</div>
147
+ <div className="text-sm text-muted-foreground">Train new models</div>
148
+ </div>
149
+ </CardContent>
150
+ </Card>
151
+ </Link>
152
+ <Link href="/settings" className="block">
153
+ <Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
154
+ <CardContent className="p-6 flex flex-col items-center text-center gap-3">
155
+ <div className="h-10 w-10 rounded-full bg-slate-500/10 flex items-center justify-center text-slate-500">
156
+ <Sparkles className="h-5 w-5" />
157
+ </div>
158
+ <div>
159
+ <div className="font-semibold">View Insights</div>
160
+ <div className="text-sm text-muted-foreground">Check predictions</div>
161
+ </div>
162
+ </CardContent>
163
+ </Card>
164
+ </Link>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ )
169
+ }
ui/app/settings/page.tsx ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { PageHeader, SectionHeader } from "@/components/page-header"
4
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
8
+ import { Label } from "@/components/ui/label"
9
+ import { Slider } from "@/components/ui/slider"
10
+ import { Switch } from "@/components/ui/switch"
11
+ import { Settings, Brain, Save } from "lucide-react"
12
+
13
+ export default function SettingsPage() {
14
+ return (
15
+ <div className="space-y-8 animate-in fade-in duration-500">
16
+ <PageHeader
17
+ title="Settings"
18
+ subtitle="Configure models, databases, and preferences"
19
+ icon={<Settings className="h-8 w-8 text-primary" />}
20
+ />
21
+
22
+ <Tabs defaultValue="models" className="w-full">
23
+ <TabsList className="w-full justify-start overflow-x-auto">
24
+ <TabsTrigger value="models">Models</TabsTrigger>
25
+ <TabsTrigger value="database">Database</TabsTrigger>
26
+ <TabsTrigger value="api">API Keys</TabsTrigger>
27
+ <TabsTrigger value="appearance">Appearance</TabsTrigger>
28
+ <TabsTrigger value="system">System</TabsTrigger>
29
+ </TabsList>
30
+ <TabsContent value="models" className="space-y-6 mt-6">
31
+ <SectionHeader title="Model Configuration" icon={<Brain className="h-5 w-5 text-primary" />} />
32
+
33
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
34
+ <Card>
35
+ <CardHeader>
36
+ <CardTitle>Embedding Models</CardTitle>
37
+ <CardDescription>Configure models used for molecular and protein embeddings</CardDescription>
38
+ </CardHeader>
39
+ <CardContent className="space-y-4">
40
+ <div className="space-y-2">
41
+ <Label htmlFor="mol-encoder">Molecule Encoder</Label>
42
+ <Select defaultValue="MolCLR">
43
+ <SelectTrigger id="mol-encoder">
44
+ <SelectValue placeholder="Select molecule encoder" />
45
+ </SelectTrigger>
46
+ <SelectContent>
47
+ <SelectItem value="MolCLR">MolCLR (Recommended)</SelectItem>
48
+ <SelectItem value="ChemBERTa">ChemBERTa</SelectItem>
49
+ <SelectItem value="GraphMVP">GraphMVP</SelectItem>
50
+ <SelectItem value="MolBERT">MolBERT</SelectItem>
51
+ </SelectContent>
52
+ </Select>
53
+ </div>
54
+ <div className="space-y-2">
55
+ <Label htmlFor="prot-encoder">Protein Encoder</Label>
56
+ <Select defaultValue="ESM-2">
57
+ <SelectTrigger id="prot-encoder">
58
+ <SelectValue placeholder="Select protein encoder" />
59
+ </SelectTrigger>
60
+ <SelectContent>
61
+ <SelectItem value="ESM-2">ESM-2 (Recommended)</SelectItem>
62
+ <SelectItem value="ProtTrans">ProtTrans</SelectItem>
63
+ <SelectItem value="UniRep">UniRep</SelectItem>
64
+ <SelectItem value="SeqVec">SeqVec</SelectItem>
65
+ </SelectContent>
66
+ </Select>
67
+ </div>
68
+ </CardContent>
69
+ </Card>
70
+
71
+ <Card>
72
+ <CardHeader>
73
+ <CardTitle>Prediction Heads</CardTitle>
74
+ <CardDescription>Configure downstream task predictors</CardDescription>
75
+ </CardHeader>
76
+ <CardContent className="space-y-4">
77
+ <div className="space-y-2">
78
+ <Label htmlFor="binding">Binding Predictor</Label>
79
+ <Select defaultValue="DrugBAN">
80
+ <SelectTrigger id="binding">
81
+ <SelectValue placeholder="Select predictor" />
82
+ </SelectTrigger>
83
+ <SelectContent>
84
+ <SelectItem value="DrugBAN">DrugBAN (Recommended)</SelectItem>
85
+ <SelectItem value="DeepDTA">DeepDTA</SelectItem>
86
+ <SelectItem value="GraphDTA">GraphDTA</SelectItem>
87
+ <SelectItem value="Custom">Custom</SelectItem>
88
+ </SelectContent>
89
+ </Select>
90
+ </div>
91
+ <div className="space-y-2">
92
+ <Label htmlFor="property">Property Predictor</Label>
93
+ <Select defaultValue="ADMET-AI">
94
+ <SelectTrigger id="property">
95
+ <SelectValue placeholder="Select predictor" />
96
+ </SelectTrigger>
97
+ <SelectContent>
98
+ <SelectItem value="ADMET-AI">ADMET-AI (Recommended)</SelectItem>
99
+ <SelectItem value="ChemProp">ChemProp</SelectItem>
100
+ <SelectItem value="Custom">Custom</SelectItem>
101
+ </SelectContent>
102
+ </Select>
103
+ </div>
104
+ </CardContent>
105
+ </Card>
106
+ </div>
107
+
108
+ <Card>
109
+ <CardHeader>
110
+ <CardTitle>LLM Settings</CardTitle>
111
+ <CardDescription>Configure language models for evidence retrieval and reasoning</CardDescription>
112
+ </CardHeader>
113
+ <CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
114
+ <div className="space-y-2">
115
+ <Label htmlFor="llm-provider">LLM Provider</Label>
116
+ <Select defaultValue="OpenAI">
117
+ <SelectTrigger id="llm-provider">
118
+ <SelectValue placeholder="Select provider" />
119
+ </SelectTrigger>
120
+ <SelectContent>
121
+ <SelectItem value="OpenAI">OpenAI</SelectItem>
122
+ <SelectItem value="Anthropic">Anthropic</SelectItem>
123
+ <SelectItem value="Local">Local (Ollama)</SelectItem>
124
+ <SelectItem value="Azure">Azure OpenAI</SelectItem>
125
+ </SelectContent>
126
+ </Select>
127
+ </div>
128
+ <div className="space-y-2">
129
+ <Label htmlFor="llm-model">Model</Label>
130
+ <Select defaultValue="GPT-4o">
131
+ <SelectTrigger id="llm-model">
132
+ <SelectValue placeholder="Select model" />
133
+ </SelectTrigger>
134
+ <SelectContent>
135
+ <SelectItem value="GPT-4o">GPT-4o</SelectItem>
136
+ <SelectItem value="GPT-4-turbo">GPT-4-turbo</SelectItem>
137
+ <SelectItem value="Claude 3.5">Claude 3.5 Sonnet</SelectItem>
138
+ <SelectItem value="Llama 3">Llama 3.1 70B</SelectItem>
139
+ </SelectContent>
140
+ </Select>
141
+ </div>
142
+ <div className="col-span-1 md:col-span-2 space-y-4">
143
+ <div className="space-y-2">
144
+ <div className="flex items-center justify-between">
145
+ <Label>Temperature: 0.7</Label>
146
+ <span className="text-xs text-muted-foreground">Creativity vs Precision</span>
147
+ </div>
148
+ <Slider defaultValue={[0.7]} max={1} step={0.1} />
149
+ </div>
150
+ <div className="flex items-center space-x-2">
151
+ <Switch id="stream" defaultChecked />
152
+ <Label htmlFor="stream">Stream Responses</Label>
153
+ </div>
154
+ </div>
155
+ </CardContent>
156
+ </Card>
157
+ </TabsContent>
158
+
159
+ <TabsContent value="appearance">
160
+ <Card>
161
+ <CardContent className="p-12 text-center text-muted-foreground">
162
+ Theme settings coming soon.
163
+ </CardContent>
164
+ </Card>
165
+ </TabsContent>
166
+
167
+ <TabsContent value="database">
168
+ <Card>
169
+ <CardContent className="p-12 text-center text-muted-foreground">
170
+ Database connection settings.
171
+ </CardContent>
172
+ </Card>
173
+ </TabsContent>
174
+
175
+ <TabsContent value="api">
176
+ <Card>
177
+ <CardContent className="p-12 text-center text-muted-foreground">
178
+ API Key configuration.
179
+ </CardContent>
180
+ </Card>
181
+ </TabsContent>
182
+ </Tabs>
183
+
184
+ <div className="fixed bottom-6 right-6">
185
+ <Button size="lg" className="shadow-2xl">
186
+ <Save className="mr-2 h-4 w-4" />
187
+ Save Changes
188
+ </Button>
189
+ </div>
190
+ </div>
191
+ )
192
+ }
ui/components.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {
22
+ "@animate-ui": "https://animate-ui.com/r/{name}.json"
23
+ }
24
+ }
ui/components/page-header.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/components/page-header.tsx
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface PageHeaderProps {
5
+ title: string
6
+ subtitle?: string
7
+ icon?: React.ReactNode
8
+ className?: string
9
+ }
10
+
11
+ export function PageHeader({ title, subtitle, icon, className }: PageHeaderProps) {
12
+ return (
13
+ <div className={cn("mb-8 space-y-2", className)}>
14
+ <h1 className="flex items-center gap-3 text-3xl font-bold tracking-tight">
15
+ {icon && <span className="text-primary">{icon}</span>}
16
+ {title}
17
+ </h1>
18
+ {subtitle && (
19
+ <p className="text-lg text-muted-foreground">
20
+ {subtitle}
21
+ </p>
22
+ )}
23
+ </div>
24
+ )
25
+ }
26
+
27
+ export function SectionHeader({ title, icon, action }: { title: string, icon?: React.ReactNode, action?: React.ReactNode }) {
28
+ return (
29
+ <div className="flex items-center justify-between mb-6">
30
+ <h2 className="text-xl font-semibold flex items-center gap-2">
31
+ {icon}
32
+ {title}
33
+ </h2>
34
+ {action}
35
+ </div>
36
+ )
37
+ }
ui/components/sidebar.tsx ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/components/sidebar.tsx
2
+ "use client"
3
+
4
+ import Link from "next/link"
5
+ import { usePathname } from "next/navigation"
6
+ import { Home, Microscope, Dna, BarChart2, Settings, Terminal } from "lucide-react"
7
+
8
+ import { cn } from "@/lib/utils"
9
+ import { Button } from "@/components/ui/button"
10
+
11
+ const sidebarItems = [
12
+ {
13
+ title: "Home",
14
+ href: "/",
15
+ icon: Home,
16
+ },
17
+ {
18
+ title: "Discovery",
19
+ href: "/discovery",
20
+ icon: Microscope,
21
+ },
22
+ {
23
+ title: "Explorer",
24
+ href: "/explorer",
25
+ icon: Dna,
26
+ },
27
+ {
28
+ title: "Data",
29
+ href: "/data",
30
+ icon: BarChart2,
31
+ },
32
+ {
33
+ title: "Settings",
34
+ href: "/settings",
35
+ icon: Settings,
36
+ },
37
+ ]
38
+
39
+ export function Sidebar() {
40
+ const pathname = usePathname()
41
+
42
+ return (
43
+ <div className="flex h-screen w-[250px] flex-col border-r bg-card pb-4 pt-6">
44
+ <div className="px-6 mb-8 flex items-center gap-2">
45
+ <div className="h-8 w-8 rounded-lg bg-primary/20 flex items-center justify-center text-primary">
46
+ <Dna className="h-5 w-5" />
47
+ </div>
48
+ <div className="font-bold text-xl tracking-tight">
49
+ Bio<span className="text-primary">Flow</span>
50
+ </div>
51
+ </div>
52
+
53
+ <div className="px-4 py-2">
54
+ <div className="text-xs font-semibold text-muted-foreground mb-4 px-2 uppercase tracking-wider">
55
+ Navigation
56
+ </div>
57
+ <nav className="space-y-1">
58
+ {sidebarItems.map((item) => (
59
+ <Link
60
+ key={item.href}
61
+ href={item.href}
62
+ >
63
+ <Button
64
+ variant={pathname === item.href ? "secondary" : "ghost"}
65
+ className={cn(
66
+ "w-full justify-start gap-3",
67
+ pathname === item.href && "bg-secondary font-medium"
68
+ )}
69
+ >
70
+ <item.icon className="h-4 w-4" />
71
+ {item.title}
72
+ </Button>
73
+ </Link>
74
+ ))}
75
+ </nav>
76
+ </div>
77
+
78
+ <div className="mt-auto px-4">
79
+ <div className="rounded-lg border bg-muted/50 p-4">
80
+ <div className="flex items-center gap-2 mb-2">
81
+ <Terminal className="h-4 w-4 text-muted-foreground" />
82
+ <span className="text-xs font-medium">Status</span>
83
+ </div>
84
+ <div className="flex items-center gap-2">
85
+ <span className="relative flex h-2 w-2">
86
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
87
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
88
+ </span>
89
+ <span className="text-xs text-muted-foreground">System Online</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ )
95
+ }
ui/components/ui/badge.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/components/ui/badge.tsx
2
+ import * as React from "react"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
16
+ destructive:
17
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
18
+ outline: "text-foreground",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "default",
23
+ },
24
+ }
25
+ )
26
+
27
+ export interface BadgeProps
28
+ extends React.HTMLAttributes<HTMLDivElement>,
29
+ VariantProps<typeof badgeVariants> {}
30
+
31
+ function Badge({ className, variant, ...props }: BadgeProps) {
32
+ return (
33
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
34
+ )
35
+ }
36
+
37
+ export { Badge, badgeVariants }
ui/components/ui/button.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/components/ui/button.tsx
2
+ import * as React from "react"
3
+ import { Slot } from "@radix-ui/react-slot"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const buttonVariants = cva(
9
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default:
14
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
15
+ destructive:
16
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
17
+ outline:
18
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
19
+ secondary:
20
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
21
+ ghost: "hover:bg-accent hover:text-accent-foreground",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2",
26
+ sm: "h-8 rounded-md px-3 text-xs",
27
+ lg: "h-10 rounded-md px-8",
28
+ icon: "h-9 w-9",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ }
36
+ )
37
+
38
+ export interface ButtonProps
39
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
40
+ VariantProps<typeof buttonVariants> {
41
+ asChild?: boolean
42
+ }
43
+
44
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
45
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
46
+ const Comp = asChild ? Slot : "button"
47
+ return (
48
+ <Comp
49
+ className={cn(buttonVariants({ variant, size, className }))}
50
+ ref={ref}
51
+ {...props}
52
+ />
53
+ )
54
+ }
55
+ )
56
+ Button.displayName = "Button"
57
+
58
+ export { Button, buttonVariants }
ui/components/ui/card.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ui/components/ui/card.tsx
2
+ import * as React from "react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Card = React.forwardRef<
7
+ HTMLDivElement,
8
+ React.HTMLAttributes<HTMLDivElement>
9
+ >(({ className, ...props }, ref) => (
10
+ <div
11
+ ref={ref}
12
+ className={cn(
13
+ "rounded-xl border bg-card text-card-foreground shadow",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ ))
19
+ Card.displayName = "Card"
20
+
21
+ const CardHeader = React.forwardRef<
22
+ HTMLDivElement,
23
+ React.HTMLAttributes<HTMLDivElement>
24
+ >(({ className, ...props }, ref) => (
25
+ <div
26
+ ref={ref}
27
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
28
+ {...props}
29
+ />
30
+ ))
31
+ CardHeader.displayName = "CardHeader"
32
+
33
+ const CardTitle = React.forwardRef<
34
+ HTMLDivElement,
35
+ React.HTMLAttributes<HTMLDivElement>
36
+ >(({ className, ...props }, ref) => (
37
+ <div
38
+ ref={ref}
39
+ className={cn("font-semibold leading-none tracking-tight", className)}
40
+ {...props}
41
+ />
42
+ ))
43
+ CardTitle.displayName = "CardTitle"
44
+
45
+ const CardDescription = React.forwardRef<
46
+ HTMLDivElement,
47
+ React.HTMLAttributes<HTMLDivElement>
48
+ >(({ className, ...props }, ref) => (
49
+ <div
50
+ ref={ref}
51
+ className={cn("text-sm text-muted-foreground", className)}
52
+ {...props}
53
+ />
54
+ ))
55
+ CardDescription.displayName = "CardDescription"
56
+
57
+ const CardContent = React.forwardRef<
58
+ HTMLDivElement,
59
+ React.HTMLAttributes<HTMLDivElement>
60
+ >(({ className, ...props }, ref) => (
61
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
62
+ ))
63
+ CardContent.displayName = "CardContent"
64
+
65
+ const CardFooter = React.forwardRef<
66
+ HTMLDivElement,
67
+ React.HTMLAttributes<HTMLDivElement>
68
+ >(({ className, ...props }, ref) => (
69
+ <div
70
+ ref={ref}
71
+ className={cn("flex items-center p-6 pt-0", className)}
72
+ {...props}
73
+ />
74
+ ))
75
+ CardFooter.displayName = "CardFooter"
76
+
77
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
ui/components/ui/input.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ export { Input }
ui/components/ui/label.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export { Label }
ui/components/ui/select.tsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SelectPrimitive from "@radix-ui/react-select"
5
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Select({
10
+ ...props
11
+ }: React.ComponentProps<typeof SelectPrimitive.Root>) {
12
+ return <SelectPrimitive.Root data-slot="select" {...props} />
13
+ }
14
+
15
+ function SelectGroup({
16
+ ...props
17
+ }: React.ComponentProps<typeof SelectPrimitive.Group>) {
18
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />
19
+ }
20
+
21
+ function SelectValue({
22
+ ...props
23
+ }: React.ComponentProps<typeof SelectPrimitive.Value>) {
24
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />
25
+ }
26
+
27
+ function SelectTrigger({
28
+ className,
29
+ size = "default",
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
33
+ size?: "sm" | "default"
34
+ }) {
35
+ return (
36
+ <SelectPrimitive.Trigger
37
+ data-slot="select-trigger"
38
+ data-size={size}
39
+ className={cn(
40
+ "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ {children}
46
+ <SelectPrimitive.Icon asChild>
47
+ <ChevronDownIcon className="size-4 opacity-50" />
48
+ </SelectPrimitive.Icon>
49
+ </SelectPrimitive.Trigger>
50
+ )
51
+ }
52
+
53
+ function SelectContent({
54
+ className,
55
+ children,
56
+ position = "item-aligned",
57
+ align = "center",
58
+ ...props
59
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
60
+ return (
61
+ <SelectPrimitive.Portal>
62
+ <SelectPrimitive.Content
63
+ data-slot="select-content"
64
+ className={cn(
65
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
66
+ position === "popper" &&
67
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
68
+ className
69
+ )}
70
+ position={position}
71
+ align={align}
72
+ {...props}
73
+ >
74
+ <SelectScrollUpButton />
75
+ <SelectPrimitive.Viewport
76
+ className={cn(
77
+ "p-1",
78
+ position === "popper" &&
79
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
80
+ )}
81
+ >
82
+ {children}
83
+ </SelectPrimitive.Viewport>
84
+ <SelectScrollDownButton />
85
+ </SelectPrimitive.Content>
86
+ </SelectPrimitive.Portal>
87
+ )
88
+ }
89
+
90
+ function SelectLabel({
91
+ className,
92
+ ...props
93
+ }: React.ComponentProps<typeof SelectPrimitive.Label>) {
94
+ return (
95
+ <SelectPrimitive.Label
96
+ data-slot="select-label"
97
+ className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ function SelectItem({
104
+ className,
105
+ children,
106
+ ...props
107
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
108
+ return (
109
+ <SelectPrimitive.Item
110
+ data-slot="select-item"
111
+ className={cn(
112
+ "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
113
+ className
114
+ )}
115
+ {...props}
116
+ >
117
+ <span
118
+ data-slot="select-item-indicator"
119
+ className="absolute right-2 flex size-3.5 items-center justify-center"
120
+ >
121
+ <SelectPrimitive.ItemIndicator>
122
+ <CheckIcon className="size-4" />
123
+ </SelectPrimitive.ItemIndicator>
124
+ </span>
125
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
126
+ </SelectPrimitive.Item>
127
+ )
128
+ }
129
+
130
+ function SelectSeparator({
131
+ className,
132
+ ...props
133
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
134
+ return (
135
+ <SelectPrimitive.Separator
136
+ data-slot="select-separator"
137
+ className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
138
+ {...props}
139
+ />
140
+ )
141
+ }
142
+
143
+ function SelectScrollUpButton({
144
+ className,
145
+ ...props
146
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
147
+ return (
148
+ <SelectPrimitive.ScrollUpButton
149
+ data-slot="select-scroll-up-button"
150
+ className={cn(
151
+ "flex cursor-default items-center justify-center py-1",
152
+ className
153
+ )}
154
+ {...props}
155
+ >
156
+ <ChevronUpIcon className="size-4" />
157
+ </SelectPrimitive.ScrollUpButton>
158
+ )
159
+ }
160
+
161
+ function SelectScrollDownButton({
162
+ className,
163
+ ...props
164
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
165
+ return (
166
+ <SelectPrimitive.ScrollDownButton
167
+ data-slot="select-scroll-down-button"
168
+ className={cn(
169
+ "flex cursor-default items-center justify-center py-1",
170
+ className
171
+ )}
172
+ {...props}
173
+ >
174
+ <ChevronDownIcon className="size-4" />
175
+ </SelectPrimitive.ScrollDownButton>
176
+ )
177
+ }
178
+
179
+ export {
180
+ Select,
181
+ SelectContent,
182
+ SelectGroup,
183
+ SelectItem,
184
+ SelectLabel,
185
+ SelectScrollDownButton,
186
+ SelectScrollUpButton,
187
+ SelectSeparator,
188
+ SelectTrigger,
189
+ SelectValue,
190
+ }
ui/components/ui/separator.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SeparatorPrimitive from "@radix-ui/react-separator"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Separator({
9
+ className,
10
+ orientation = "horizontal",
11
+ decorative = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
14
+ return (
15
+ <SeparatorPrimitive.Root
16
+ data-slot="separator"
17
+ decorative={decorative}
18
+ orientation={orientation}
19
+ className={cn(
20
+ "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ export { Separator }
ui/components/ui/skeleton.tsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("bg-accent animate-pulse rounded-md", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
ui/components/ui/slider.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SliderPrimitive from "@radix-ui/react-slider"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Slider({
9
+ className,
10
+ defaultValue,
11
+ value,
12
+ min = 0,
13
+ max = 100,
14
+ ...props
15
+ }: React.ComponentProps<typeof SliderPrimitive.Root>) {
16
+ const _values = React.useMemo(
17
+ () =>
18
+ Array.isArray(value)
19
+ ? value
20
+ : Array.isArray(defaultValue)
21
+ ? defaultValue
22
+ : [min, max],
23
+ [value, defaultValue, min, max]
24
+ )
25
+
26
+ return (
27
+ <SliderPrimitive.Root
28
+ data-slot="slider"
29
+ defaultValue={defaultValue}
30
+ value={value}
31
+ min={min}
32
+ max={max}
33
+ className={cn(
34
+ "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ <SliderPrimitive.Track
40
+ data-slot="slider-track"
41
+ className={cn(
42
+ "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
43
+ )}
44
+ >
45
+ <SliderPrimitive.Range
46
+ data-slot="slider-range"
47
+ className={cn(
48
+ "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
49
+ )}
50
+ />
51
+ </SliderPrimitive.Track>
52
+ {Array.from({ length: _values.length }, (_, index) => (
53
+ <SliderPrimitive.Thumb
54
+ data-slot="slider-thumb"
55
+ key={index}
56
+ className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
57
+ />
58
+ ))}
59
+ </SliderPrimitive.Root>
60
+ )
61
+ }
62
+
63
+ export { Slider }
ui/components/ui/switch.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SwitchPrimitive from "@radix-ui/react-switch"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Switch({
9
+ className,
10
+ size = "default",
11
+ ...props
12
+ }: React.ComponentProps<typeof SwitchPrimitive.Root> & {
13
+ size?: "sm" | "default"
14
+ }) {
15
+ return (
16
+ <SwitchPrimitive.Root
17
+ data-slot="switch"
18
+ data-size={size}
19
+ className={cn(
20
+ "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ <SwitchPrimitive.Thumb
26
+ data-slot="switch-thumb"
27
+ className={cn(
28
+ "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
29
+ )}
30
+ />
31
+ </SwitchPrimitive.Root>
32
+ )
33
+ }
34
+
35
+ export { Switch }
ui/components/ui/table.tsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
8
+ return (
9
+ <div
10
+ data-slot="table-container"
11
+ className="relative w-full overflow-x-auto"
12
+ >
13
+ <table
14
+ data-slot="table"
15
+ className={cn("w-full caption-bottom text-sm", className)}
16
+ {...props}
17
+ />
18
+ </div>
19
+ )
20
+ }
21
+
22
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
23
+ return (
24
+ <thead
25
+ data-slot="table-header"
26
+ className={cn("[&_tr]:border-b", className)}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
33
+ return (
34
+ <tbody
35
+ data-slot="table-body"
36
+ className={cn("[&_tr:last-child]:border-0", className)}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
43
+ return (
44
+ <tfoot
45
+ data-slot="table-footer"
46
+ className={cn(
47
+ "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56
+ return (
57
+ <tr
58
+ data-slot="table-row"
59
+ className={cn(
60
+ "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69
+ return (
70
+ <th
71
+ data-slot="table-head"
72
+ className={cn(
73
+ "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
74
+ className
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82
+ return (
83
+ <td
84
+ data-slot="table-cell"
85
+ className={cn(
86
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function TableCaption({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<"caption">) {
98
+ return (
99
+ <caption
100
+ data-slot="table-caption"
101
+ className={cn("text-muted-foreground mt-4 text-sm", className)}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ export {
108
+ Table,
109
+ TableHeader,
110
+ TableBody,
111
+ TableFooter,
112
+ TableHead,
113
+ TableRow,
114
+ TableCell,
115
+ TableCaption,
116
+ }
ui/components/ui/tabs.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Tabs({
10
+ className,
11
+ orientation = "horizontal",
12
+ ...props
13
+ }: React.ComponentProps<typeof TabsPrimitive.Root>) {
14
+ return (
15
+ <TabsPrimitive.Root
16
+ data-slot="tabs"
17
+ data-orientation={orientation}
18
+ orientation={orientation}
19
+ className={cn(
20
+ "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ const tabsListVariants = cva(
29
+ "rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
30
+ {
31
+ variants: {
32
+ variant: {
33
+ default: "bg-muted",
34
+ line: "gap-1 bg-transparent",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ },
40
+ }
41
+ )
42
+
43
+ function TabsList({
44
+ className,
45
+ variant = "default",
46
+ ...props
47
+ }: React.ComponentProps<typeof TabsPrimitive.List> &
48
+ VariantProps<typeof tabsListVariants>) {
49
+ return (
50
+ <TabsPrimitive.List
51
+ data-slot="tabs-list"
52
+ data-variant={variant}
53
+ className={cn(tabsListVariants({ variant }), className)}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ function TabsTrigger({
60
+ className,
61
+ ...props
62
+ }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
63
+ return (
64
+ <TabsPrimitive.Trigger
65
+ data-slot="tabs-trigger"
66
+ className={cn(
67
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
68
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
69
+ "data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
70
+ "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
71
+ className
72
+ )}
73
+ {...props}
74
+ />
75
+ )
76
+ }
77
+
78
+ function TabsContent({
79
+ className,
80
+ ...props
81
+ }: React.ComponentProps<typeof TabsPrimitive.Content>) {
82
+ return (
83
+ <TabsPrimitive.Content
84
+ data-slot="tabs-content"
85
+ className={cn("flex-1 outline-none", className)}
86
+ {...props}
87
+ />
88
+ )
89
+ }
90
+
91
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
ui/components/ui/textarea.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
6
+ export interface TextareaProps
7
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
8
+
9
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
10
+ ({ className, ...props }, ref) => {
11
+ return (
12
+ <textarea
13
+ className={cn(
14
+ "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
15
+ className
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+ )
23
+ Textarea.displayName = "Textarea"
24
+
25
+ export { Textarea }
ui/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
ui/hooks/use-controlled-state.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+
3
+ interface CommonControlledStateProps<T> {
4
+ value?: T;
5
+ defaultValue?: T;
6
+ }
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ export function useControlledState<T, Rest extends any[] = []>(
10
+ props: CommonControlledStateProps<T> & {
11
+ onChange?: (value: T, ...args: Rest) => void;
12
+ },
13
+ ): readonly [T, (next: T, ...args: Rest) => void] {
14
+ const { value, defaultValue, onChange } = props;
15
+
16
+ const [state, setInternalState] = React.useState<T>(
17
+ value !== undefined ? value : (defaultValue as T),
18
+ );
19
+
20
+ React.useEffect(() => {
21
+ if (value !== undefined) setInternalState(value);
22
+ }, [value]);
23
+
24
+ const setState = React.useCallback(
25
+ (next: T, ...args: Rest) => {
26
+ setInternalState(next);
27
+ onChange?.(next, ...args);
28
+ },
29
+ [onChange],
30
+ );
31
+
32
+ return [state, setState] as const;
33
+ }
ui/hooks/use-mobile.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
ui/lib/api-utils.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export function getApiUrl() {
2
+ if (typeof window !== "undefined") return ""; // browser should use relative url
3
+ if (process.env.BACKEND_URL) return `https://${process.env.BACKEND_URL}`; // SSR should use vercel url
4
+ return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
5
+ }
ui/lib/data-service.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DataResponse } from "@/types/data";
2
+
3
+ export async function getData(): Promise<DataResponse> {
4
+ // Mock data simulation - replacing actual database/API call
5
+ const datasets = [
6
+ { name: "DrugBank Compounds", type: "Molecules", count: "12,450", size: "45.2 MB", updated: "2024-01-15" },
7
+ { name: "ChEMBL Kinase Inhibitors", type: "Molecules", count: "8,234", size: "32.1 MB", updated: "2024-01-10" },
8
+ { name: "Custom Protein Targets", type: "Proteins", count: "1,245", size: "78.5 MB", updated: "2024-01-08" },
9
+ ];
10
+
11
+ const stats = {
12
+ datasets: 5,
13
+ molecules: "24.5K",
14
+ proteins: "1.2K",
15
+ storage: "156 MB",
16
+ };
17
+
18
+ return Promise.resolve({ datasets, stats });
19
+ }
ui/lib/explorer-service.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ExplorerResponse, DataPoint } from "@/types/explorer";
2
+ import { ExplorerRequestSchema } from "@/schemas/explorer";
3
+
4
+ const clusters = [
5
+ { cx: 2, cy: 3, color: "var(--color-chart-1)" },
6
+ { cx: -2, cy: -1, color: "var(--color-chart-2)" },
7
+ { cx: 4, cy: -2, color: "var(--color-chart-3)" },
8
+ { cx: -1, cy: 4, color: "var(--color-chart-4)" },
9
+ ];
10
+
11
+ export async function getExplorerPoints(
12
+ dataset?: string,
13
+ view?: string,
14
+ colorBy?: string
15
+ ): Promise<ExplorerResponse> {
16
+ // Validate params using the schema to ensure defaults are applied if needed, even for internal calls
17
+ // Note: safeParse is synchronous, but we can treat this as an async service
18
+ const result = ExplorerRequestSchema.safeParse({
19
+ dataset,
20
+ view,
21
+ colorBy,
22
+ });
23
+
24
+ if (!result.success) {
25
+ // Return empty/default response or throw type-safe error
26
+ // For now, adhering to existing behavior, we can just throw or fallback
27
+ throw new Error("Invalid parameters");
28
+ }
29
+
30
+
31
+ // TODO: replace mock generation with backend embeddings
32
+ const points: DataPoint[] = [];
33
+ for (let i = 0; i < 200; i++) {
34
+ const cluster = clusters[Math.floor(i / 50)];
35
+ points.push({
36
+ x: cluster.cx + (Math.random() - 0.5) * 2,
37
+ y: cluster.cy + (Math.random() - 0.5) * 2,
38
+ z: Math.random() * 100,
39
+ color: cluster.color,
40
+ name: `Mol_${i}`,
41
+ affinity: Math.random() * 100,
42
+ });
43
+ }
44
+
45
+ const metrics = {
46
+ activeMolecules: 12450,
47
+ clusters: 4,
48
+ avgConfidence: 0.89,
49
+ };
50
+
51
+ return Promise.resolve({ points, metrics });
52
+ }
ui/lib/get-strict-context.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react';
2
+
3
+ function getStrictContext<T>(
4
+ name?: string,
5
+ ): readonly [
6
+ ({
7
+ value,
8
+ children,
9
+ }: {
10
+ value: T;
11
+ children?: React.ReactNode;
12
+ }) => React.JSX.Element,
13
+ () => T,
14
+ ] {
15
+ const Context = React.createContext<T | undefined>(undefined);
16
+
17
+ const Provider = ({
18
+ value,
19
+ children,
20
+ }: {
21
+ value: T;
22
+ children?: React.ReactNode;
23
+ }) => <Context.Provider value={value}>{children}</Context.Provider>;
24
+
25
+ const useSafeContext = () => {
26
+ const ctx = React.useContext(Context);
27
+ if (ctx === undefined) {
28
+ throw new Error(`useContext must be used within ${name ?? 'a Provider'}`);
29
+ }
30
+ return ctx;
31
+ };
32
+
33
+ return [Provider, useSafeContext] as const;
34
+ }
35
+
36
+ export { getStrictContext };
ui/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
ui/next.config.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ reactStrictMode: true,
5
+ poweredByHeader: false,
6
+ productionBrowserSourceMaps: false,
7
+ compress: true,
8
+ cacheComponents: true,
9
+ turbopack: {},
10
+ experimental: {
11
+ turbopackFileSystemCacheForDev: true,
12
+ turbopackFileSystemCacheForBuild: false,
13
+ },
14
+ images: {
15
+ formats: ["image/avif", "image/webp"],
16
+ remotePatterns: [],
17
+ dangerouslyAllowSVG: false,
18
+ },
19
+ async headers() {
20
+ return [
21
+ {
22
+ source: "/:path*",
23
+ headers: [
24
+ { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
25
+ { key: "X-Frame-Options", value: "DENY" },
26
+ { key: "X-Content-Type-Options", value: "nosniff" },
27
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
28
+ { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
29
+ { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
30
+ { key: "Cross-Origin-Resource-Policy", value: "same-origin" },
31
+ ],
32
+ },
33
+ ];
34
+ },
35
+ };
36
+
37
+ export default nextConfig;
ui/package.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ui",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint .",
10
+ "type-check": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@radix-ui/react-label": "^2.1.8",
14
+ "@radix-ui/react-select": "^2.2.6",
15
+ "@radix-ui/react-separator": "^1.1.8",
16
+ "@radix-ui/react-slider": "^1.3.6",
17
+ "@radix-ui/react-slot": "^1.2.4",
18
+ "@radix-ui/react-switch": "^1.2.6",
19
+ "@radix-ui/react-tabs": "^1.1.13",
20
+ "class-variance-authority": "^0.7.1",
21
+ "clsx": "^2.1.1",
22
+ "framer-motion": "^12.29.2",
23
+ "lucide-react": "^0.563.0",
24
+ "motion": "^12.29.2",
25
+ "next": "^16.1.4",
26
+ "radix-ui": "^1.4.3",
27
+ "react": "^19.2.3",
28
+ "react-dom": "^19.2.3",
29
+ "recharts": "^3.7.0",
30
+ "tailwind-merge": "^3.4.0",
31
+ "zod": "^4.3.6"
32
+ },
33
+ "devDependencies": {
34
+ "@tailwindcss/postcss": "^4.1.18",
35
+ "@types/node": "^25.0.10",
36
+ "@types/react": "^19.2.9",
37
+ "@types/react-dom": "^19.2.3",
38
+ "eslint": "^9.39.2",
39
+ "eslint-config-next": "^16.1.4",
40
+ "shadcn": "^3.7.0",
41
+ "tailwindcss": "^4.1.18",
42
+ "tw-animate-css": "^1.4.0",
43
+ "typescript": "^5.9.3"
44
+ }
45
+ }
ui/pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
ui/pnpm-workspace.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ packages:
2
+ - .
ui/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
ui/public/file.svg ADDED
ui/public/globe.svg ADDED