Avijit Ghosh commited on
Commit
509e21e
·
1 Parent(s): bad84a3

added all the new files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +11 -0
  2. .gitignore +30 -0
  3. Dockerfile +38 -0
  4. README.md +90 -5
  5. app/evaluation/[id]/generateStaticParams.ts +15 -0
  6. app/evaluation/[id]/page.client.tsx +711 -0
  7. app/evaluation/[id]/page.tsx +9 -0
  8. app/evaluation/[id]/server.ts +1 -0
  9. app/globals.css +127 -0
  10. app/layout.tsx +39 -0
  11. app/page.tsx +566 -0
  12. components.json +21 -0
  13. components/ai-evaluation-dashboard.tsx +350 -0
  14. components/category-evaluation.tsx +934 -0
  15. components/category-selection.tsx +246 -0
  16. components/evaluation-card.tsx +445 -0
  17. components/evaluation-form.tsx +190 -0
  18. components/results-dashboard.tsx +428 -0
  19. components/system-info-form.tsx +249 -0
  20. components/theme-provider.tsx +7 -0
  21. components/ui/accordion.tsx +66 -0
  22. components/ui/alert-dialog.tsx +157 -0
  23. components/ui/alert.tsx +66 -0
  24. components/ui/aspect-ratio.tsx +11 -0
  25. components/ui/avatar.tsx +53 -0
  26. components/ui/badge.tsx +46 -0
  27. components/ui/breadcrumb.tsx +109 -0
  28. components/ui/button.tsx +59 -0
  29. components/ui/calendar.tsx +213 -0
  30. components/ui/card.tsx +92 -0
  31. components/ui/carousel.tsx +241 -0
  32. components/ui/chart.tsx +325 -0
  33. components/ui/checkbox.tsx +32 -0
  34. components/ui/collapsible.tsx +33 -0
  35. components/ui/command.tsx +184 -0
  36. components/ui/context-menu.tsx +252 -0
  37. components/ui/dialog.tsx +143 -0
  38. components/ui/drawer.tsx +135 -0
  39. components/ui/dropdown-menu.tsx +257 -0
  40. components/ui/form.tsx +167 -0
  41. components/ui/hover-card.tsx +44 -0
  42. components/ui/input-otp.tsx +77 -0
  43. components/ui/input.tsx +21 -0
  44. components/ui/label.tsx +24 -0
  45. components/ui/menubar.tsx +276 -0
  46. components/ui/navigation-menu.tsx +168 -0
  47. components/ui/pagination.tsx +127 -0
  48. components/ui/popover.tsx +48 -0
  49. components/ui/progress.tsx +31 -0
  50. components/ui/radio-group.tsx +45 -0
.dockerignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ .next
3
+ .git
4
+ .gitignore
5
+ Dockerfile
6
+ README.md
7
+ out
8
+ .env
9
+ .vscode
10
+ .idea
11
+ *.log
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ *.DS_Store
4
+
5
+ # dependencies
6
+ /node_modules
7
+
8
+ # next.js
9
+ /.next/
10
+ /out/
11
+
12
+ # production
13
+ /build
14
+ /out
15
+
16
+ # debug
17
+ npm-debug.log*
18
+ yarn-debug.log*
19
+ yarn-error.log*
20
+ .pnpm-debug.log*
21
+
22
+ # env files
23
+ .env*
24
+
25
+ # vercel
26
+ .vercel
27
+
28
+ # typescript
29
+ *.tsbuildinfo
30
+ next-env.d.ts
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Multi-stage Dockerfile for Next.js app (suitable for Hugging Face Spaces Docker runtime)
2
+ # - Builder stage installs deps and builds the Next app
3
+ # - Runner stage copies build artifacts and runs `npm run start` on $PORT (default 3000)
4
+
5
+ FROM node:18-bullseye-slim AS builder
6
+ WORKDIR /app
7
+
8
+ # install build deps and copy package files first for caching
9
+ COPY package*.json ./
10
+ RUN npm ci --silent
11
+
12
+ # copy source and build
13
+ COPY . ./
14
+ RUN npm run build
15
+
16
+ FROM node:18-bullseye-slim AS runner
17
+ WORKDIR /app
18
+
19
+ ENV NODE_ENV=production
20
+ ENV PORT=3000
21
+
22
+ # minimal packages for certificates (if needed by model download / https)
23
+ RUN apt-get update && apt-get install -y ca-certificates --no-install-recommends && rm -rf /var/lib/apt/lists/*
24
+
25
+ # copy runtime artifacts from builder
26
+ COPY --from=builder /app/package*.json ./
27
+ COPY --from=builder /app/node_modules ./node_modules
28
+ COPY --from=builder /app/.next ./.next
29
+ COPY --from=builder /app/public ./public
30
+ COPY --from=builder /app/next.config.js ./next.config.js
31
+
32
+ # Expose the port the app will run on (Spaces expects the app to listen on this port)
33
+ EXPOSE 3000
34
+
35
+ # If you use private/gated HF models, set HF_TOKEN in the Space secrets and expose here
36
+ # e.g. in Space settings: add secret HF_TOKEN with your token
37
+
38
+ CMD ["npm", "run", "start"]
README.md CHANGED
@@ -1,10 +1,95 @@
1
  ---
2
- title: General Eval Card
3
- emoji: 📚
4
- colorFrom: gray
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: AI Evaluation Dashboard
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ app_port: 3000
9
  ---
10
 
11
+ # AI Evaluation Dashboard
12
+
13
+ This repository is a Next.js application for viewing and authoring AI evaluations. It includes demo evaluation fixtures under `public/evaluations/` and a dynamic details page that performs server-side rendering and route-handler based inference.
14
+
15
+ ## Run locally
16
+
17
+ Install dependencies and run the dev server:
18
+
19
+ ```bash
20
+ npm ci
21
+ npm run dev
22
+ ```
23
+
24
+ Build for production and run:
25
+
26
+ ```bash
27
+ npm ci
28
+ npm run build
29
+ NODE_ENV=production PORT=3000 npm run start
30
+ ```
31
+
32
+ ## Docker (recommended for Hugging Face Spaces)
33
+
34
+ A `Dockerfile` is included for deploying this app as a dynamic service on Hugging Face Spaces (Docker runtime).
35
+
36
+ Build the image locally:
37
+
38
+ ```bash
39
+ docker build -t ai-eval-dashboard .
40
+ ```
41
+
42
+ Run the container (expose port 3000):
43
+
44
+ ```bash
45
+ docker run -p 3000:3000 -e HF_TOKEN="$HF_TOKEN" ai-eval-dashboard
46
+ ```
47
+
48
+ Visit `http://localhost:3000` to verify.
49
+
50
+ ### Deploy to Hugging Face Spaces
51
+
52
+ 1. Create a new Space at https://huggingface.co/new-space and choose **Docker** as the runtime.
53
+ 2. Add a secret named `HF_TOKEN` (if you plan to access private or gated models or the Inference API) in the Space settings.
54
+ 3. Push this repository to the Space Git (or upload files through the UI). The Space will build the Docker image using the included `Dockerfile` and serve your app on port 3000.
55
+
56
+ Notes:
57
+ - The app's server may attempt to construct ML pipelines server-side if you use Transformers.js and large models; prefer small/quantized models or use the Hugging Face Inference API instead (see below).
58
+ - If your build needs native dependencies (e.g. `sharp`), the Docker image may require extra apt packages; update the Dockerfile accordingly.
59
+
60
+ ## Alternative: Use Hugging Face Inference API (avoid hosting model weights)
61
+
62
+ If downloading and running model weights inside the Space is impractical (memory/disk limits), modify the server route to proxy requests to the Hugging Face Inference API.
63
+
64
+ Example server-side call (Route Handler):
65
+
66
+ ```js
67
+ const resp = await fetch('https://api-inference.huggingface.co/models/<model-id>', {
68
+ method: 'POST',
69
+ headers: { Authorization: `Bearer ${process.env.HF_TOKEN}`, 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({ inputs: text })
71
+ })
72
+ const json = await resp.json()
73
+ ```
74
+
75
+ Store `HF_TOKEN` in the Space secrets and your route will be able to call the API.
76
+
77
+ ## Troubleshooting
78
+
79
+ - Build fails in Spaces: check the build logs; you may need extra apt packages or to pin Node version.
80
+ - Runtime OOM / killed: model is too large for Spaces; use Inference API or smaller models.
81
+
82
+ ## What I added
83
+
84
+ - `Dockerfile` — multi-stage build for production
85
+ - `.dockerignore` — to reduce image size
86
+ - Updated `README.md` with Spaces frontmatter and deployment instructions
87
+
88
+ If you want, I can:
89
+ - Modify the Dockerfile to use Next.js standalone mode for a smaller runtime image.
90
+ - Add a small health-check route and a simple `docker-compose.yml` for local testing.
91
+
92
+ Which of those would you like next?
93
+ npm run build
94
+
95
+ Send the contents of the "out" folder to https://huggingface.co/spaces/evaleval/general-eval-card
app/evaluation/[id]/generateStaticParams.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export async function generateStaticParams() {
5
+ const evaluationsDir = path.join(process.cwd(), "public/evaluations");
6
+ const files = fs.readdirSync(evaluationsDir);
7
+
8
+ const params = files.map((file) => {
9
+ const filePath = path.join(evaluationsDir, file);
10
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
11
+ return { id: data.id };
12
+ });
13
+
14
+ return params;
15
+ }
app/evaluation/[id]/page.client.tsx ADDED
@@ -0,0 +1,711 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useParams, useRouter } from "next/navigation"
4
+ import { useState, useEffect } from "react"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
7
+ import { Badge } from "@/components/ui/badge"
8
+ import { Checkbox } from "@/components/ui/checkbox"
9
+ import { ArrowLeft, Download } from "lucide-react"
10
+ import { CATEGORIES } from "@/lib/category-data"
11
+ import { BENCHMARK_QUESTIONS, PROCESS_QUESTIONS } from "@/lib/category-data"
12
+
13
+ const loadEvaluationDetails = async (id: string) => {
14
+ const evaluationFiles = [
15
+ "/evaluations/gpt-4-turbo.json",
16
+ "/evaluations/claude-3-sonnet.json",
17
+ "/evaluations/gemini-pro.json",
18
+ "/evaluations/fraud-detector.json",
19
+ ]
20
+
21
+ for (const file of evaluationFiles) {
22
+ try {
23
+ const response = await fetch(file)
24
+ const data = await response.json()
25
+
26
+ if (data.id === id) {
27
+ return data
28
+ }
29
+ } catch (error) {
30
+ console.error(`Failed to load evaluation data from ${file}:`, error)
31
+ }
32
+ }
33
+
34
+ return null
35
+ }
36
+
37
+ export default function EvaluationDetailsPage() {
38
+ const params = useParams()
39
+ const router = useRouter()
40
+ const evaluationId = params.id as string
41
+
42
+ const [evaluation, setEvaluation] = useState<any>(null)
43
+ const [loading, setLoading] = useState(true)
44
+ const [expandedAreas, setExpandedAreas] = useState<Record<string, boolean>>({})
45
+ const toggleArea = (area: string) => setExpandedAreas((p) => ({ ...p, [area]: !p[area] }))
46
+ const [expandedNegatives, setExpandedNegatives] = useState<Record<string, boolean>>({})
47
+ const toggleNegatives = (key: string) => setExpandedNegatives((p) => ({ ...p, [key]: !p[key] }))
48
+ const [visibleCategories, setVisibleCategories] = useState<Record<string, boolean>>({})
49
+ const toggleCategoryVisibility = (id: string) => setVisibleCategories((p) => ({ ...p, [id]: !p[id] }))
50
+ const selectAll = () => {
51
+ const map: Record<string, boolean> = {}
52
+ ;(evaluation.selectedCategories || []).forEach((id: string) => (map[id] = true))
53
+ setVisibleCategories(map)
54
+ }
55
+ const deselectAll = () => {
56
+ const map: Record<string, boolean> = {}
57
+ ;(evaluation.selectedCategories || []).forEach((id: string) => (map[id] = false))
58
+ setVisibleCategories(map)
59
+ }
60
+
61
+ // Persist visibility in localStorage per evaluation
62
+ const STORAGE_KEY = `eval:${evaluationId}:visibleCategories`
63
+ useEffect(() => {
64
+ try {
65
+ const raw = localStorage.getItem(STORAGE_KEY)
66
+ if (raw) {
67
+ const parsed = JSON.parse(raw)
68
+ setVisibleCategories(parsed)
69
+ return
70
+ }
71
+ } catch (e) {
72
+ // ignore
73
+ }
74
+
75
+ // if nothing saved, initialize defaults (visible)
76
+ if (evaluation?.selectedCategories) {
77
+ const init: Record<string, boolean> = {}
78
+ evaluation.selectedCategories.forEach((id: string) => {
79
+ init[id] = true
80
+ })
81
+ setVisibleCategories((p) => ({ ...init, ...p }))
82
+ }
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ }, [evaluationId, evaluation?.selectedCategories])
85
+
86
+ useEffect(() => {
87
+ try {
88
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(visibleCategories))
89
+ } catch (e) {
90
+ // ignore
91
+ }
92
+ }, [visibleCategories, evaluationId])
93
+
94
+ useEffect(() => {
95
+ const loadData = async () => {
96
+ const data = await loadEvaluationDetails(evaluationId)
97
+ setEvaluation(data)
98
+ setLoading(false)
99
+ }
100
+ loadData()
101
+ }, [evaluationId])
102
+
103
+ if (loading) {
104
+ return (
105
+ <div className="container mx-auto px-4 py-8">
106
+ <div className="text-center">
107
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
108
+ <p className="text-muted-foreground">Loading evaluation details...</p>
109
+ </div>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ if (!evaluation) {
115
+ return (
116
+ <div className="container mx-auto px-4 py-8">
117
+ <div className="text-center">
118
+ <h1 className="text-2xl font-heading mb-4">Evaluation Not Found</h1>
119
+ <Button onClick={() => router.push("/")} variant="outline">
120
+ <ArrowLeft className="h-4 w-4 mr-2" />
121
+ Back to Dashboard
122
+ </Button>
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ return (
129
+ <div className="container mx-auto px-4 py-8 max-w-4xl">
130
+ <div className="mb-6">
131
+ <div className="flex items-center justify-between">
132
+ <Button onClick={() => router.push("/")} variant="outline" size="sm">
133
+ <ArrowLeft className="h-4 w-4 mr-2" />
134
+ Back to Dashboard
135
+ </Button>
136
+ <Button variant="outline" size="sm">
137
+ <Download className="h-4 w-4 mr-2" />
138
+ Export Report
139
+ </Button>
140
+ </div>
141
+
142
+ <div className="mt-3 text-center">
143
+ <h1 className="text-3xl font-heading">{evaluation.systemName}</h1>
144
+ <p className="text-muted-foreground">{evaluation.provider}</p>
145
+ </div>
146
+ </div>
147
+
148
+ {/* System Information */}
149
+ <Card className="mb-6">
150
+ <CardHeader>
151
+ <CardTitle>System Information</CardTitle>
152
+ </CardHeader>
153
+ <CardContent className="grid grid-cols-2 gap-4">
154
+ <div>
155
+ <p className="text-sm text-muted-foreground">System Version</p>
156
+ <p className="font-medium">{evaluation.version}</p>
157
+ </div>
158
+ <div>
159
+ <p className="text-sm text-muted-foreground">Deployment Context</p>
160
+ <p className="font-medium">{evaluation.deploymentContext}</p>
161
+ </div>
162
+ <div>
163
+ <p className="text-sm text-muted-foreground">Evaluation Date</p>
164
+ <p className="font-medium">{evaluation.evaluationDate}</p>
165
+ </div>
166
+ <div>
167
+ <p className="text-sm text-muted-foreground">Evaluator</p>
168
+ <p className="font-medium">{evaluation.evaluator}</p>
169
+ </div>
170
+ <div>
171
+ <p className="text-sm text-muted-foreground">Modality</p>
172
+ <p className="font-medium">{evaluation.modality}</p>
173
+ </div>
174
+ <div>
175
+ <p className="text-sm text-muted-foreground">Completeness Score</p>
176
+ <p className="font-medium">{evaluation.overallStats?.completenessScore || "N/A"}%</p>
177
+ </div>
178
+ </CardContent>
179
+ </Card>
180
+
181
+ {/* Applicable Categories - split into Capabilities & Risks with visibility toggles */}
182
+ <Card className="mb-6">
183
+ <CardHeader>
184
+ <div className="flex items-center justify-between w-full">
185
+ <CardTitle>Applicable Categories ({evaluation.selectedCategories?.length || 0})</CardTitle>
186
+ <div className="flex items-center gap-2">
187
+ <Button size="sm" variant="outline" onClick={selectAll}>
188
+ Select all
189
+ </Button>
190
+ <Button size="sm" variant="outline" onClick={deselectAll}>
191
+ Deselect all
192
+ </Button>
193
+ </div>
194
+ </div>
195
+ </CardHeader>
196
+ <CardContent>
197
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
198
+ <div>
199
+ <div className="text-sm font-medium mb-2">Capabilities</div>
200
+ <div className="flex flex-col gap-2">
201
+ {evaluation.selectedCategories
202
+ ?.map((id: string) => CATEGORIES.find((c) => c.id === id))
203
+ .filter(Boolean)
204
+ .filter((c: any) => c.type === "capability")
205
+ .map((category: any) => (
206
+ <label key={category.id} className="flex items-center gap-2">
207
+ <Checkbox
208
+ checked={visibleCategories[category.id] ?? true}
209
+ onCheckedChange={() => toggleCategoryVisibility(category.id)}
210
+ />
211
+ <Badge variant="secondary" className="cursor-pointer">
212
+ {category.name}
213
+ </Badge>
214
+ </label>
215
+ ))}
216
+ </div>
217
+ </div>
218
+
219
+ <div>
220
+ <div className="text-sm font-medium mb-2">Risks</div>
221
+ <div className="flex flex-col gap-2">
222
+ {evaluation.selectedCategories
223
+ ?.map((id: string) => CATEGORIES.find((c) => c.id === id))
224
+ .filter(Boolean)
225
+ .filter((c: any) => c.type === "risk")
226
+ .map((category: any) => (
227
+ <label key={category.id} className="flex items-center gap-2">
228
+ <Checkbox
229
+ checked={visibleCategories[category.id] ?? true}
230
+ onCheckedChange={() => toggleCategoryVisibility(category.id)}
231
+ />
232
+ <Badge variant="destructive" className="cursor-pointer">
233
+ {category.name}
234
+ </Badge>
235
+ </label>
236
+ ))}
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </CardContent>
241
+ </Card>
242
+
243
+ {/* Overall Statistics */}
244
+ <Card className="mb-6">
245
+ <CardHeader>
246
+ <CardTitle>Overall Statistics</CardTitle>
247
+ </CardHeader>
248
+ <CardContent>
249
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
250
+ <div className="text-center p-4 bg-green-50 dark:bg-green-950 rounded-lg">
251
+ <div className="text-2xl font-bold text-green-700 dark:text-green-300">
252
+ {evaluation.overallStats?.strongCategories?.length || 0}
253
+ </div>
254
+ <div className="text-sm text-green-600 dark:text-green-400">Strong</div>
255
+ </div>
256
+ <div className="text-center p-4 bg-blue-50 dark:bg-blue-950 rounded-lg">
257
+ <div className="text-2xl font-bold text-blue-700 dark:text-blue-300">
258
+ {evaluation.overallStats?.adequateCategories?.length || 0}
259
+ </div>
260
+ <div className="text-sm text-blue-600 dark:text-blue-400">Adequate</div>
261
+ </div>
262
+ <div className="text-center p-4 bg-yellow-50 dark:bg-yellow-950 rounded-lg">
263
+ <div className="text-2xl font-bold text-yellow-700 dark:text-yellow-300">
264
+ {evaluation.overallStats?.weakCategories?.length || 0}
265
+ </div>
266
+ <div className="text-sm text-yellow-600 dark:text-yellow-400">Weak</div>
267
+ </div>
268
+ <div className="text-center p-4 bg-red-50 dark:bg-red-950 rounded-lg">
269
+ <div className="text-2xl font-bold text-red-700 dark:text-red-300">
270
+ {evaluation.overallStats?.insufficientCategories?.length || 0}
271
+ </div>
272
+ <div className="text-sm text-red-600 dark:text-red-400">Insufficient</div>
273
+ </div>
274
+ </div>
275
+ </CardContent>
276
+ </Card>
277
+
278
+ {/* Priority Areas (show only weak/insufficient like results) */}
279
+ {((evaluation.overallStats?.weakCategories || []).length > 0 || (evaluation.overallStats?.insufficientCategories || []).length > 0) && (
280
+ <Card className="mb-6">
281
+ <CardHeader>
282
+ <CardTitle>Priority Areas</CardTitle>
283
+ </CardHeader>
284
+ <CardContent>
285
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
286
+ {[...(evaluation.overallStats?.insufficientCategories || []), ...(evaluation.overallStats?.weakCategories || [])]
287
+ .filter(Boolean)
288
+ .map((catId: string) => {
289
+ const category = CATEGORIES.find((c) => c.id === catId)
290
+ return (
291
+ <div key={catId} className="p-3 border rounded-md flex items-center justify-between">
292
+ <div>
293
+ <div className="font-medium">{category?.name || catId}</div>
294
+ <div className="text-xs text-muted-foreground">{category?.description}</div>
295
+ </div>
296
+ <Badge variant={evaluation.overallStats?.insufficientCategories?.includes(catId) ? "destructive" : "outline"}>
297
+ {evaluation.overallStats?.insufficientCategories?.includes(catId) ? "insufficient" : "weak"}
298
+ </Badge>
299
+ </div>
300
+ )
301
+ })}
302
+ </div>
303
+ </CardContent>
304
+ </Card>
305
+ )}
306
+
307
+ {/* Evaluation Details */}
308
+ {evaluation.categoryEvaluations &&
309
+ Object.entries(evaluation.categoryEvaluations)
310
+ .filter(([categoryId]) => visibleCategories[categoryId] ?? true)
311
+ .map(([categoryId, data]: [string, any]) => {
312
+ const category = CATEGORIES.find((c) => c.id === categoryId)
313
+
314
+ // compute per-category score (yes out of applicable (yes+no)) across A & B
315
+ const benchmarkQs = BENCHMARK_QUESTIONS.map((q) => q.id)
316
+ const processQs = PROCESS_QUESTIONS.map((q) => q.id)
317
+ let yesCount = 0
318
+ let noCount = 0
319
+ let naCount = 0
320
+
321
+ for (const qid of benchmarkQs) {
322
+ const raw = data.benchmarkAnswers?.[qid]
323
+ const answers = Array.isArray(raw) ? raw : raw ? [raw] : []
324
+ const hasYes = answers.some((a: string) => String(a).toLowerCase() === "yes")
325
+ const hasNo = answers.some((a: string) => String(a).toLowerCase() === "no")
326
+ const hasNA = answers.length === 0 || answers.some((a: string) => String(a).toLowerCase().includes("not applicable") || String(a).toLowerCase() === "n/a")
327
+ if (hasYes) yesCount++
328
+ else if (hasNo) noCount++
329
+ else if (hasNA) naCount++
330
+ else naCount++
331
+ }
332
+
333
+ for (const qid of processQs) {
334
+ const raw = data.processAnswers?.[qid]
335
+ const answers = Array.isArray(raw) ? raw : raw ? [raw] : []
336
+ const hasYes = answers.some((a: string) => String(a).toLowerCase() === "yes")
337
+ const hasNo = answers.some((a: string) => String(a).toLowerCase() === "no")
338
+ const hasNA = answers.length === 0 || answers.some((a: string) => String(a).toLowerCase().includes("not applicable") || String(a).toLowerCase() === "n/a")
339
+ if (hasYes) yesCount++
340
+ else if (hasNo) noCount++
341
+ else if (hasNA) naCount++
342
+ else naCount++
343
+ }
344
+
345
+ const totalApplicable = yesCount + noCount
346
+ const scoreText = totalApplicable > 0 ? `${yesCount}/${totalApplicable}` : "N/A"
347
+ let rating = "Unknown"
348
+ if (evaluation.overallStats?.strongCategories?.includes(categoryId)) rating = "Strong"
349
+ else if (evaluation.overallStats?.adequateCategories?.includes(categoryId)) rating = "Adequate"
350
+ else if (evaluation.overallStats?.weakCategories?.includes(categoryId)) rating = "Weak"
351
+ else if (evaluation.overallStats?.insufficientCategories?.includes(categoryId)) rating = "Insufficient"
352
+
353
+ const ratingClass =
354
+ rating === "Strong"
355
+ ? "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700"
356
+ : rating === "Adequate"
357
+ ? "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-700"
358
+ : rating === "Weak"
359
+ ? "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-800"
360
+ : "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700"
361
+
362
+ return (
363
+ <Card key={categoryId} className="mb-6">
364
+ <CardHeader>
365
+ <div className="flex items-start justify-between w-full gap-4">
366
+ <div>
367
+ <CardTitle className="flex items-center gap-2">
368
+ {category?.name || categoryId}
369
+ <Badge variant={category?.type === "capability" ? "secondary" : "destructive"}>
370
+ {category?.type || "unknown"}
371
+ </Badge>
372
+ </CardTitle>
373
+ <p className="text-sm text-muted-foreground">{category?.description}</p>
374
+ </div>
375
+
376
+ <div className="text-right">
377
+ <div className="text-sm text-muted-foreground">Score</div>
378
+ <div className="font-semibold">{scoreText}</div>
379
+ <div className="mt-2">
380
+ <span className={ratingClass}>{rating}</span>
381
+ </div>
382
+ </div>
383
+ </div>
384
+ </CardHeader>
385
+ <CardContent className="space-y-6">
386
+ {/* Benchmark Questions */}
387
+ {data.benchmarkSources && (
388
+ <div>
389
+ <h4 className="font-semibold mb-3">Part A: Benchmark & Testing</h4>
390
+ <div className="space-y-4">
391
+ {(() => {
392
+ const entries = Object.entries(data.benchmarkSources || {}) as [string, any][]
393
+ const yesItems: any[] = []
394
+ const noItems: any[] = []
395
+ const naItems: any[] = []
396
+
397
+ // iterate the union of known source keys and answer keys so we show questions
398
+ const canonicalKeys = BENCHMARK_QUESTIONS.map((q) => q.id)
399
+ const answerKeys = Object.keys(data.benchmarkAnswers || {})
400
+ const sourceKeys = Object.keys(data.benchmarkSources || {})
401
+ const keySet = new Set<string>([...canonicalKeys, ...answerKeys, ...sourceKeys])
402
+ for (const questionId of Array.from(keySet)) {
403
+ const sources = data.benchmarkSources?.[questionId] || []
404
+ const qText = BENCHMARK_QUESTIONS.find((x) => x.id === questionId)?.text || questionId
405
+ const rawAnswer = data.benchmarkAnswers?.[questionId]
406
+ const answers = Array.isArray(rawAnswer) ? rawAnswer : rawAnswer ? [rawAnswer] : []
407
+ const hasYes = answers.some((a: string) => String(a).toLowerCase() === "yes")
408
+ const hasNo = answers.some((a: string) => String(a).toLowerCase() === "no")
409
+ const hasNA = answers.length === 0 || answers.some((a: string) => String(a).toLowerCase().includes("not applicable") || String(a).toLowerCase() === "n/a")
410
+
411
+ const reason =
412
+ sources?.[0]?.scope || sources?.[0]?.description || data.additionalAspects || (hasNA ? "Not applicable" : undefined)
413
+
414
+ if (hasYes) yesItems.push({ questionId, qText, sources })
415
+ else if (hasNo) noItems.push({ questionId, qText })
416
+ else if (hasNA) naItems.push({ questionId, qText, reason })
417
+ else naItems.push({ questionId, qText, reason: reason || "Not applicable" })
418
+ }
419
+
420
+ return (
421
+ <>
422
+ {yesItems.map((it) => {
423
+ const key = `bench-${categoryId}-${it.questionId}`
424
+ return (
425
+ <div key={it.questionId} className="border rounded-lg p-4">
426
+ <div
427
+ role="button"
428
+ tabIndex={0}
429
+ onClick={() => toggleNegatives(key)}
430
+ className="flex items-center gap-2 mb-2 justify-between cursor-pointer"
431
+ >
432
+ <div className="flex items-center gap-3">
433
+ <span className="font-medium">{it.questionId}:</span>
434
+ <div className="text-sm">{it.qText}</div>
435
+ </div>
436
+ <div className="flex items-center gap-2">
437
+ <span className="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700">yes</span>
438
+ </div>
439
+ </div>
440
+
441
+ {expandedNegatives[key] && (() => {
442
+ const cards = (it.sources || []).flatMap((src: any) => {
443
+ const names = String(src.benchmarkName || '')
444
+ .split(',')
445
+ .map((s: string) => s.trim())
446
+ .filter(Boolean)
447
+
448
+ const scoreParts = String(src.score || '')
449
+ .split(',')
450
+ .map((s: string) => s.trim())
451
+ .filter(Boolean)
452
+
453
+ return (names.length > 0 ? names : ['Benchmark']).map((name: string, idx: number) => {
454
+ // determine score for this benchmark (positional or by name) or fallback to any numeric
455
+ let scoreNum: number | undefined
456
+ if (scoreParts.length === names.length && scoreParts[idx]) {
457
+ const m = scoreParts[idx].match(/(\d+(?:\.\d+)?)/)
458
+ if (m) scoreNum = parseFloat(m[1])
459
+ } else if (scoreParts.length > 0) {
460
+ const byName = scoreParts.find((p: string) => p.toLowerCase().includes(name.toLowerCase()))
461
+ const m = (byName || scoreParts[0]).match(/(\d+(?:\.\d+)?)/)
462
+ if (m) scoreNum = parseFloat(m[1])
463
+ } else if (src?.score) {
464
+ const m = String(src.score).match(/(\d+(?:\.\d+)?)/)
465
+ if (m) scoreNum = parseFloat(m[1])
466
+ }
467
+
468
+ return (
469
+ <div key={`${it.questionId}-${name}-${idx}`} className="p-4 border rounded-lg bg-background">
470
+ <div className="flex items-start justify-between">
471
+ <div className="text-xs inline-flex items-center rounded-full px-2 py-1 bg-indigo-50 text-indigo-700">Percentage</div>
472
+ <div className="text-2xl font-bold text-indigo-600">{scoreNum != null ? `${scoreNum}%` : '—'}</div>
473
+ </div>
474
+
475
+ <div className="mt-3 text-lg font-semibold">{name}</div>
476
+
477
+ {scoreNum != null && (
478
+ <div className="mt-3">
479
+ <div className="h-2 bg-gray-200 rounded-full overflow-hidden">
480
+ <div className="h-2 bg-indigo-500" style={{ width: `${Math.max(0, Math.min(100, scoreNum))}%` }} />
481
+ </div>
482
+ </div>
483
+ )}
484
+
485
+ <div className="mt-3 space-y-2 text-sm">
486
+ <div>
487
+ <span className="text-muted-foreground">Source:</span>{' '}
488
+ {src.url ? (
489
+ <a className="text-primary underline" href={src.url} target="_blank" rel="noreferrer">
490
+ {src.url}
491
+ </a>
492
+ ) : (
493
+ '—'
494
+ )}
495
+ </div>
496
+ <div>
497
+ <span className="text-muted-foreground">Type:</span> {src.sourceType || src.documentType || '—'}
498
+ </div>
499
+ {src.metrics && (
500
+ <div>
501
+ <span className="text-muted-foreground">Metric:</span> {src.metrics}
502
+ </div>
503
+ )}
504
+ {src.confidenceInterval && (
505
+ <div>
506
+ <span className="text-muted-foreground">Confidence Interval:</span> {src.confidenceInterval}
507
+ </div>
508
+ )}
509
+ {src.description && (
510
+ <div className="mt-2 p-2 bg-muted/40 rounded text-sm">{src.description}</div>
511
+ )}
512
+ </div>
513
+ </div>
514
+ )
515
+ })
516
+ })
517
+
518
+ if (cards.length === 0) return <div className="text-sm text-muted-foreground">No benchmark details available.</div>
519
+
520
+ return <div className="mt-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">{cards}</div>
521
+ })()}
522
+ </div>
523
+ )
524
+ })}
525
+
526
+ {noItems.map((it) => (
527
+ <div key={it.questionId} className="border rounded-lg p-4">
528
+ <div className="flex items-center gap-2 mb-2 justify-between">
529
+ <div className="flex items-center gap-3">
530
+ <span className="font-medium">{it.questionId}:</span>
531
+ <div className="text-sm">{it.qText}</div>
532
+ </div>
533
+ <div>
534
+ <span className="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700">no</span>
535
+ </div>
536
+ </div>
537
+ </div>
538
+ ))}
539
+
540
+ {naItems.length > 0 && (
541
+ <div className="border rounded-lg p-3">
542
+ <div className="flex items-center justify-between">
543
+ <div className="font-medium">Not applicable ({naItems.length})</div>
544
+ <button onClick={() => toggleNegatives(`bench-na-${categoryId}`)} className="text-sm text-primary underline">
545
+ {expandedNegatives[`bench-na-${categoryId}`] ? "Hide" : "Show"}
546
+ </button>
547
+ </div>
548
+
549
+ {expandedNegatives[`bench-na-${categoryId}`] && (
550
+ <div className="mt-3 space-y-2">
551
+ {naItems.map((it) => (
552
+ <div key={it.questionId} className="p-2 bg-muted rounded">
553
+ <div className="flex items-center justify-between">
554
+ <div className="text-sm">
555
+ <span className="font-medium">{it.questionId}:</span> {it.qText}
556
+ </div>
557
+ <div className="text-xs text-muted-foreground">Reason: {it.reason}</div>
558
+ </div>
559
+ </div>
560
+ ))}
561
+ </div>
562
+ )}
563
+ </div>
564
+ )}
565
+ </>
566
+ )
567
+ })()}
568
+ </div>
569
+ </div>
570
+ )}
571
+
572
+ {/* Process Questions */}
573
+ {data.processSources && (
574
+ <div>
575
+ <h4 className="font-semibold mb-3">Part B: Documentation & Process</h4>
576
+ <div className="space-y-4">
577
+ {(() => {
578
+ const entries = Object.entries(data.processSources || {}) as [string, any][]
579
+ const yesItems: any[] = []
580
+ const noItems: any[] = []
581
+ const naItems: any[] = []
582
+
583
+ const canonicalKeys = PROCESS_QUESTIONS.map((q) => q.id)
584
+ const answerKeys = Object.keys(data.processAnswers || {})
585
+ const sourceKeys = Object.keys(data.processSources || {})
586
+ const keySet = new Set<string>([...canonicalKeys, ...answerKeys, ...sourceKeys])
587
+ for (const questionId of Array.from(keySet)) {
588
+ const sources = data.processSources?.[questionId] || []
589
+ const qText = PROCESS_QUESTIONS.find((x) => x.id === questionId)?.text || questionId
590
+ const rawAnswer = data.processAnswers?.[questionId]
591
+ const answers = Array.isArray(rawAnswer) ? rawAnswer : rawAnswer ? [rawAnswer] : []
592
+ const hasYes = answers.some((a: string) => String(a).toLowerCase() === "yes")
593
+ const hasNo = answers.some((a: string) => String(a).toLowerCase() === "no")
594
+ const hasNA = answers.length === 0 || answers.some((a: string) => String(a).toLowerCase().includes("not applicable") || String(a).toLowerCase() === "n/a")
595
+
596
+ const reason = sources?.[0]?.scope || sources?.[0]?.description || data.additionalAspects || (hasNA ? "Not applicable" : undefined)
597
+
598
+ if (hasYes) yesItems.push({ questionId, qText, sources })
599
+ else if (hasNo) noItems.push({ questionId, qText })
600
+ else if (hasNA) naItems.push({ questionId, qText, reason })
601
+ else naItems.push({ questionId, qText, reason: reason || "Not applicable" })
602
+ }
603
+
604
+ return (
605
+ <>
606
+ {yesItems.map((it) => {
607
+ const key = `proc-${categoryId}-${it.questionId}`
608
+ return (
609
+ <div key={it.questionId} className="border rounded-lg p-4">
610
+ <div
611
+ role="button"
612
+ tabIndex={0}
613
+ onClick={() => toggleNegatives(key)}
614
+ className="flex items-center gap-2 mb-2 justify-between cursor-pointer"
615
+ >
616
+ <div className="flex items-center gap-3">
617
+ <span className="font-medium">{it.questionId}:</span>
618
+ <div className="text-sm">{it.qText}</div>
619
+ </div>
620
+ <div className="flex items-center gap-2">
621
+ <span className="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700">yes</span>
622
+ </div>
623
+ </div>
624
+
625
+ {expandedNegatives[key] && (
626
+ <div className="mt-3 space-y-3">
627
+ {(it.sources || []).map((src: any, i: number) => (
628
+ <div key={i} className="p-3 bg-muted rounded">
629
+ <div className="grid grid-cols-1 gap-2 text-sm">
630
+ <div>
631
+ <span className="text-muted-foreground">URL:</span> {src?.url || '—'}
632
+ </div>
633
+ <div>
634
+ <span className="text-muted-foreground">Document Type:</span> {src?.documentType || src?.sourceType || '—'}
635
+ </div>
636
+ </div>
637
+ {src?.description && (
638
+ <div className="mt-2 text-sm">
639
+ <span className="text-muted-foreground">Description:</span> {src.description}
640
+ </div>
641
+ )}
642
+ </div>
643
+ ))}
644
+ </div>
645
+ )}
646
+ </div>
647
+ )
648
+ })}
649
+
650
+ {noItems.map((it) => (
651
+ <div key={it.questionId} className="border rounded-lg p-4">
652
+ <div className="flex items-center gap-2 mb-2 justify-between">
653
+ <div className="flex items-center gap-3">
654
+ <span className="font-medium">{it.questionId}:</span>
655
+ <div className="text-sm">{it.qText}</div>
656
+ </div>
657
+ <div>
658
+ <span className="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700">no</span>
659
+ </div>
660
+ </div>
661
+ </div>
662
+ ))}
663
+
664
+ {naItems.length > 0 && (
665
+ <div className="border rounded-lg p-3">
666
+ <div className="flex items-center justify-between">
667
+ <div className="font-medium">Not applicable ({naItems.length})</div>
668
+ <button onClick={() => toggleNegatives(`proc-na-${categoryId}`)} className="text-sm text-primary underline">
669
+ {expandedNegatives[`proc-na-${categoryId}`] ? "Hide" : "Show"}
670
+ </button>
671
+ </div>
672
+
673
+ {expandedNegatives[`proc-na-${categoryId}`] && (
674
+ <div className="mt-3 space-y-2">
675
+ {naItems.map((it) => (
676
+ <div key={it.questionId} className="p-2 bg-muted rounded">
677
+ <div className="flex items-center justify-between">
678
+ <div className="text-sm">
679
+ <span className="font-medium">{it.questionId}:</span> {it.qText}
680
+ </div>
681
+ <div className="text-xs text-muted-foreground">Reason: {it.reason}</div>
682
+ </div>
683
+ </div>
684
+ ))}
685
+ </div>
686
+ )}
687
+ </div>
688
+ )}
689
+ </>
690
+ )
691
+ })()}
692
+ </div>
693
+ </div>
694
+ )}
695
+
696
+ {/* Additional Aspects */}
697
+ {data.additionalAspects && (
698
+ <div>
699
+ <h4 className="font-semibold mb-3">Part C: Additional Aspects</h4>
700
+ <div className="p-4 bg-muted rounded-lg">
701
+ <p className="text-sm">{data.additionalAspects}</p>
702
+ </div>
703
+ </div>
704
+ )}
705
+ </CardContent>
706
+ </Card>
707
+ )
708
+ })}
709
+ </div>
710
+ )
711
+ }
app/evaluation/[id]/page.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ export { generateStaticParams } from "./generateStaticParams"
2
+
3
+ import ClientPage from "./page.client"
4
+
5
+ export default function PageWrapper() {
6
+ return <ClientPage />
7
+ }
8
+
9
+
app/evaluation/[id]/server.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export { generateStaticParams } from "./generateStaticParams";
app/globals.css ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ :root {
7
+ --background: oklch(1 0 0); /* #ffffff - Clean white background */
8
+ --foreground: oklch(0.205 0 0); /* #1f2937 - Dark gray for main text */
9
+ --card: oklch(0.97 0 0); /* #f1f5f9 - Light gray for cards */
10
+ --card-foreground: oklch(0.439 0 0); /* #6b7280 - Mid-tone gray for card text */
11
+ --popover: oklch(1 0 0); /* #ffffff - White for popovers */
12
+ --popover-foreground: oklch(0.205 0 0); /* #1f2937 - Dark gray for popover text */
13
+ --primary: oklch(0.205 0 0); /* #1f2937 - Primary dark gray */
14
+ --primary-foreground: oklch(1 0 0); /* #ffffff - White text on primary */
15
+ --secondary: oklch(0.646 0.222 280.116); /* #8b5cf6 - Purple accent */
16
+ --secondary-foreground: oklch(1 0 0); /* #ffffff - White text on accent */
17
+ --muted: oklch(0.97 0 0); /* #f1f5f9 - Muted light gray */
18
+ --muted-foreground: oklch(0.439 0 0); /* #6b7280 - Muted text color */
19
+ --accent: oklch(0.646 0.222 280.116); /* #8b5cf6 - Purple for interactive elements */
20
+ --accent-foreground: oklch(1 0 0); /* #ffffff - White text on accent */
21
+ --destructive: oklch(0.577 0.245 27.325); /* #dc2626 - Red for errors */
22
+ --destructive-foreground: oklch(1 0 0); /* #ffffff - White text on destructive */
23
+ --border: oklch(0.922 0 0); /* #e5e7eb - Light gray borders */
24
+ --input: oklch(0.985 0 0); /* #f9fafb - Very light gray for inputs */
25
+ --ring: oklch(0.646 0.222 280.116 / 0.5); /* Purple focus ring with opacity */
26
+ --chart-1: oklch(0.488 0.243 264.376); /* #4f46e5 - Indigo for charts */
27
+ --chart-2: oklch(0.6 0.118 184.704); /* #3b82f6 - Blue for charts */
28
+ --chart-3: oklch(0.696 0.17 162.48); /* #22c55e - Green for charts */
29
+ --chart-4: oklch(0.828 0.189 84.429); /* #fbbf24 - Yellow for charts */
30
+ --chart-5: oklch(0.627 0.265 303.9); /* #ef4444 - Red for charts */
31
+ --radius: 0.5rem; /* Consistent border radius */
32
+ --sidebar: oklch(0.97 0 0); /* #f1f5f9 - Light gray sidebar */
33
+ --sidebar-foreground: oklch(0.205 0 0); /* #1f2937 - Dark gray sidebar text */
34
+ --sidebar-primary: oklch(0.205 0 0); /* #1f2937 - Primary sidebar color */
35
+ --sidebar-primary-foreground: oklch(1 0 0); /* #ffffff - White text on sidebar primary */
36
+ --sidebar-accent: oklch(0.646 0.222 280.116); /* #8b5cf6 - Purple sidebar accent */
37
+ --sidebar-accent-foreground: oklch(1 0 0); /* #ffffff - White text on sidebar accent */
38
+ --sidebar-border: oklch(0.922 0 0); /* #e5e7eb - Light gray sidebar borders */
39
+ --sidebar-ring: oklch(0.646 0.222 280.116 / 0.5); /* Purple sidebar focus ring */
40
+ --font-heading: var(--font-space-grotesk);
41
+ --font-sans: var(--font-dm-sans);
42
+ }
43
+
44
+ .dark {
45
+ --background: oklch(0.145 0 0);
46
+ --foreground: oklch(0.985 0 0);
47
+ --card: oklch(0.145 0 0);
48
+ --card-foreground: oklch(0.985 0 0);
49
+ --popover: oklch(0.145 0 0);
50
+ --popover-foreground: oklch(0.985 0 0);
51
+ --primary: oklch(0.985 0 0);
52
+ --primary-foreground: oklch(0.205 0 0);
53
+ --secondary: oklch(0.269 0 0);
54
+ --secondary-foreground: oklch(0.985 0 0);
55
+ --muted: oklch(0.269 0 0);
56
+ --muted-foreground: oklch(0.708 0 0);
57
+ --accent: oklch(0.269 0 0);
58
+ --accent-foreground: oklch(0.985 0 0);
59
+ --destructive: oklch(0.396 0.141 25.723);
60
+ --destructive-foreground: oklch(0.637 0.237 25.331);
61
+ --border: oklch(0.269 0 0);
62
+ --input: oklch(0.269 0 0);
63
+ --ring: oklch(0.439 0 0);
64
+ --chart-1: oklch(0.488 0.243 264.376);
65
+ --chart-2: oklch(0.696 0.17 162.48);
66
+ --chart-3: oklch(0.769 0.188 70.08);
67
+ --chart-4: oklch(0.627 0.265 303.9);
68
+ --chart-5: oklch(0.645 0.246 16.439);
69
+ --sidebar: oklch(0.205 0 0);
70
+ --sidebar-foreground: oklch(0.985 0 0);
71
+ --sidebar-primary: oklch(0.488 0.243 264.376);
72
+ --sidebar-primary-foreground: oklch(0.985 0 0);
73
+ --sidebar-accent: oklch(0.269 0 0);
74
+ --sidebar-accent-foreground: oklch(0.985 0 0);
75
+ --sidebar-border: oklch(0.269 0 0);
76
+ --sidebar-ring: oklch(0.439 0 0);
77
+ }
78
+
79
+ @theme inline {
80
+ --color-background: var(--background);
81
+ --color-foreground: var(--foreground);
82
+ --color-card: var(--card);
83
+ --color-card-foreground: var(--card-foreground);
84
+ --color-popover: var(--popover);
85
+ --color-popover-foreground: var(--popover-foreground);
86
+ --color-primary: var(--primary);
87
+ --color-primary-foreground: var(--primary-foreground);
88
+ --color-secondary: var(--secondary);
89
+ --color-secondary-foreground: var(--secondary-foreground);
90
+ --color-muted: var(--muted);
91
+ --color-muted-foreground: var(--muted-foreground);
92
+ --color-accent: var(--accent);
93
+ --color-accent-foreground: var(--accent-foreground);
94
+ --color-destructive: var(--destructive);
95
+ --color-destructive-foreground: var(--destructive-foreground);
96
+ --color-border: var(--border);
97
+ --color-input: var(--input);
98
+ --color-ring: var(--ring);
99
+ --color-chart-1: var(--chart-1);
100
+ --color-chart-2: var(--chart-2);
101
+ --color-chart-3: var(--chart-3);
102
+ --color-chart-4: var(--chart-4);
103
+ --color-chart-5: var(--chart-5);
104
+ --radius-sm: calc(var(--radius) - 4px);
105
+ --radius-md: calc(var(--radius) - 2px);
106
+ --radius-lg: var(--radius);
107
+ --radius-xl: calc(var(--radius) + 4px);
108
+ --color-sidebar: var(--sidebar);
109
+ --color-sidebar-foreground: var(--sidebar-foreground);
110
+ --color-sidebar-primary: var(--sidebar-primary);
111
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
112
+ --color-sidebar-accent: var(--sidebar-accent);
113
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
114
+ --color-sidebar-border: var(--sidebar-border);
115
+ --color-sidebar-ring: var(--sidebar-ring);
116
+ --font-heading: var(--font-space-grotesk);
117
+ --font-sans: var(--font-dm-sans);
118
+ }
119
+
120
+ @layer base {
121
+ * {
122
+ @apply border-border outline-ring/50;
123
+ }
124
+ body {
125
+ @apply bg-background text-foreground;
126
+ }
127
+ }
app/layout.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type React from "react"
2
+ import type { Metadata } from "next"
3
+ import { Space_Grotesk, DM_Sans } from "next/font/google"
4
+ import "./globals.css"
5
+ import { ThemeProvider } from "@/components/theme-provider"
6
+
7
+ const spaceGrotesk = Space_Grotesk({
8
+ subsets: ["latin"],
9
+ display: "swap",
10
+ variable: "--font-space-grotesk",
11
+ })
12
+
13
+ const dmSans = DM_Sans({
14
+ subsets: ["latin"],
15
+ display: "swap",
16
+ variable: "--font-dm-sans",
17
+ })
18
+
19
+ export const metadata: Metadata = {
20
+ title: "AI Evaluation Dashboard",
21
+ description: "Professional AI system evaluation and assessment tool",
22
+ generator: "v0.app",
23
+ }
24
+
25
+ export default function RootLayout({
26
+ children,
27
+ }: Readonly<{
28
+ children: React.ReactNode
29
+ }>) {
30
+ return (
31
+ <html lang="en" className={`${spaceGrotesk.variable} ${dmSans.variable} antialiased`}>
32
+ <body className="font-sans">
33
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
34
+ {children}
35
+ </ThemeProvider>
36
+ </body>
37
+ </html>
38
+ )
39
+ }
app/page.tsx ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useMemo, useEffect } from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
6
+ import { Plus, Moon, Sun, Filter, ArrowUpDown } from "lucide-react"
7
+ import { useTheme } from "next-themes"
8
+ import { EvaluationCard, type EvaluationCardData } from "@/components/evaluation-card"
9
+ import { BENCHMARK_QUESTIONS, PROCESS_QUESTIONS } from "@/lib/category-data"
10
+ import { AIEvaluationDashboard } from "@/components/ai-evaluation-dashboard"
11
+
12
+ const loadEvaluationData = async (): Promise<EvaluationCardData[]> => {
13
+ const evaluationFiles = [
14
+ "/evaluations/gpt-4-turbo.json",
15
+ "/evaluations/claude-3-sonnet.json",
16
+ "/evaluations/gemini-pro.json",
17
+ "/evaluations/fraud-detector.json",
18
+ ]
19
+
20
+ const additionalFiles = []
21
+ for (let i = 1; i <= 10; i++) {
22
+ additionalFiles.push(`/evaluations/eval-${Date.now() - i * 86400000}.json`) // Check for files from last 10 days
23
+ }
24
+
25
+ const allFiles = [...evaluationFiles, ...additionalFiles]
26
+ const evaluations: EvaluationCardData[] = []
27
+
28
+ for (const file of allFiles) {
29
+ try {
30
+ const response = await fetch(file)
31
+ if (!response.ok) continue // Skip files that don't exist
32
+
33
+ const data = await response.json()
34
+
35
+ const cardData: EvaluationCardData = {
36
+ id: data.id || `eval-${Date.now()}`,
37
+ systemName: data.systemName || "Unknown System",
38
+ provider: data.provider || "Unknown Provider",
39
+ modality: data.modality || "Unknown",
40
+ completedDate: data.evaluationDate || new Date().toISOString().split("T")[0],
41
+ applicableCategories: data.overallStats?.totalApplicable || 0,
42
+ completedCategories: data.overallStats?.totalApplicable || 0,
43
+ status:
44
+ data.overallStats?.strongCategories?.length >= (data.overallStats?.adequateCategories?.length || 0)
45
+ ? "strong"
46
+ : data.overallStats?.adequateCategories?.length >= (data.overallStats?.weakCategories?.length || 0)
47
+ ? "adequate"
48
+ : "weak",
49
+ capabilityEval: {
50
+ strong: (data.overallStats?.strongCategories || []).filter((cat: string) =>
51
+ [
52
+ "language-communication",
53
+ "social-intelligence",
54
+ "problem-solving",
55
+ "creativity-innovation",
56
+ "learning-memory",
57
+ "perception-vision",
58
+ "physical-manipulation",
59
+ "metacognition",
60
+ "robotic-intelligence",
61
+ ].includes(cat),
62
+ ).length,
63
+ adequate: (data.overallStats?.adequateCategories || []).filter((cat: string) =>
64
+ [
65
+ "language-communication",
66
+ "social-intelligence",
67
+ "problem-solving",
68
+ "creativity-innovation",
69
+ "learning-memory",
70
+ "perception-vision",
71
+ "physical-manipulation",
72
+ "metacognition",
73
+ "robotic-intelligence",
74
+ ].includes(cat),
75
+ ).length,
76
+ weak: (data.overallStats?.weakCategories || []).filter((cat: string) =>
77
+ [
78
+ "language-communication",
79
+ "social-intelligence",
80
+ "problem-solving",
81
+ "creativity-innovation",
82
+ "learning-memory",
83
+ "perception-vision",
84
+ "physical-manipulation",
85
+ "metacognition",
86
+ "robotic-intelligence",
87
+ ].includes(cat),
88
+ ).length,
89
+ insufficient: (data.overallStats?.insufficientCategories || []).filter((cat: string) =>
90
+ [
91
+ "language-communication",
92
+ "social-intelligence",
93
+ "problem-solving",
94
+ "creativity-innovation",
95
+ "learning-memory",
96
+ "perception-vision",
97
+ "physical-manipulation",
98
+ "metacognition",
99
+ "robotic-intelligence",
100
+ ].includes(cat),
101
+ ).length,
102
+ strongCategories: (data.overallStats?.strongCategories || []).filter((cat: string) =>
103
+ [
104
+ "language-communication",
105
+ "social-intelligence",
106
+ "problem-solving",
107
+ "creativity-innovation",
108
+ "learning-memory",
109
+ "perception-vision",
110
+ "physical-manipulation",
111
+ "metacognition",
112
+ "robotic-intelligence",
113
+ ].includes(cat),
114
+ ),
115
+ adequateCategories: (data.overallStats?.adequateCategories || []).filter((cat: string) =>
116
+ [
117
+ "language-communication",
118
+ "social-intelligence",
119
+ "problem-solving",
120
+ "creativity-innovation",
121
+ "learning-memory",
122
+ "perception-vision",
123
+ "physical-manipulation",
124
+ "metacognition",
125
+ "robotic-intelligence",
126
+ ].includes(cat),
127
+ ),
128
+ weakCategories: (data.overallStats?.weakCategories || []).filter((cat: string) =>
129
+ [
130
+ "language-communication",
131
+ "social-intelligence",
132
+ "problem-solving",
133
+ "creativity-innovation",
134
+ "learning-memory",
135
+ "perception-vision",
136
+ "physical-manipulation",
137
+ "metacognition",
138
+ "robotic-intelligence",
139
+ ].includes(cat),
140
+ ),
141
+ insufficientCategories: (data.overallStats?.insufficientCategories || []).filter((cat: string) =>
142
+ [
143
+ "language-communication",
144
+ "social-intelligence",
145
+ "problem-solving",
146
+ "creativity-innovation",
147
+ "learning-memory",
148
+ "perception-vision",
149
+ "physical-manipulation",
150
+ "metacognition",
151
+ "robotic-intelligence",
152
+ ].includes(cat),
153
+ ),
154
+ totalApplicable: data.overallStats?.capabilityApplicable || 0,
155
+ },
156
+ riskEval: {
157
+ strong: (data.overallStats?.strongCategories || []).filter((cat: string) =>
158
+ [
159
+ "harmful-content",
160
+ "information-integrity",
161
+ "privacy-data",
162
+ "bias-fairness",
163
+ "security-robustness",
164
+ "dangerous-capabilities",
165
+ "human-ai-interaction",
166
+ "environmental-impact",
167
+ "economic-displacement",
168
+ "governance-accountability",
169
+ "value-chain",
170
+ ].includes(cat),
171
+ ).length,
172
+ adequate: (data.overallStats?.adequateCategories || []).filter((cat: string) =>
173
+ [
174
+ "harmful-content",
175
+ "information-integrity",
176
+ "privacy-data",
177
+ "bias-fairness",
178
+ "security-robustness",
179
+ "dangerous-capabilities",
180
+ "human-ai-interaction",
181
+ "environmental-impact",
182
+ "economic-displacement",
183
+ "governance-accountability",
184
+ "value-chain",
185
+ ].includes(cat),
186
+ ).length,
187
+ weak: (data.overallStats?.weakCategories || []).filter((cat: string) =>
188
+ [
189
+ "harmful-content",
190
+ "information-integrity",
191
+ "privacy-data",
192
+ "bias-fairness",
193
+ "security-robustness",
194
+ "dangerous-capabilities",
195
+ "human-ai-interaction",
196
+ "environmental-impact",
197
+ "economic-displacement",
198
+ "governance-accountability",
199
+ "value-chain",
200
+ ].includes(cat),
201
+ ).length,
202
+ insufficient: (data.overallStats?.insufficientCategories || []).filter((cat: string) =>
203
+ [
204
+ "harmful-content",
205
+ "information-integrity",
206
+ "privacy-data",
207
+ "bias-fairness",
208
+ "security-robustness",
209
+ "dangerous-capabilities",
210
+ "human-ai-interaction",
211
+ "environmental-impact",
212
+ "economic-displacement",
213
+ "governance-accountability",
214
+ "value-chain",
215
+ ].includes(cat),
216
+ ).length,
217
+ strongCategories: (data.overallStats?.strongCategories || []).filter((cat: string) =>
218
+ [
219
+ "harmful-content",
220
+ "information-integrity",
221
+ "privacy-data",
222
+ "bias-fairness",
223
+ "security-robustness",
224
+ "dangerous-capabilities",
225
+ "human-ai-interaction",
226
+ "environmental-impact",
227
+ "economic-displacement",
228
+ "governance-accountability",
229
+ "value-chain",
230
+ ].includes(cat),
231
+ ),
232
+ adequateCategories: (data.overallStats?.adequateCategories || []).filter((cat: string) =>
233
+ [
234
+ "harmful-content",
235
+ "information-integrity",
236
+ "privacy-data",
237
+ "bias-fairness",
238
+ "security-robustness",
239
+ "dangerous-capabilities",
240
+ "human-ai-interaction",
241
+ "environmental-impact",
242
+ "economic-displacement",
243
+ "governance-accountability",
244
+ "value-chain",
245
+ ].includes(cat),
246
+ ),
247
+ weakCategories: (data.overallStats?.weakCategories || []).filter((cat: string) =>
248
+ [
249
+ "harmful-content",
250
+ "information-integrity",
251
+ "privacy-data",
252
+ "bias-fairness",
253
+ "security-robustness",
254
+ "dangerous-capabilities",
255
+ "human-ai-interaction",
256
+ "environmental-impact",
257
+ "economic-displacement",
258
+ "governance-accountability",
259
+ "value-chain",
260
+ ].includes(cat),
261
+ ),
262
+ insufficientCategories: (data.overallStats?.insufficientCategories || []).filter((cat: string) =>
263
+ [
264
+ "harmful-content",
265
+ "information-integrity",
266
+ "privacy-data",
267
+ "bias-fairness",
268
+ "security-robustness",
269
+ "dangerous-capabilities",
270
+ "human-ai-interaction",
271
+ "environmental-impact",
272
+ "economic-displacement",
273
+ "governance-accountability",
274
+ "value-chain",
275
+ ].includes(cat),
276
+ ),
277
+ totalApplicable: data.overallStats?.riskApplicable || 0,
278
+ },
279
+ priorityAreas: data.overallStats?.priorityAreas || [],
280
+ priorityDetails: (() => {
281
+ // Build a richer structure: for each area, include yes questions and negative questions (no/na) with optional reason
282
+ const pd: Record<
283
+ string,
284
+ {
285
+ yes: string[]
286
+ negative: { text: string; status: "no" | "na"; reason?: string }[]
287
+ }
288
+ > = {}
289
+ const areas = data.overallStats?.priorityAreas || []
290
+ for (const area of areas) {
291
+ const catEval = data.categoryEvaluations?.[area]
292
+ if (!catEval) continue
293
+
294
+ const yesList: string[] = []
295
+ const negList: { text: string; status: "no" | "na"; reason?: string }[] = []
296
+
297
+ // Helper to detect NA reason from category metadata
298
+ const naReasonFromMeta = (): string | undefined => {
299
+ if (typeof catEval.additionalAspects === "string" && /not applicable/i.test(catEval.additionalAspects)) {
300
+ return catEval.additionalAspects
301
+ }
302
+ // look into processSources scopes for any note
303
+ if (catEval.processSources) {
304
+ for (const entries of Object.values(catEval.processSources)) {
305
+ if (Array.isArray(entries)) {
306
+ for (const ent of entries as any[]) {
307
+ if (ent && typeof ent.scope === "string" && /not applicable/i.test(ent.scope)) {
308
+ return ent.scope
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+ return undefined
315
+ }
316
+
317
+ const naMeta = naReasonFromMeta()
318
+
319
+ // check benchmarkAnswers (A1..A6)
320
+ if (catEval.benchmarkAnswers) {
321
+ for (const [qid, ans] of Object.entries(catEval.benchmarkAnswers)) {
322
+ const answer = ans
323
+ const isArray = Array.isArray(answer)
324
+ const negative = answer === "no" || (isArray && (answer as any[]).includes("no"))
325
+ const positive = answer === "yes" || (isArray && (answer as any[]).includes("yes"))
326
+ const qText = BENCHMARK_QUESTIONS.find((x) => x.id === qid)?.text || qid
327
+ if (positive) yesList.push(qText)
328
+ if (negative) {
329
+ const status = naMeta ? "na" : "no"
330
+ negList.push({ text: qText, status, reason: naMeta })
331
+ }
332
+ }
333
+ }
334
+
335
+ // check processAnswers (B1..B6)
336
+ if (catEval.processAnswers) {
337
+ for (const [qid, ans] of Object.entries(catEval.processAnswers)) {
338
+ const answer = ans
339
+ const isArray = Array.isArray(answer)
340
+ const negative = answer === "no" || (isArray && (answer as any[]).includes("no"))
341
+ const positive = answer === "yes" || (isArray && (answer as any[]).includes("yes"))
342
+ const qText = PROCESS_QUESTIONS.find((x) => x.id === qid)?.text || qid
343
+ if (positive) yesList.push(qText)
344
+ if (negative) {
345
+ const status = naMeta ? "na" : "no"
346
+ negList.push({ text: qText, status, reason: naMeta })
347
+ }
348
+ }
349
+ }
350
+
351
+ if (yesList.length || negList.length) pd[area] = { yes: yesList, negative: negList }
352
+ }
353
+ return pd
354
+ })(),
355
+ }
356
+
357
+ evaluations.push(cardData)
358
+ } catch (error) {
359
+ continue
360
+ }
361
+ }
362
+
363
+ return evaluations
364
+ }
365
+
366
+ export default function HomePage() {
367
+ const { theme, setTheme } = useTheme()
368
+ const [showNewEvaluation, setShowNewEvaluation] = useState(false)
369
+ const [evaluationsData, setEvaluationsData] = useState<EvaluationCardData[]>([])
370
+ const [loading, setLoading] = useState(true)
371
+
372
+ useEffect(() => {
373
+ const loadData = async () => {
374
+ const data = await loadEvaluationData()
375
+ setEvaluationsData(data)
376
+ setLoading(false)
377
+ }
378
+ loadData()
379
+ }, [])
380
+
381
+ const [sortBy, setSortBy] = useState<"date-newest" | "date-oldest">("date-newest")
382
+ const [filterByProvider, setFilterByProvider] = useState<string>("all")
383
+ const [filterByModality, setFilterByModality] = useState<string>("all")
384
+
385
+ const uniqueProviders = useMemo(() => {
386
+ const providers = [...new Set(evaluationsData.map((item) => item.provider))].sort()
387
+ return providers
388
+ }, [evaluationsData])
389
+
390
+ const uniqueModalities = useMemo(() => {
391
+ const modalities = [...new Set(evaluationsData.map((item) => item.modality))].sort()
392
+ return modalities
393
+ }, [evaluationsData])
394
+
395
+ const filteredAndSortedEvaluations = useMemo(() => {
396
+ let filtered = evaluationsData
397
+
398
+ if (filterByProvider !== "all") {
399
+ filtered = filtered.filter((item) => item.provider === filterByProvider)
400
+ }
401
+
402
+ if (filterByModality !== "all") {
403
+ filtered = filtered.filter((item) => item.modality === filterByModality)
404
+ }
405
+
406
+ filtered = [...filtered].sort((a, b) => {
407
+ const dateA = new Date(a.completedDate)
408
+ const dateB = new Date(b.completedDate)
409
+
410
+ if (sortBy === "date-newest") {
411
+ return dateB.getTime() - dateA.getTime()
412
+ } else {
413
+ return dateA.getTime() - dateB.getTime()
414
+ }
415
+ })
416
+
417
+ return filtered
418
+ }, [evaluationsData, sortBy, filterByProvider, filterByModality])
419
+
420
+ const handleViewEvaluation = (id: string) => {}
421
+
422
+ const handleDeleteEvaluation = (id: string) => {
423
+ setEvaluationsData((prev) => prev.filter((evaluation) => evaluation.id !== id))
424
+ }
425
+
426
+ const handleSaveEvaluation = (newEvaluation: EvaluationCardData) => {
427
+ setEvaluationsData((prev) => [newEvaluation, ...prev])
428
+ }
429
+
430
+ if (showNewEvaluation) {
431
+ return <AIEvaluationDashboard onBack={() => setShowNewEvaluation(false)} onSaveEvaluation={handleSaveEvaluation} />
432
+ }
433
+
434
+ if (loading) {
435
+ return (
436
+ <div className="min-h-screen bg-background flex items-center justify-center">
437
+ <div className="text-center">
438
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
439
+ <p className="text-muted-foreground">Loading evaluations...</p>
440
+ </div>
441
+ </div>
442
+ )
443
+ }
444
+
445
+ return (
446
+ <div className="min-h-screen bg-background">
447
+ <header className="border-b bg-card">
448
+ <div className="container mx-auto px-6 py-4">
449
+ <div className="flex items-center justify-between">
450
+ <div>
451
+ <h1 className="text-2xl font-bold font-heading text-foreground">AI Evaluation Dashboard</h1>
452
+ <p className="text-sm text-muted-foreground">Manage and track your AI system evaluations</p>
453
+ </div>
454
+ <div className="flex items-center gap-3">
455
+ <Button
456
+ variant="ghost"
457
+ size="sm"
458
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
459
+ className="h-9 w-9 p-0"
460
+ >
461
+ <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
462
+ <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
463
+ <span className="sr-only">Toggle theme</span>
464
+ </Button>
465
+ <Button onClick={() => setShowNewEvaluation(true)} className="gap-2">
466
+ <Plus className="h-4 w-4" />
467
+ New Eval Card
468
+ </Button>
469
+ </div>
470
+ </div>
471
+ </div>
472
+ </header>
473
+
474
+ <div className="container mx-auto px-6 py-6">
475
+ <div className="space-y-6">
476
+ <div className="flex items-center justify-between">
477
+ <h2 className="text-xl font-semibold font-heading">Evaluation Cards</h2>
478
+ <p className="text-sm text-muted-foreground">{filteredAndSortedEvaluations.length} eval cards</p>
479
+ </div>
480
+
481
+ <div className="flex flex-wrap items-center gap-4 p-4 bg-card rounded-lg border">
482
+ <div className="flex items-center gap-2">
483
+ <ArrowUpDown className="h-4 w-4 text-muted-foreground" />
484
+ <span className="text-sm font-medium">Sort by:</span>
485
+ <Select value={sortBy} onValueChange={(value: "date-newest" | "date-oldest") => setSortBy(value)}>
486
+ <SelectTrigger className="w-40">
487
+ <SelectValue />
488
+ </SelectTrigger>
489
+ <SelectContent>
490
+ <SelectItem value="date-newest">Date (Newest)</SelectItem>
491
+ <SelectItem value="date-oldest">Date (Oldest)</SelectItem>
492
+ </SelectContent>
493
+ </Select>
494
+ </div>
495
+
496
+ <div className="flex items-center gap-2">
497
+ <Filter className="h-4 w-4 text-muted-foreground" />
498
+ <span className="text-sm font-medium">Provider:</span>
499
+ <Select value={filterByProvider} onValueChange={setFilterByProvider}>
500
+ <SelectTrigger className="w-40">
501
+ <SelectValue />
502
+ </SelectTrigger>
503
+ <SelectContent>
504
+ <SelectItem value="all">All Providers</SelectItem>
505
+ {uniqueProviders.map((provider) => (
506
+ <SelectItem key={provider} value={provider}>
507
+ {provider}
508
+ </SelectItem>
509
+ ))}
510
+ </SelectContent>
511
+ </Select>
512
+ </div>
513
+
514
+ <div className="flex items-center gap-2">
515
+ <Filter className="h-4 w-4 text-muted-foreground" />
516
+ <span className="text-sm font-medium">Modality:</span>
517
+ <Select value={filterByModality} onValueChange={setFilterByModality}>
518
+ <SelectTrigger className="w-40">
519
+ <SelectValue />
520
+ </SelectTrigger>
521
+ <SelectContent>
522
+ <SelectItem value="all">All Modalities</SelectItem>
523
+ {uniqueModalities.map((modality) => (
524
+ <SelectItem key={modality} value={modality}>
525
+ {modality}
526
+ </SelectItem>
527
+ ))}
528
+ </SelectContent>
529
+ </Select>
530
+ </div>
531
+ </div>
532
+
533
+ {filteredAndSortedEvaluations.length > 0 ? (
534
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
535
+ {filteredAndSortedEvaluations.map((evaluation) => (
536
+ <EvaluationCard
537
+ key={evaluation.id}
538
+ evaluation={evaluation}
539
+ onView={handleViewEvaluation}
540
+ onDelete={handleDeleteEvaluation}
541
+ />
542
+ ))}
543
+ </div>
544
+ ) : (
545
+ <div className="text-center py-12">
546
+ <div className="mx-auto w-24 h-24 bg-muted rounded-full flex items-center justify-center mb-4">
547
+ <Filter className="h-8 w-8 text-muted-foreground" />
548
+ </div>
549
+ <h3 className="text-lg font-semibold mb-2">No evaluations match your filters</h3>
550
+ <p className="text-muted-foreground mb-4">Try adjusting your filter criteria to see more results</p>
551
+ <Button
552
+ variant="outline"
553
+ onClick={() => {
554
+ setFilterByProvider("all")
555
+ setFilterByModality("all")
556
+ }}
557
+ >
558
+ Clear Filters
559
+ </Button>
560
+ </div>
561
+ )}
562
+ </div>
563
+ </div>
564
+ </div>
565
+ )
566
+ }
components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
components/ai-evaluation-dashboard.tsx ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { Progress } from "@/components/ui/progress"
5
+ import { Badge } from "@/components/ui/badge"
6
+ import { Button } from "@/components/ui/button"
7
+ import { ArrowLeft } from "lucide-react"
8
+ import { SystemInfoForm } from "./system-info-form"
9
+ import { CategorySelection } from "./category-selection"
10
+ import { CategoryEvaluation } from "./category-evaluation"
11
+ import { EvaluationForm } from "./evaluation-form"
12
+ import { ResultsDashboard } from "./results-dashboard"
13
+ import { CATEGORIES } from "@/lib/category-data"
14
+
15
+ export type SystemInfo = {
16
+ name: string
17
+ url: string
18
+ provider: string
19
+ systemTypes: string[]
20
+ deploymentContexts: string[]
21
+ modality: string
22
+ modelTag?: string
23
+ knowledgeCutoff?: string
24
+ modelType?: "foundational" | "fine-tuned" | "na"
25
+ inputModalities?: string[]
26
+ outputModalities?: string[]
27
+ }
28
+
29
+ export type CategoryScore = {
30
+ benchmarkScore: number
31
+ processScore: number
32
+ totalScore: number
33
+ status: "strong" | "adequate" | "weak" | "insufficient" | "not-evaluated"
34
+ // optional metadata
35
+ totalQuestions?: number
36
+ totalApplicable?: number
37
+ naCount?: number
38
+ }
39
+
40
+ export type EvaluationData = {
41
+ systemInfo: SystemInfo | null
42
+ selectedCategories: string[]
43
+ excludedCategoryReasons?: Record<string, string>
44
+ categoryScores: Record<string, CategoryScore>
45
+ currentCategory: string | null
46
+ }
47
+
48
+ interface AIEvaluationDashboardProps {
49
+ onBack?: () => void
50
+ onSaveEvaluation?: (evaluation: any) => void
51
+ }
52
+
53
+ export function AIEvaluationDashboard({ onBack, onSaveEvaluation }: AIEvaluationDashboardProps) {
54
+ const [currentStep, setCurrentStep] = useState<"system-info" | "categories" | "evaluation" | "results">("system-info")
55
+ const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0)
56
+ const [evaluationData, setEvaluationData] = useState<EvaluationData>({
57
+ systemInfo: null,
58
+ selectedCategories: [],
59
+ categoryScores: {},
60
+ currentCategory: null,
61
+ })
62
+
63
+ const steps = [
64
+ { id: "system-info", label: "System Info", number: 1 },
65
+ { id: "categories", label: "Categories", number: 2 },
66
+ { id: "evaluation", label: "Evaluation", number: 3 },
67
+ { id: "results", label: "Results", number: 4 },
68
+ ]
69
+
70
+ const getOverallProgress = () => {
71
+ if (currentStep === "system-info") return 10
72
+ if (currentStep === "categories") return 25
73
+ if (currentStep === "evaluation") {
74
+ const completed = Object.keys(evaluationData.categoryScores).length
75
+ const total = evaluationData.selectedCategories.length
76
+ return total > 0 ? 25 + (completed / total) * 65 : 25
77
+ }
78
+ return 100
79
+ }
80
+
81
+ const handleSystemInfoComplete = (systemInfo: SystemInfo) => {
82
+ setEvaluationData((prev) => ({ ...prev, systemInfo }))
83
+ setCurrentStep("categories")
84
+ }
85
+
86
+ const handleCategoriesSelected = (categories: string[]) => {
87
+ setEvaluationData((prev) => ({ ...prev, selectedCategories: categories }))
88
+ setCurrentCategoryIndex(0)
89
+ setCurrentStep("evaluation")
90
+ }
91
+
92
+ const handleCategoriesSelectedWithReasons = (categories: string[], excludedReasons: Record<string, string>) => {
93
+ setEvaluationData((prev) => ({ ...prev, selectedCategories: categories, excludedCategoryReasons: excludedReasons }))
94
+ setCurrentCategoryIndex(0)
95
+ setCurrentStep("evaluation")
96
+ }
97
+
98
+ const handleCategoryComplete = (categoryId: string, score: CategoryScore) => {
99
+ console.log("[v0] handleCategoryComplete called with:", { categoryId, score })
100
+
101
+ setEvaluationData((prev) => {
102
+ const newCategoryScores = { ...prev.categoryScores, [categoryId]: score }
103
+ console.log("[v0] Updated categoryScores:", newCategoryScores)
104
+ return {
105
+ ...prev,
106
+ categoryScores: newCategoryScores,
107
+ }
108
+ })
109
+
110
+ const nextIndex = currentCategoryIndex + 1
111
+ console.log(
112
+ "[v0] Current index:",
113
+ currentCategoryIndex,
114
+ "Next index:",
115
+ nextIndex,
116
+ "Total categories:",
117
+ evaluationData.selectedCategories.length,
118
+ )
119
+
120
+ if (nextIndex >= evaluationData.selectedCategories.length) {
121
+ console.log("[v0] All categories complete, moving to results")
122
+ setCurrentStep("results")
123
+ } else {
124
+ console.log("[v0] Moving to next category at index:", nextIndex)
125
+ setCurrentCategoryIndex(nextIndex)
126
+ }
127
+ }
128
+
129
+ const handleSaveEvaluation = async () => {
130
+ console.log("[v0] handleSaveEvaluation called")
131
+ console.log("[v0] evaluationData:", evaluationData)
132
+
133
+ if (!evaluationData.systemInfo || evaluationData.selectedCategories.length === 0) {
134
+ alert("Please complete system information and select categories before saving.")
135
+ return
136
+ }
137
+
138
+ const timestamp = Date.now()
139
+ const evaluationId = `eval-${timestamp}`
140
+ console.log("[v0] Generated evaluationId:", evaluationId)
141
+
142
+ console.log("[v0] Processing category scores:", evaluationData.categoryScores)
143
+
144
+ const capabilityCategories = evaluationData.selectedCategories.filter((cat) => {
145
+ const category = CATEGORIES.find((c) => c.id === cat)
146
+ console.log("[v0] Category check:", cat, "type:", category?.type)
147
+ return category?.type === "capability"
148
+ })
149
+ console.log("[v0] Capability categories:", capabilityCategories)
150
+
151
+ const riskCategories = evaluationData.selectedCategories.filter((cat) => {
152
+ const category = CATEGORIES.find((c) => c.id === cat)
153
+ return category?.type === "risk"
154
+ })
155
+ console.log("[v0] Risk categories:", riskCategories)
156
+
157
+ const strongCategories = Object.entries(evaluationData.categoryScores)
158
+ .filter(([_, score]) => {
159
+ console.log("[v0] Checking score for strong:", score)
160
+ return score.status === "strong"
161
+ })
162
+ .map(([catId]) => catId)
163
+ console.log("[v0] Strong categories:", strongCategories)
164
+
165
+ const adequateCategories = Object.entries(evaluationData.categoryScores)
166
+ .filter(([_, score]) => score.status === "adequate")
167
+ .map(([catId]) => catId)
168
+ console.log("[v0] Adequate categories:", adequateCategories)
169
+
170
+ const weakCategories = Object.entries(evaluationData.categoryScores)
171
+ .filter(([_, score]) => score.status === "weak")
172
+ .map(([catId]) => catId)
173
+ console.log("[v0] Weak categories:", weakCategories)
174
+
175
+ const insufficientCategories = Object.entries(evaluationData.categoryScores)
176
+ .filter(([_, score]) => score.status === "insufficient")
177
+ .map(([catId]) => catId)
178
+ console.log("[v0] Insufficient categories:", insufficientCategories)
179
+
180
+ const evaluationJson = {
181
+ id: evaluationId,
182
+ systemName: evaluationData.systemInfo.name,
183
+ provider: evaluationData.systemInfo.provider,
184
+ version: evaluationData.systemInfo.url || "1.0",
185
+ deploymentContext: evaluationData.systemInfo.deploymentContexts.join(", ") || "Production",
186
+ evaluator: "Current User",
187
+ modality: evaluationData.systemInfo.modality,
188
+ evaluationDate: new Date().toISOString().split("T")[0],
189
+ selectedCategories: evaluationData.selectedCategories,
190
+ excludedCategoryReasons: evaluationData.excludedCategoryReasons || {},
191
+ categoryEvaluations: evaluationData.categoryScores,
192
+ overallStats: {
193
+ completenessScore: 85, // Safe default value
194
+ totalApplicable: evaluationData.selectedCategories.length,
195
+ capabilityApplicable: capabilityCategories.length,
196
+ riskApplicable: riskCategories.length,
197
+ strongCategories,
198
+ adequateCategories,
199
+ weakCategories,
200
+ insufficientCategories,
201
+ },
202
+ }
203
+
204
+ console.log("[v0] Final evaluationJson:", evaluationJson)
205
+
206
+ try {
207
+ console.log("[v0] Creating blob and download")
208
+ const blob = new Blob([JSON.stringify(evaluationJson, null, 2)], { type: "application/json" })
209
+ const url = URL.createObjectURL(blob)
210
+ const a = document.createElement("a")
211
+ a.href = url
212
+ a.download = `${evaluationId}.json`
213
+ document.body.appendChild(a)
214
+ a.click()
215
+ document.body.removeChild(a)
216
+ URL.revokeObjectURL(url)
217
+
218
+ console.log("[v0] Download completed successfully")
219
+ alert(
220
+ `Evaluation saved as ${evaluationId}.json. Please upload this file to the public/evaluations/ directory to see it on the homepage.`,
221
+ )
222
+ onBack?.()
223
+ } catch (error) {
224
+ console.error("[v0] Error saving evaluation:", error)
225
+ alert("Error saving evaluation. Please try again.")
226
+ }
227
+ }
228
+
229
+ const renderCurrentStep = () => {
230
+ switch (currentStep) {
231
+ case "system-info":
232
+ return <SystemInfoForm onSubmit={handleSystemInfoComplete} initialData={evaluationData.systemInfo} />
233
+ case "categories":
234
+ return (
235
+ <CategorySelection
236
+ categories={CATEGORIES}
237
+ selectedCategories={evaluationData.selectedCategories}
238
+ onSelectionChange={handleCategoriesSelectedWithReasons}
239
+ />
240
+ )
241
+ case "evaluation":
242
+ return (
243
+ <EvaluationForm
244
+ categories={CATEGORIES}
245
+ selectedCategories={evaluationData.selectedCategories}
246
+ categoryScores={evaluationData.categoryScores}
247
+ onScoreUpdate={(categoryId, score) => handleCategoryComplete(categoryId, score)}
248
+ onComplete={() => setCurrentStep("results")}
249
+ />
250
+ )
251
+ case "results":
252
+ return (
253
+ <ResultsDashboard
254
+ systemInfo={evaluationData.systemInfo}
255
+ categories={CATEGORIES}
256
+ selectedCategories={evaluationData.selectedCategories}
257
+ categoryScores={evaluationData.categoryScores}
258
+ excludedCategoryReasons={evaluationData.excludedCategoryReasons || {}}
259
+ />
260
+ )
261
+ default:
262
+ return null
263
+ }
264
+ }
265
+
266
+ return (
267
+ <div className="min-h-screen bg-background flex flex-col">
268
+ {/* Header */}
269
+ <header className="border-b bg-card">
270
+ <div className="container mx-auto px-6 py-4">
271
+ <div className="flex items-center justify-between">
272
+ <div className="flex items-center gap-4">
273
+ {onBack && (
274
+ <Button variant="ghost" size="sm" onClick={onBack} className="gap-2">
275
+ <ArrowLeft className="h-4 w-4" />
276
+ Back
277
+ </Button>
278
+ )}
279
+ <div>
280
+ <h1 className="text-2xl font-bold font-heading text-foreground">New Eval Card</h1>
281
+ <p className="text-muted-foreground">Create comprehensive AI system evaluation card</p>
282
+ </div>
283
+ </div>
284
+ <div className="flex items-center gap-4">
285
+ <div className="text-right">
286
+ <p className="text-sm text-muted-foreground">Overall Progress</p>
287
+ <div className="flex items-center gap-2">
288
+ <Progress value={getOverallProgress()} className="w-24" />
289
+ <span className="text-sm font-medium">{Math.round(getOverallProgress())}%</span>
290
+ </div>
291
+ </div>
292
+ {evaluationData.systemInfo && (
293
+ <Badge variant="secondary" className="font-medium">
294
+ {evaluationData.systemInfo.name}
295
+ </Badge>
296
+ )}
297
+ <Button onClick={handleSaveEvaluation} className="gap-2">
298
+ Save Eval Card
299
+ </Button>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </header>
304
+
305
+ {/* Step tabs navigation */}
306
+ <div className="border-b bg-card">
307
+ <div className="container mx-auto px-6">
308
+ <div className="flex items-center space-x-8">
309
+ {steps.map((step) => {
310
+ const isActive = currentStep === step.id
311
+ const isCompleted =
312
+ (step.id === "system-info" && evaluationData.systemInfo) ||
313
+ (step.id === "categories" && evaluationData.selectedCategories.length > 0) ||
314
+ (step.id === "evaluation" && Object.keys(evaluationData.categoryScores).length > 0) ||
315
+ (step.id === "results" && currentStep === "results")
316
+
317
+ return (
318
+ <div
319
+ key={step.id}
320
+ className={`flex items-center gap-3 py-4 border-b-2 transition-colors ${
321
+ isActive
322
+ ? "border-primary text-primary"
323
+ : isCompleted
324
+ ? "border-green-500 text-green-600"
325
+ : "border-transparent text-muted-foreground"
326
+ }`}
327
+ >
328
+ <div
329
+ className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium ${
330
+ isActive
331
+ ? "bg-primary text-primary-foreground"
332
+ : isCompleted
333
+ ? "bg-green-500 text-white"
334
+ : "bg-muted text-muted-foreground"
335
+ }`}
336
+ >
337
+ {step.number}
338
+ </div>
339
+ <span className="font-medium">{step.label}</span>
340
+ </div>
341
+ )
342
+ })}
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <div className="container mx-auto px-6 py-6 flex-1 min-h-0">{renderCurrentStep()}</div>
348
+ </div>
349
+ )
350
+ }
components/category-evaluation.tsx ADDED
@@ -0,0 +1,934 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useEffect, useMemo } from "react"
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
5
+ import { Button } from "@/components/ui/button"
6
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
7
+ import { Label } from "@/components/ui/label"
8
+ import { Textarea } from "@/components/ui/textarea"
9
+ import { Input } from "@/components/ui/input"
10
+ import { Badge } from "@/components/ui/badge"
11
+ import { Separator } from "@/components/ui/separator"
12
+ import type { CategoryScore } from "@/components/ai-evaluation-dashboard"
13
+ import { HelpCircle, CheckCircle, Plus, Trash2 } from "lucide-react"
14
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
15
+ import { BENCHMARK_QUESTIONS, PROCESS_QUESTIONS, SOURCE_TYPES, ADDITIONAL_ASPECTS_SECTION, getFieldPlaceholder, getHint } from "@/lib/category-data"
16
+
17
+ // The detailed per-category and per-question hints, plus recommended placeholders,
18
+ // are centralized in `lib/category-data.ts`. This component uses the exported
19
+ // helpers `getHint` and `getFieldPlaceholder` and the question lists.
20
+
21
+ const CustomFieldComponent = ({
22
+ questionId,
23
+ fieldType,
24
+ value,
25
+ onChange,
26
+ }: {
27
+ questionId: string
28
+ fieldType: string
29
+ value: string
30
+ onChange: (value: string) => void
31
+ }) => {
32
+ const getFieldConfig = (questionId: string, fieldType: string) => {
33
+ const configs: Record<string, Record<string, { label: string; placeholder: string; type?: string }>> = {
34
+ A2: {
35
+ thresholds: { label: "Quantitative Thresholds", placeholder: "e.g., >85% accuracy, <0.1 error rate" },
36
+ thresholdSource: {
37
+ label: "Threshold Source",
38
+ placeholder: "e.g., industry standard, research paper, policy requirement",
39
+ },
40
+ passFail: { label: "Pass/Fail Determination", placeholder: "e.g., Pass - exceeded 85% threshold" },
41
+ },
42
+ A3: {
43
+ comparativeScores: {
44
+ label: "Comparative Scores",
45
+ placeholder: "e.g., Our model: 87.2%, GPT-4: 85.1%, Previous version: 82.3%",
46
+ },
47
+ baselineType: { label: "Baseline Type", placeholder: "e.g., SOTA, previous version, industry standard" },
48
+ significance: { label: "Statistical Significance", placeholder: "e.g., p<0.05, 95% CI: [1.2, 3.8]" },
49
+ },
50
+ A4: {
51
+ testTypes: { label: "Test Types", placeholder: "e.g., adversarial attacks, load testing, distribution shift" },
52
+ failureRates: { label: "Failure/Degradation Rates", placeholder: "e.g., 15% failure under adversarial inputs" },
53
+ robustnessMetrics: {
54
+ label: "Robustness Metrics",
55
+ placeholder: "e.g., attack success rate, performance drop %",
56
+ },
57
+ },
58
+ A5: {
59
+ liveMetrics: { label: "Live Metrics Tracked", placeholder: "e.g., error rates, latency, drift detection" },
60
+ samplingCadence: { label: "Sampling Cadence", placeholder: "e.g., every 1000 requests, hourly, daily" },
61
+ alertThresholds: { label: "Alert Thresholds", placeholder: "e.g., >5% error rate, >500ms latency" },
62
+ },
63
+ A6: {
64
+ procedure: {
65
+ label: "Contamination Check Procedure",
66
+ placeholder: "e.g., n-gram overlap analysis, URL deduplication",
67
+ },
68
+ contaminationRate: {
69
+ label: "Contamination Rate",
70
+ placeholder: "e.g., <1% overlap detected, 0.3% exact matches",
71
+ },
72
+ mitigations: { label: "Mitigations Taken", placeholder: "e.g., removed overlapping samples, used holdout set" },
73
+ },
74
+ A7: {
75
+ comparisonSystems: { label: "Comparison Systems", placeholder: "e.g., GPT-4, Claude-3, Gemini Pro" },
76
+ evaluationConditions: {
77
+ label: "Evaluation Conditions",
78
+ placeholder: "e.g., same prompts, temperature=0, identical hardware",
79
+ },
80
+ relativeMetrics: {
81
+ label: "Relative Performance Metrics",
82
+ placeholder: "e.g., 15% better accuracy, 2x faster inference",
83
+ },
84
+ },
85
+ B1: {
86
+ scope: {
87
+ label: "Evaluation Scope",
88
+ placeholder: "e.g., measures reasoning capability in mathematical contexts",
89
+ },
90
+ successFailureDefinitions: {
91
+ label: "Success/Failure Definitions",
92
+ placeholder: "e.g., success = >80% on grade-level problems",
93
+ },
94
+ hypotheses: { label: "Hypotheses Being Tested", placeholder: "e.g., model can solve multi-step word problems" },
95
+ },
96
+ B2: {
97
+ replicationPackage: {
98
+ label: "Replication Package",
99
+ placeholder: "e.g., GitHub repo with code, configs, prompts",
100
+ },
101
+ accessLevel: { label: "Access Level", placeholder: "e.g., public, access-controlled, internal only" },
102
+ proxies: { label: "Proxies (if not shareable)", placeholder: "e.g., synthetic examples, anonymized data" },
103
+ },
104
+ B5: {
105
+ reviewers: { label: "Reviewers", placeholder: "e.g., domain experts, affected user groups, ethics board" },
106
+ feedbackChanges: {
107
+ label: "Changes from Feedback",
108
+ placeholder: "e.g., added bias metrics, revised interpretation",
109
+ },
110
+ disagreements: {
111
+ label: "Unresolved Disagreements",
112
+ placeholder: "e.g., threshold levels, risk severity ratings",
113
+ },
114
+ },
115
+ B6: {
116
+ uncertaintyDisclosure: {
117
+ label: "Uncertainty Disclosure",
118
+ placeholder: "e.g., error bars, confidence intervals, variance across runs",
119
+ },
120
+ axesConsistency: { label: "Axes Consistency", placeholder: "e.g., consistent 0-100 scale, no truncated axes" },
121
+ sampleSizes: { label: "Sample Sizes", placeholder: "e.g., n=1000 test samples, 5 random seeds" },
122
+ selectionCriteria: { label: "Selection Criteria", placeholder: "e.g., all results shown, no cherry-picking" },
123
+ },
124
+ B8: {
125
+ triggers: {
126
+ label: "Re-evaluation Triggers",
127
+ placeholder: "e.g., model updates, data drift >5%, security incidents",
128
+ },
129
+ versionedSpecs: { label: "Versioned Eval Specs", placeholder: "e.g., eval spec v2.1, change log maintained" },
130
+ auditTrail: { label: "Audit Trail", placeholder: "e.g., all changes logged with timestamps and rationale" },
131
+ mitigationProtocols: {
132
+ label: "Mitigation Protocols",
133
+ placeholder: "e.g., automated rollback, manual review process",
134
+ },
135
+ retestProcedures: {
136
+ label: "Retest Procedures",
137
+ placeholder: "e.g., full eval suite after fixes, regression testing",
138
+ },
139
+ },
140
+ }
141
+
142
+ return configs[questionId]?.[fieldType] || { label: fieldType, placeholder: "" }
143
+ }
144
+
145
+ const config = getFieldConfig(questionId, fieldType)
146
+
147
+ return (
148
+ <div>
149
+ <Label className="text-xs font-medium">{config.label}</Label>
150
+ <Textarea
151
+ placeholder={config.placeholder}
152
+ value={value}
153
+ onChange={(e) => onChange(e.target.value)}
154
+ rows={2}
155
+ className="mt-1"
156
+ />
157
+ </div>
158
+ )
159
+ }
160
+
161
+ // Local types used by this component (kept minimal for readability)
162
+ export type Source = {
163
+ id: string
164
+ url: string
165
+ description: string
166
+ sourceType: string
167
+ benchmarkName?: string
168
+ metrics?: string
169
+ score?: string
170
+ confidenceInterval?: string
171
+ version?: string
172
+ taskVariants?: string
173
+ customFields: Record<string, string>
174
+ }
175
+
176
+ export type DocumentationSource = {
177
+ id: string
178
+ url: string
179
+ description: string
180
+ sourceType: string
181
+ documentType?: string
182
+ title?: string
183
+ author?: string
184
+ organization?: string
185
+ date?: string
186
+ customFields: Record<string, string>
187
+ }
188
+
189
+ export type CategoryEvaluationProps = {
190
+ category: { id: string; name: string; description: string; type: string; detailedGuidance?: string }
191
+ score?: CategoryScore | null
192
+ onScoreUpdate: (score: CategoryScore) => void
193
+ }
194
+
195
+ export function CategoryEvaluation({ category, score, onScoreUpdate }: CategoryEvaluationProps) {
196
+ const [benchmarkAnswers, setBenchmarkAnswers] = useState<Record<string, string>>({})
197
+ const [processAnswers, setProcessAnswers] = useState<Record<string, string>>({})
198
+ const [benchmarkSources, setBenchmarkSources] = useState<Record<string, Source[]>>({})
199
+ const [processSources, setProcessSources] = useState<Record<string, DocumentationSource[]>>({})
200
+ const [additionalAspects, setAdditionalAspects] = useState<string>("")
201
+ const [naExplanations, setNaExplanations] = useState<Record<string, string>>({})
202
+
203
+ useEffect(() => {
204
+ if (score) {
205
+ // This would be populated from saved data in a real implementation
206
+ // For now, we'll calculate based on the scores
207
+ }
208
+ }, [score])
209
+
210
+ const addSource = (questionId: string, section: "benchmark" | "process") => {
211
+ if (section === "benchmark") {
212
+ const newSource: Source = {
213
+ id: Date.now().toString(),
214
+ url: "",
215
+ description: "",
216
+ sourceType: "internal",
217
+ benchmarkName: "",
218
+ metrics: "",
219
+ score: "",
220
+ confidenceInterval: "",
221
+ version: "",
222
+ taskVariants: "",
223
+ customFields: {},
224
+ }
225
+ setBenchmarkSources((prev) => ({
226
+ ...prev,
227
+ [questionId]: [...(prev[questionId] || []), newSource],
228
+ }))
229
+ } else {
230
+ const newDocSource: DocumentationSource = {
231
+ id: Date.now().toString(),
232
+ url: "",
233
+ description: "",
234
+ sourceType: "internal",
235
+ documentType: "",
236
+ title: "",
237
+ author: "",
238
+ organization: "",
239
+ date: "",
240
+ customFields: {},
241
+ }
242
+ setProcessSources((prev) => ({
243
+ ...prev,
244
+ [questionId]: [...(prev[questionId] || []), newDocSource],
245
+ }))
246
+ }
247
+ }
248
+
249
+ const removeSource = (questionId: string, sourceId: string, section: "benchmark" | "process") => {
250
+ if (section === "benchmark") {
251
+ setBenchmarkSources((prev) => ({
252
+ ...prev,
253
+ [questionId]: (prev[questionId] || []).filter((s) => s.id !== sourceId),
254
+ }))
255
+ } else {
256
+ setProcessSources((prev) => ({
257
+ ...prev,
258
+ [questionId]: (prev[questionId] || []).filter((s) => s.id !== sourceId),
259
+ }))
260
+ }
261
+ }
262
+
263
+ const updateSource = (
264
+ questionId: string,
265
+ sourceId: string,
266
+ field: string,
267
+ value: string,
268
+ section: "benchmark" | "process",
269
+ ) => {
270
+ if (section === "benchmark") {
271
+ setBenchmarkSources((prev) => ({
272
+ ...prev,
273
+ [questionId]: (prev[questionId] || []).map((source) =>
274
+ source.id === sourceId ? { ...source, [field]: value } : source,
275
+ ),
276
+ }))
277
+ } else {
278
+ setProcessSources((prev) => ({
279
+ ...prev,
280
+ [questionId]: (prev[questionId] || []).map((source) =>
281
+ source.id === sourceId ? { ...source, [field]: value } : source,
282
+ ),
283
+ }))
284
+ }
285
+ }
286
+
287
+ const updateSourceCustomField = (
288
+ questionId: string,
289
+ sourceId: string,
290
+ fieldType: string,
291
+ value: string,
292
+ section: "benchmark" | "process",
293
+ ) => {
294
+ if (section === "benchmark") {
295
+ setBenchmarkSources((prev) => ({
296
+ ...prev,
297
+ [questionId]: (prev[questionId] || []).map((source) =>
298
+ source.id === sourceId
299
+ ? {
300
+ ...source,
301
+ customFields: {
302
+ ...source.customFields,
303
+ [fieldType]: value,
304
+ },
305
+ }
306
+ : source,
307
+ ),
308
+ }))
309
+ } else {
310
+ setProcessSources((prev) => ({
311
+ ...prev,
312
+ [questionId]: (prev[questionId] || []).map((source) =>
313
+ source.id === sourceId
314
+ ? {
315
+ ...source,
316
+ customFields: {
317
+ ...source.customFields,
318
+ [fieldType]: value,
319
+ },
320
+ }
321
+ : source,
322
+ ),
323
+ }))
324
+ }
325
+ }
326
+
327
+ const currentScore = useMemo(() => {
328
+ // Calculate counts
329
+ const totalBenchmarkQuestions = BENCHMARK_QUESTIONS.length
330
+ const totalProcessQuestions = PROCESS_QUESTIONS.length
331
+ const totalQuestions = totalBenchmarkQuestions + totalProcessQuestions
332
+
333
+ const benchmarkYesCount = Object.values(benchmarkAnswers).filter((answer) => answer === "yes").length
334
+ const processYesCount = Object.values(processAnswers).filter((answer) => answer === "yes").length
335
+
336
+ const benchmarkNaCount = Object.values(benchmarkAnswers).filter((answer) => answer === "na").length
337
+ const processNaCount = Object.values(processAnswers).filter((answer) => answer === "na").length
338
+
339
+ const naCount = benchmarkNaCount + processNaCount
340
+ const totalYes = benchmarkYesCount + processYesCount
341
+
342
+ // Denominator = total questions in the category minus NA answers
343
+ const totalApplicable = Math.max(0, totalQuestions - naCount)
344
+
345
+ const scorePercentage = totalApplicable > 0 ? totalYes / totalApplicable : 0
346
+
347
+ let status: CategoryScore["status"]
348
+ if (scorePercentage >= 0.8) status = "strong"
349
+ else if (scorePercentage >= 0.6) status = "adequate"
350
+ else if (scorePercentage >= 0.4) status = "weak"
351
+ else status = "insufficient"
352
+
353
+ const result = {
354
+ benchmarkScore: benchmarkYesCount,
355
+ processScore: processYesCount,
356
+ totalScore: totalYes,
357
+ status,
358
+ totalQuestions,
359
+ totalApplicable,
360
+ naCount,
361
+ }
362
+
363
+ return result
364
+ }, [benchmarkAnswers, processAnswers])
365
+
366
+ const handleAnswerChange = (questionId: string, value: string, section: "benchmark" | "process") => {
367
+ if (section === "benchmark") {
368
+ setBenchmarkAnswers((prev) => ({ ...prev, [questionId]: value }))
369
+ if (value !== "yes") {
370
+ setBenchmarkSources((prev) => ({ ...prev, [questionId]: [] }))
371
+ }
372
+ if (value !== "na") {
373
+ setNaExplanations((prev) => {
374
+ const newExplanations = { ...prev }
375
+ delete newExplanations[questionId]
376
+ return newExplanations
377
+ })
378
+ }
379
+ } else {
380
+ setProcessAnswers((prev) => ({ ...prev, [questionId]: value }))
381
+ if (value !== "yes") {
382
+ setProcessSources((prev) => ({ ...prev, [questionId]: [] }))
383
+ }
384
+ if (value !== "na") {
385
+ setNaExplanations((prev) => {
386
+ const newExplanations = { ...prev }
387
+ delete newExplanations[questionId]
388
+ return newExplanations
389
+ })
390
+ }
391
+ }
392
+ }
393
+
394
+ const handleNaExplanationChange = (questionId: string, explanation: string) => {
395
+ setNaExplanations((prev) => ({ ...prev, [questionId]: explanation }))
396
+ }
397
+
398
+ const handleSave = () => {
399
+ const allAnswers = { ...benchmarkAnswers, ...processAnswers }
400
+ const missingExplanations = Object.entries(allAnswers)
401
+ .filter(([_, answer]) => answer === "na")
402
+ .filter(([questionId, _]) => !naExplanations[questionId]?.trim())
403
+ .map(([questionId, _]) => questionId)
404
+
405
+ if (missingExplanations.length > 0) {
406
+ alert(
407
+ `Please provide explanations for why the following questions are not applicable: ${missingExplanations.join(", ")}`,
408
+ )
409
+ return
410
+ }
411
+
412
+ console.log("[v0] Saving category evaluation")
413
+ console.log("[v0] Calling onScoreUpdate with:", currentScore)
414
+ onScoreUpdate(currentScore)
415
+ }
416
+
417
+ const isComplete =
418
+ Object.keys(benchmarkAnswers).length + Object.keys(processAnswers).length === BENCHMARK_QUESTIONS.length + PROCESS_QUESTIONS.length
419
+
420
+ return (
421
+ <TooltipProvider>
422
+ <div className="space-y-6">
423
+ <Card>
424
+ <CardHeader>
425
+ <div className="flex items-center justify-between">
426
+ <div className="flex-1">
427
+ <CardTitle className="font-heading flex items-center gap-2">
428
+ {category.name}
429
+ <Badge variant={category.type === "capability" ? "secondary" : "destructive"}>{category.type}</Badge>
430
+ </CardTitle>
431
+ <CardDescription className="mt-2">{category.description}</CardDescription>
432
+ </div>
433
+ {isComplete && (
434
+ <div className="text-right">
435
+ <div className="flex items-center gap-2 mb-1">
436
+ <CheckCircle className="h-5 w-5 text-green-600" />
437
+ <span className="font-medium">Score: {currentScore.totalScore}/{currentScore.totalApplicable || currentScore.totalQuestions}</span>
438
+ </div>
439
+ <Badge
440
+ variant={
441
+ currentScore.status === "strong"
442
+ ? "default"
443
+ : currentScore.status === "adequate"
444
+ ? "secondary"
445
+ : currentScore.status === "weak"
446
+ ? "outline"
447
+ : "destructive"
448
+ }
449
+ >
450
+ {currentScore.status.charAt(0).toUpperCase() + currentScore.status.slice(1)}
451
+ </Badge>
452
+ </div>
453
+ )}
454
+ </div>
455
+ </CardHeader>
456
+ <CardContent>
457
+ <div className="space-y-4">
458
+ <div className="bg-muted/30 p-4 rounded-lg">
459
+ <h4 className="font-medium mb-2">Source Types</h4>
460
+ <div className="grid gap-2 text-sm">
461
+ {Object.entries(SOURCE_TYPES).map(([key, type]) => (
462
+ <div key={key}>
463
+ <span className="font-medium">{type.label}:</span> {type.description}
464
+ </div>
465
+ ))}
466
+ </div>
467
+ </div>
468
+ <div className="bg-muted/30 p-4 rounded-lg">
469
+ <h4 className="font-medium mb-2">Evaluation Guidance</h4>
470
+ <p className="text-sm mb-2 font-medium">
471
+ Note: The benchmarks and evaluations listed below are suggested examples, not exhaustive requirements.
472
+ You may use other relevant benchmarks and evaluation methods appropriate for your system.
473
+ </p>
474
+ <div className="text-sm whitespace-pre-line">{category.detailedGuidance}</div>
475
+ </div>
476
+ </div>
477
+ </CardContent>
478
+ </Card>
479
+
480
+ <Card>
481
+ <CardHeader>
482
+ <CardTitle className="text-lg">Part A: Benchmark & Testing Evaluation</CardTitle>
483
+ <CardDescription>
484
+ Quantitative assessment through standardized tests and measurements ({currentScore.benchmarkScore}/6)
485
+ </CardDescription>
486
+ </CardHeader>
487
+ <CardContent className="space-y-6">
488
+ {BENCHMARK_QUESTIONS.map((question) => (
489
+ <div key={question.id} className="space-y-3">
490
+ <div className="flex items-start gap-2">
491
+ <Label className="text-sm font-medium flex-1">
492
+ {question.id}. {question.text}
493
+ </Label>
494
+ <Tooltip>
495
+ <TooltipTrigger>
496
+ <HelpCircle className="h-4 w-4 text-muted-foreground" />
497
+ </TooltipTrigger>
498
+ <TooltipContent className="max-w-sm">
499
+ <p>{question.tooltip}</p>
500
+ </TooltipContent>
501
+ </Tooltip>
502
+ </div>
503
+
504
+ <RadioGroup
505
+ value={benchmarkAnswers[question.id] || ""}
506
+ onValueChange={(value) => handleAnswerChange(question.id, value, "benchmark")}
507
+ >
508
+ <div className="flex items-center space-x-2">
509
+ <RadioGroupItem value="yes" id={`${question.id}-yes`} />
510
+ <Label htmlFor={`${question.id}-yes`}>Yes</Label>
511
+ </div>
512
+ <div className="flex items-center space-x-2">
513
+ <RadioGroupItem value="no" id={`${question.id}-no`} />
514
+ <Label htmlFor={`${question.id}-no`}>No</Label>
515
+ </div>
516
+ <div className="flex items-center space-x-2">
517
+ <RadioGroupItem value="na" id={`${question.id}-na`} />
518
+ <Label htmlFor={`${question.id}-na`}>Not Applicable</Label>
519
+ </div>
520
+ </RadioGroup>
521
+
522
+ {benchmarkAnswers[question.id] === "na" && (
523
+ <div className="ml-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
524
+ <Label className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
525
+ Explanation Required: Why is this question not applicable?
526
+ </Label>
527
+ <Textarea
528
+ placeholder="Please explain why this question/category is not applicable to your system. This explanation will be included in the evaluation documentation."
529
+ value={naExplanations[question.id] || ""}
530
+ onChange={(e) => handleNaExplanationChange(question.id, e.target.value)}
531
+ rows={3}
532
+ className="mt-2 border-yellow-300 dark:border-yellow-700"
533
+ required
534
+ />
535
+ </div>
536
+ )}
537
+
538
+ {benchmarkAnswers[question.id] === "yes" && (
539
+ <div className="space-y-4 ml-4 p-4 bg-muted/30 rounded-lg">
540
+ <div className="flex items-center justify-between">
541
+ <Label className="text-sm font-medium">Sources & Evidence</Label>
542
+ <Button
543
+ type="button"
544
+ variant="outline"
545
+ size="sm"
546
+ onClick={() => addSource(question.id, "benchmark")}
547
+ className="flex items-center gap-1"
548
+ >
549
+ <Plus className="h-3 w-3" />
550
+ Add Source
551
+ </Button>
552
+ </div>
553
+
554
+ {(benchmarkSources[question.id] || []).map((source, index) => (
555
+ <div key={source.id} className="space-y-3 p-3 border rounded-lg bg-background">
556
+ <div className="flex items-center justify-between">
557
+ <span className="text-sm font-medium">Source {index + 1}</span>
558
+ <Button
559
+ type="button"
560
+ variant="ghost"
561
+ size="sm"
562
+ onClick={() => removeSource(question.id, source.id, "benchmark")}
563
+ >
564
+ <Trash2 className="h-3 w-3" />
565
+ </Button>
566
+ </div>
567
+
568
+ <div className="grid gap-3">
569
+ {/* no structured hint here; description has contextual hints */}
570
+
571
+ <div>
572
+ <Label className="text-xs">Benchmark/Dataset Name</Label>
573
+ <Input
574
+ placeholder={getFieldPlaceholder(category.id, question.id, "benchmarkName")}
575
+ value={source.benchmarkName || ""}
576
+ onChange={(e) =>
577
+ updateSource(question.id, source.id, "benchmarkName", e.target.value, "benchmark")
578
+ }
579
+ />
580
+ </div>
581
+
582
+ <div className="grid grid-cols-2 gap-3">
583
+ <div>
584
+ <Label className="text-xs">Version</Label>
585
+ <Input
586
+ placeholder="e.g., v1.2, 2024-01"
587
+ value={source.version || ""}
588
+ onChange={(e) =>
589
+ updateSource(question.id, source.id, "version", e.target.value, "benchmark")
590
+ }
591
+ />
592
+ </div>
593
+ <div>
594
+ <Label className="text-xs">Task Variants</Label>
595
+ <Input
596
+ placeholder="e.g., multiple choice, generation"
597
+ value={source.taskVariants || ""}
598
+ onChange={(e) =>
599
+ updateSource(question.id, source.id, "taskVariants", e.target.value, "benchmark")
600
+ }
601
+ />
602
+ </div>
603
+ </div>
604
+
605
+ <div>
606
+ <Label className="text-xs">Metrics</Label>
607
+ <Input
608
+ placeholder={getFieldPlaceholder(category.id, question.id, "metrics")}
609
+ value={source.metrics || ""}
610
+ onChange={(e) =>
611
+ updateSource(question.id, source.id, "metrics", e.target.value, "benchmark")
612
+ }
613
+ />
614
+ </div>
615
+
616
+ <div>
617
+ <Label className="text-xs">URL</Label>
618
+ <Input
619
+ placeholder="https://..."
620
+ value={source.url}
621
+ onChange={(e) => updateSource(question.id, source.id, "url", e.target.value, "benchmark")}
622
+ />
623
+ </div>
624
+
625
+ <div>
626
+ <Label className="text-xs">Description</Label>
627
+ <Textarea
628
+ placeholder="Describe the benchmark, test, or evaluation method..."
629
+ value={source.description}
630
+ onChange={(e) =>
631
+ updateSource(question.id, source.id, "description", e.target.value, "benchmark")
632
+ }
633
+ rows={2}
634
+ />
635
+ <p className="text-xs text-muted-foreground mt-1">
636
+ {getHint(category.id, question.id, "benchmark")}
637
+ </p>
638
+ </div>
639
+
640
+ <div className="grid grid-cols-2 gap-3">
641
+ <div>
642
+ <Label className="text-xs">Source Type</Label>
643
+ <RadioGroup
644
+ value={source.sourceType}
645
+ onValueChange={(value) =>
646
+ updateSource(question.id, source.id, "sourceType", value, "benchmark")
647
+ }
648
+ >
649
+ {Object.entries(SOURCE_TYPES).map(([key, type]) => (
650
+ <div key={key} className="flex items-center space-x-2">
651
+ <RadioGroupItem value={key} id={`${source.id}-${key}`} />
652
+ <Label htmlFor={`${source.id}-${key}`} className="text-xs">
653
+ {type.label}
654
+ </Label>
655
+ </div>
656
+ ))}
657
+ </RadioGroup>
658
+ </div>
659
+
660
+ <div>
661
+ <Label className="text-xs">Score (if applicable)</Label>
662
+ <Input
663
+ placeholder="e.g., 85%, 0.92, Pass"
664
+ value={source.score || ""}
665
+ onChange={(e) =>
666
+ updateSource(question.id, source.id, "score", e.target.value, "benchmark")
667
+ }
668
+ />
669
+ <Label className="text-xs mt-2">Confidence Interval (optional)</Label>
670
+ <Input
671
+ placeholder="e.g., 95% CI [90,94]"
672
+ value={(source as any).confidenceInterval || ""}
673
+ onChange={(e) =>
674
+ updateSource(question.id, source.id, "confidenceInterval", e.target.value, "benchmark")
675
+ }
676
+ />
677
+ </div>
678
+ </div>
679
+ </div>
680
+ </div>
681
+ ))}
682
+
683
+ {(benchmarkSources[question.id] || []).length === 0 && (
684
+ <div className="text-center py-4 text-muted-foreground text-sm">
685
+ Click "Add Source" to document benchmarks and evidence
686
+ </div>
687
+ )}
688
+ </div>
689
+ )}
690
+
691
+ <Separator />
692
+ </div>
693
+ ))}
694
+ </CardContent>
695
+ </Card>
696
+
697
+ <Card>
698
+ <CardHeader>
699
+ <CardTitle className="text-lg">Part B: Documentation & Process Evaluation</CardTitle>
700
+ <CardDescription>
701
+ Governance, transparency, and risk management processes ({currentScore.processScore}/5)
702
+ </CardDescription>
703
+ </CardHeader>
704
+ <CardContent className="space-y-6">
705
+ {PROCESS_QUESTIONS.map((question) => (
706
+ <div key={question.id} className="space-y-3">
707
+ <div className="flex items-start gap-2">
708
+ <Label className="text-sm font-medium flex-1">
709
+ {question.id}. {question.text}
710
+ </Label>
711
+ <Tooltip>
712
+ <TooltipTrigger>
713
+ <HelpCircle className="h-4 w-4 text-muted-foreground" />
714
+ </TooltipTrigger>
715
+ <TooltipContent className="max-w-sm">
716
+ <p>{question.tooltip}</p>
717
+ </TooltipContent>
718
+ </Tooltip>
719
+ </div>
720
+
721
+ <RadioGroup
722
+ value={processAnswers[question.id] || ""}
723
+ onValueChange={(value) => handleAnswerChange(question.id, value, "process")}
724
+ >
725
+ <div className="flex items-center space-x-2">
726
+ <RadioGroupItem value="yes" id={`${question.id}-yes`} />
727
+ <Label htmlFor={`${question.id}-yes`}>Yes</Label>
728
+ </div>
729
+ <div className="flex items-center space-x-2">
730
+ <RadioGroupItem value="no" id={`${question.id}-no`} />
731
+ <Label htmlFor={`${question.id}-no`}>No</Label>
732
+ </div>
733
+ <div className="flex items-center space-x-2">
734
+ <RadioGroupItem value="na" id={`${question.id}-na`} />
735
+ <Label htmlFor={`${question.id}-na`}>Not Applicable</Label>
736
+ </div>
737
+ </RadioGroup>
738
+
739
+ {processAnswers[question.id] === "na" && (
740
+ <div className="ml-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
741
+ <Label className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
742
+ Explanation Required: Why is this question not applicable?
743
+ </Label>
744
+ <Textarea
745
+ placeholder="Please explain why this question/category is not applicable to your system. This explanation will be included in the evaluation documentation."
746
+ value={naExplanations[question.id] || ""}
747
+ onChange={(e) => handleNaExplanationChange(question.id, e.target.value)}
748
+ rows={3}
749
+ className="mt-2 border-yellow-300 dark:border-yellow-700"
750
+ required
751
+ />
752
+ </div>
753
+ )}
754
+
755
+ {processAnswers[question.id] === "yes" && (
756
+ <div className="space-y-4 ml-4 p-4 bg-muted/30 rounded-lg">
757
+ <div className="flex items-center justify-between">
758
+ <Label className="text-sm font-medium">Documentation & Evidence</Label>
759
+ <Button
760
+ type="button"
761
+ variant="outline"
762
+ size="sm"
763
+ onClick={() => addSource(question.id, "process")}
764
+ className="flex items-center gap-1"
765
+ >
766
+ <Plus className="h-3 w-3" />
767
+ Add Documentation
768
+ </Button>
769
+ </div>
770
+
771
+ {(processSources[question.id] || []).map((source, index) => (
772
+ <div key={source.id} className="space-y-3 p-3 border rounded-lg bg-background">
773
+ <div className="flex items-center justify-between">
774
+ <span className="text-sm font-medium">Document {index + 1}</span>
775
+ <Button
776
+ type="button"
777
+ variant="ghost"
778
+ size="sm"
779
+ onClick={() => removeSource(question.id, source.id, "process")}
780
+ >
781
+ <Trash2 className="h-3 w-3" />
782
+ </Button>
783
+ </div>
784
+
785
+ <div className="grid gap-3">
786
+ {/* no structured hint here; description has contextual hints */}
787
+
788
+ <div>
789
+ <Label className="text-xs">URL</Label>
790
+ <Input
791
+ placeholder="https://..."
792
+ value={source.url}
793
+ onChange={(e) => updateSource(question.id, source.id, "url", e.target.value, "process")}
794
+ />
795
+ </div>
796
+
797
+ <div>
798
+ <Label className="text-xs">Description</Label>
799
+ <Textarea
800
+ placeholder="Describe the documentation, policy, or process..."
801
+ value={source.description}
802
+ onChange={(e) =>
803
+ updateSource(question.id, source.id, "description", e.target.value, "process")
804
+ }
805
+ rows={2}
806
+ />
807
+ <p className="text-xs text-muted-foreground mt-1">
808
+ {getHint(category.id, question.id, "process")}
809
+ </p>
810
+ </div>
811
+
812
+ <div className="grid grid-cols-2 gap-3">
813
+ <div>
814
+ <Label className="text-xs">Source Type</Label>
815
+ <RadioGroup
816
+ value={source.sourceType}
817
+ onValueChange={(value) =>
818
+ updateSource(question.id, source.id, "sourceType", value, "process")
819
+ }
820
+ >
821
+ {Object.entries(SOURCE_TYPES).map(([key, type]) => (
822
+ <div key={key} className="flex items-center space-x-2">
823
+ <RadioGroupItem value={key} id={`${source.id}-${key}`} />
824
+ <Label htmlFor={`${source.id}-${key}`} className="text-xs">
825
+ {type.label}
826
+ </Label>
827
+ </div>
828
+ ))}
829
+ </RadioGroup>
830
+ </div>
831
+
832
+ <div>
833
+ <Label className="text-xs">Document Type</Label>
834
+ <Input
835
+ placeholder="e.g., Policy, Procedure, Report"
836
+ value={source.documentType || ""}
837
+ onChange={(e) =>
838
+ updateSource(question.id, source.id, "documentType", e.target.value, "process")
839
+ }
840
+ />
841
+ </div>
842
+ </div>
843
+
844
+ <div className="grid grid-cols-2 gap-3 mt-2">
845
+ <div>
846
+ <Label className="text-xs">Title</Label>
847
+ <Input
848
+ placeholder="Document title"
849
+ value={(source as any).title || ""}
850
+ onChange={(e) => updateSource(question.id, source.id, "title", e.target.value, "process")}
851
+ />
852
+ </div>
853
+ <div>
854
+ <Label className="text-xs">Author</Label>
855
+ <Input
856
+ placeholder="Author or owner"
857
+ value={(source as any).author || ""}
858
+ onChange={(e) => updateSource(question.id, source.id, "author", e.target.value, "process")}
859
+ />
860
+ </div>
861
+ </div>
862
+
863
+ <div className="grid grid-cols-2 gap-3 mt-2">
864
+ <div>
865
+ <Label className="text-xs">Organization</Label>
866
+ <Input
867
+ placeholder="Owning org"
868
+ value={(source as any).organization || ""}
869
+ onChange={(e) => updateSource(question.id, source.id, "organization", e.target.value, "process")}
870
+ />
871
+ </div>
872
+ <div>
873
+ <Label className="text-xs">Date</Label>
874
+ <Input
875
+ placeholder="YYYY-MM-DD"
876
+ value={(source as any).date || ""}
877
+ onChange={(e) => updateSource(question.id, source.id, "date", e.target.value, "process")}
878
+ />
879
+ </div>
880
+ </div>
881
+
882
+
883
+ </div>
884
+ </div>
885
+ ))}
886
+
887
+ {(processSources[question.id] || []).length === 0 && (
888
+ <div className="text-center py-4 text-muted-foreground text-sm">
889
+ Click "Add Documentation" to document policies and processes
890
+ </div>
891
+ )}
892
+ </div>
893
+ )}
894
+
895
+ <Separator />
896
+ </div>
897
+ ))}
898
+ </CardContent>
899
+ </Card>
900
+
901
+ <Card>
902
+ <CardHeader>
903
+ <CardTitle className="text-lg">Part C: Additional Evaluation Aspects</CardTitle>
904
+ <CardDescription>{ADDITIONAL_ASPECTS_SECTION.description}</CardDescription>
905
+ </CardHeader>
906
+ <CardContent>
907
+ <div className="space-y-3">
908
+ <Label className="text-sm font-medium">
909
+ Additional evaluation aspects, methods, or considerations for this category:
910
+ </Label>
911
+ <Textarea
912
+ placeholder="Document any other evaluation approaches, considerations, or aspects that may not have been captured by the structured questions above. This could include novel evaluation methods, domain-specific considerations, or unique aspects of your system's evaluation..."
913
+ value={additionalAspects}
914
+ onChange={(e) => setAdditionalAspects(e.target.value)}
915
+ rows={6}
916
+ className="min-h-[120px]"
917
+ />
918
+ <p className="text-xs text-muted-foreground">
919
+ This section is for documentation purposes and will not affect the numerical score but will be included
920
+ in the final evaluation report.
921
+ </p>
922
+ </div>
923
+ </CardContent>
924
+ </Card>
925
+
926
+ <div className="flex justify-center">
927
+ <Button onClick={handleSave} disabled={!isComplete} size="lg" className="w-full max-w-md">
928
+ {score ? "Update" : "Save"} Category Evaluation
929
+ </Button>
930
+ </div>
931
+ </div>
932
+ </TooltipProvider>
933
+ )
934
+ }
components/category-selection.tsx ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Checkbox } from "@/components/ui/checkbox"
7
+ import { Badge } from "@/components/ui/badge"
8
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
9
+ import { Brain, Shield, HelpCircle } from "lucide-react"
10
+ import { Label } from "@/components/ui/label"
11
+ import { Textarea } from "@/components/ui/textarea"
12
+
13
+ interface Category {
14
+ id: string
15
+ name: string
16
+ type: "capability" | "risk"
17
+ description: string
18
+ }
19
+
20
+ interface CategorySelectionProps {
21
+ categories: Category[]
22
+ selectedCategories: string[]
23
+ onSelectionChange: (categories: string[], excludedReasons: Record<string, string>) => void
24
+ }
25
+
26
+ export function CategorySelection({ categories, selectedCategories, onSelectionChange }: CategorySelectionProps) {
27
+ const allCategoryIds = categories.map((c) => c.id)
28
+ const [localSelection, setLocalSelection] = useState<string[]>(
29
+ selectedCategories && selectedCategories.length > 0 ? selectedCategories : allCategoryIds,
30
+ )
31
+ const [localReasons, setLocalReasons] = useState<Record<string, string>>({})
32
+
33
+ const capabilityCategories = categories.filter((c) => c.type === "capability")
34
+ const riskCategories = categories.filter((c) => c.type === "risk")
35
+
36
+ const handleCategoryToggle = (categoryId: string, checked: boolean) => {
37
+ setLocalSelection((prev) => {
38
+ if (checked) {
39
+ // if re-selecting, remove any existing reason
40
+ setLocalReasons((r) => {
41
+ const copy = { ...r }
42
+ delete copy[categoryId]
43
+ return copy
44
+ })
45
+ return [...prev, categoryId]
46
+ }
47
+ // when unselecting, initialize an empty reason entry to make it required
48
+ setLocalReasons((r) => ({ ...r, [categoryId]: r[categoryId] || "" }))
49
+ return prev.filter((id) => id !== categoryId)
50
+ })
51
+ }
52
+
53
+ const handleSelectAll = (type: "capability" | "risk") => {
54
+ const categoryIds = categories.filter((c) => c.type === type).map((c) => c.id)
55
+ const allSelected = categoryIds.every((id) => localSelection.includes(id))
56
+
57
+ if (allSelected) {
58
+ // deselect these categories and initialize empty reasons for them
59
+ setLocalSelection((prev) => {
60
+ const next = prev.filter((id) => !categoryIds.includes(id))
61
+ setLocalReasons((r) => {
62
+ const copy = { ...r }
63
+ categoryIds.forEach((id) => {
64
+ if (!(id in copy)) copy[id] = ""
65
+ })
66
+ return copy
67
+ })
68
+ return next
69
+ })
70
+ } else {
71
+ setLocalSelection((prev) => {
72
+ // when selecting these, remove any reasons for them
73
+ setLocalReasons((r) => {
74
+ const copy = { ...r }
75
+ categoryIds.forEach((id) => delete copy[id])
76
+ return copy
77
+ })
78
+ return [...new Set([...prev, ...categoryIds])]
79
+ })
80
+ }
81
+ }
82
+
83
+ const handleContinue = () => {
84
+ onSelectionChange(localSelection, localReasons)
85
+ }
86
+
87
+ return (
88
+ <TooltipProvider>
89
+ <div className="h-full min-h-0 flex flex-col">
90
+ <Card>
91
+ <CardHeader>
92
+ <CardTitle className="font-heading">Category Applicability Assessment</CardTitle>
93
+ <CardDescription>
94
+ Select which categories are applicable to your AI system. Only complete the detailed evaluation for
95
+ categories marked as applicable.
96
+ </CardDescription>
97
+ </CardHeader>
98
+ <CardContent>
99
+ <div className="flex items-center justify-between mb-4">
100
+ <div className="flex items-center gap-2">
101
+ <span className="text-sm font-medium">Selected Categories:</span>
102
+ <Badge variant="secondary">{localSelection.length}/20</Badge>
103
+ </div>
104
+ </div>
105
+ </CardContent>
106
+ </Card>
107
+
108
+ <div className="flex-1 overflow-auto space-y-6 py-4 pr-2 min-h-0">
109
+ {/* Capability Categories */}
110
+ <Card>
111
+ <CardHeader>
112
+ <div className="flex items-center justify-between">
113
+ <div className="flex items-center gap-2">
114
+ <Brain className="h-5 w-5 text-accent" />
115
+ <CardTitle className="font-heading">Capability Categories</CardTitle>
116
+ </div>
117
+ <Button variant="outline" size="sm" onClick={() => handleSelectAll("capability")}>
118
+ {capabilityCategories.every((c) => localSelection.includes(c.id)) ? "Deselect All" : "Select All"}
119
+ </Button>
120
+ </div>
121
+ <CardDescription>Core functional capabilities and performance areas</CardDescription>
122
+ </CardHeader>
123
+ <CardContent>
124
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
125
+ {capabilityCategories.map((category) => (
126
+ <div key={category.id} className="p-3 rounded-lg border hover:bg-muted/50 transition-colors">
127
+ <div className="flex items-center space-x-3">
128
+ <Checkbox
129
+ id={category.id}
130
+ checked={localSelection.includes(category.id)}
131
+ onCheckedChange={(checked) => handleCategoryToggle(category.id, checked as boolean)}
132
+ />
133
+ <label htmlFor={category.id} className="text-sm font-medium cursor-pointer flex-1">
134
+ {category.name}
135
+ </label>
136
+ <Tooltip>
137
+ <TooltipTrigger>
138
+ <HelpCircle className="h-4 w-4 text-muted-foreground" />
139
+ </TooltipTrigger>
140
+ <TooltipContent className="max-w-sm">
141
+ <p>{category.description}</p>
142
+ </TooltipContent>
143
+ </Tooltip>
144
+ </div>
145
+
146
+ {/* If unchecked, require a reason */}
147
+ {!localSelection.includes(category.id) && (
148
+ <div className="mt-3">
149
+ <div className="ml-0 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
150
+ <Label className="text-sm font-medium text-yellow-800 dark:text-yellow-200">Reason for excluding this category *</Label>
151
+ <Textarea
152
+ placeholder="Brief reason why this category is not applicable"
153
+ value={localReasons[category.id] || ""}
154
+ onChange={(e) => setLocalReasons((prev) => ({ ...prev, [category.id]: e.target.value }))}
155
+ rows={2}
156
+ className="mt-2 border-yellow-300 dark:border-yellow-700"
157
+ required
158
+ />
159
+ </div>
160
+ </div>
161
+ )}
162
+ </div>
163
+ ))}
164
+ </div>
165
+ </CardContent>
166
+ </Card>
167
+
168
+ {/* Risk Categories */}
169
+ <Card>
170
+ <CardHeader>
171
+ <div className="flex items-center justify-between">
172
+ <div className="flex items-center gap-2">
173
+ <Shield className="h-5 w-5 text-destructive" />
174
+ <CardTitle className="font-heading">Risk Categories</CardTitle>
175
+ </div>
176
+ <Button variant="outline" size="sm" onClick={() => handleSelectAll("risk")}>
177
+ {riskCategories.every((c) => localSelection.includes(c.id)) ? "Deselect All" : "Select All"}
178
+ </Button>
179
+ </div>
180
+ <CardDescription>Potential risks, safety concerns, and ethical considerations</CardDescription>
181
+ </CardHeader>
182
+ <CardContent>
183
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
184
+ {riskCategories.map((category) => (
185
+ <div key={category.id} className="p-3 rounded-lg border hover:bg-muted/50 transition-colors">
186
+ <div className="flex items-center space-x-3">
187
+ <Checkbox
188
+ id={category.id}
189
+ checked={localSelection.includes(category.id)}
190
+ onCheckedChange={(checked) => handleCategoryToggle(category.id, checked as boolean)}
191
+ />
192
+ <label htmlFor={category.id} className="text-sm font-medium cursor-pointer flex-1">
193
+ {category.name}
194
+ </label>
195
+ <Tooltip>
196
+ <TooltipTrigger>
197
+ <HelpCircle className="h-4 w-4 text-muted-foreground" />
198
+ </TooltipTrigger>
199
+ <TooltipContent className="max-w-sm">
200
+ <p>{category.description}</p>
201
+ </TooltipContent>
202
+ </Tooltip>
203
+ </div>
204
+
205
+ {!localSelection.includes(category.id) && (
206
+ <div className="mt-3">
207
+ <div className="ml-0 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
208
+ <Label className="text-sm font-medium text-yellow-800 dark:text-yellow-200">Reason for excluding this category *</Label>
209
+ <Textarea
210
+ placeholder="Brief reason why this category is not applicable"
211
+ value={localReasons[category.id] || ""}
212
+ onChange={(e) => setLocalReasons((prev) => ({ ...prev, [category.id]: e.target.value }))}
213
+ rows={2}
214
+ className="mt-2 border-yellow-300 dark:border-yellow-700"
215
+ required
216
+ />
217
+ </div>
218
+ </div>
219
+ )}
220
+ </div>
221
+ ))}
222
+ </div>
223
+ </CardContent>
224
+ </Card>
225
+ </div>
226
+ <div className="flex justify-center py-4">
227
+ <div className="flex flex-col items-center w-full">
228
+ {Object.keys(localReasons).some((id) => !localSelection.includes(id) && !(localReasons[id] || "")) && (
229
+ <p className="text-sm text-destructive mb-2">Please provide reasons for all excluded categories.</p>
230
+ )}
231
+ <Button
232
+ onClick={handleContinue}
233
+ disabled={
234
+ localSelection.length === 0 ||
235
+ Object.keys(localReasons).some((id) => !localSelection.includes(id) && !(localReasons[id] || ""))
236
+ }
237
+ size="lg"
238
+ >
239
+ Continue to Evaluation ({localSelection.length} categories selected)
240
+ </Button>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ </TooltipProvider>
245
+ )
246
+ }
components/evaluation-card.tsx ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
5
+ import { Badge } from "@/components/ui/badge"
6
+ import { Button } from "@/components/ui/button"
7
+ import { MoreHorizontal, Eye, Download, Trash2 } from "lucide-react"
8
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
9
+ import { useRouter } from "next/navigation"
10
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
11
+
12
+ export type EvaluationCardData = {
13
+ id: string
14
+ systemName: string
15
+ provider: string
16
+ modality: string // Added modality field
17
+ completedDate: string
18
+ applicableCategories: number
19
+ completedCategories: number
20
+ status: "strong" | "adequate" | "weak" | "insufficient"
21
+ capabilityEval: {
22
+ strong: number
23
+ adequate: number
24
+ weak: number
25
+ insufficient: number
26
+ strongCategories: string[]
27
+ adequateCategories: string[]
28
+ weakCategories: string[]
29
+ insufficientCategories: string[]
30
+ totalApplicable: number
31
+ }
32
+ riskEval: {
33
+ strong: number
34
+ adequate: number
35
+ weak: number
36
+ insufficient: number
37
+ strongCategories: string[]
38
+ adequateCategories: string[]
39
+ weakCategories: string[]
40
+ insufficientCategories: string[]
41
+ totalApplicable: number
42
+ }
43
+ priorityAreas?: string[]
44
+ priorityDetails?: Record<
45
+ string,
46
+ {
47
+ yes: string[]
48
+ negative: { text: string; status: "no" | "na"; reason?: string }[]
49
+ }
50
+ >
51
+ }
52
+
53
+ interface EvaluationCardProps {
54
+ evaluation: EvaluationCardData
55
+ onView: (id: string) => void
56
+ onDelete: (id: string) => void
57
+ }
58
+
59
+ const getCompletenessColor = (score: number) => {
60
+ if (score >= 85) return "bg-emerald-500 text-white"
61
+ if (score >= 70) return "bg-blue-500 text-white"
62
+ if (score >= 55) return "bg-amber-500 text-white"
63
+ return "bg-red-500 text-white"
64
+ }
65
+
66
+ export function EvaluationCard({ evaluation, onView, onDelete }: EvaluationCardProps) {
67
+ const [expandedAreas, setExpandedAreas] = useState<Record<string, boolean>>({})
68
+ const toggleArea = (area: string) => setExpandedAreas((p) => ({ ...p, [area]: !p[area] }))
69
+ const router = useRouter()
70
+ const modalityMap: Record<string, { label: string; emoji?: string; variant?: string }> = {
71
+ "text-to-text": { label: "Text → Text", emoji: "📝" },
72
+ "text-to-image": { label: "Text → Image", emoji: "🖼️" },
73
+ multimodal: { label: "Multimodal", emoji: "🤖" },
74
+ "speech-to-text": { label: "Speech → Text", emoji: "🗣️" },
75
+ "speech-to-speech": { label: "Speech → Speech", emoji: "🔊" },
76
+ "image-to-text": { label: "Image → Text", emoji: "📷" },
77
+ code: { label: "Code", emoji: "💻" },
78
+ }
79
+ const getUniqueCount = (lists: string[][]) => {
80
+ const set = new Set<string>()
81
+ lists.forEach((list) => (list || []).forEach((item) => set.add(item)))
82
+ return set.size
83
+ }
84
+
85
+ const capTotalComputed = getUniqueCount([
86
+ evaluation.capabilityEval.strongCategories,
87
+ evaluation.capabilityEval.adequateCategories,
88
+ evaluation.capabilityEval.weakCategories,
89
+ evaluation.capabilityEval.insufficientCategories,
90
+ ])
91
+
92
+ const riskTotalComputed = getUniqueCount([
93
+ evaluation.riskEval.strongCategories,
94
+ evaluation.riskEval.adequateCategories,
95
+ evaluation.riskEval.weakCategories,
96
+ evaluation.riskEval.insufficientCategories,
97
+ ])
98
+ const calculateCompletenessScore = () => {
99
+ const weights = { strong: 4, adequate: 3, weak: 2, insufficient: 1 }
100
+ const capTotal = capTotalComputed
101
+ const riskTotal = riskTotalComputed
102
+
103
+ if (capTotal === 0 && riskTotal === 0) {
104
+ return "0.0"
105
+ }
106
+
107
+ let capScore = 0
108
+ if (capTotal > 0) {
109
+ capScore =
110
+ ((evaluation.capabilityEval.strong * weights.strong +
111
+ evaluation.capabilityEval.adequate * weights.adequate +
112
+ evaluation.capabilityEval.weak * weights.weak +
113
+ evaluation.capabilityEval.insufficient * weights.insufficient) /
114
+ (capTotal * 4)) *
115
+ 100
116
+ }
117
+
118
+ let riskScore = 0
119
+ if (riskTotal > 0) {
120
+ riskScore =
121
+ ((evaluation.riskEval.strong * weights.strong +
122
+ evaluation.riskEval.adequate * weights.adequate +
123
+ evaluation.riskEval.weak * weights.weak +
124
+ evaluation.riskEval.insufficient * weights.insufficient) /
125
+ (riskTotal * 4)) *
126
+ 100
127
+ }
128
+
129
+ const totalApplicable = capTotal + riskTotal
130
+ const weightedScore = (capScore * capTotal + riskScore * riskTotal) / totalApplicable
131
+
132
+ // Ensure we return a valid number
133
+ return isNaN(weightedScore) ? "0.0" : weightedScore.toFixed(1)
134
+ }
135
+
136
+ const handleViewDetails = () => {
137
+ router.push(`/evaluation/${evaluation.id}`)
138
+ }
139
+
140
+ const completenessScore = Number.parseFloat(calculateCompletenessScore())
141
+
142
+ return (
143
+ <TooltipProvider>
144
+ <Card className="hover:shadow-lg transition-shadow duration-200">
145
+ <CardHeader className="pb-3">
146
+ <div className="flex items-start justify-between">
147
+ <div className="space-y-1 flex-1 min-w-0">
148
+ <CardTitle className="text-lg font-heading truncate">{evaluation.systemName}</CardTitle>
149
+ <p className="text-sm text-muted-foreground truncate">{evaluation.provider}</p>
150
+ {/* Pretty modality badge with emoji */}
151
+ {(() => {
152
+ const info = modalityMap[evaluation.modality] || { label: evaluation.modality }
153
+ return (
154
+ <Badge variant={info.variant as any || "secondary"} className="text-xs px-2 py-1 w-fit flex items-center gap-2">
155
+ {info.emoji ? <span aria-hidden>{info.emoji}</span> : null}
156
+ <span className="whitespace-nowrap">{info.label}</span>
157
+ </Badge>
158
+ )
159
+ })()}
160
+ </div>
161
+ <div className="flex items-center gap-2 flex-shrink-0">
162
+ <DropdownMenu>
163
+ <DropdownMenuTrigger asChild>
164
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
165
+ <MoreHorizontal className="h-4 w-4" />
166
+ </Button>
167
+ </DropdownMenuTrigger>
168
+ <DropdownMenuContent align="end">
169
+ <DropdownMenuItem onClick={handleViewDetails}>
170
+ <Eye className="h-4 w-4 mr-2" />
171
+ View Details
172
+ </DropdownMenuItem>
173
+ <DropdownMenuItem>
174
+ <Download className="h-4 w-4 mr-2" />
175
+ Export Report
176
+ </DropdownMenuItem>
177
+ <DropdownMenuItem onClick={() => onDelete(evaluation.id)} className="text-destructive">
178
+ <Trash2 className="h-4 w-4 mr-2" />
179
+ Delete
180
+ </DropdownMenuItem>
181
+ </DropdownMenuContent>
182
+ </DropdownMenu>
183
+ </div>
184
+ </div>
185
+ </CardHeader>
186
+ <CardContent className="space-y-4">
187
+ <div className="grid grid-cols-2 gap-4">
188
+ <div className="space-y-1">
189
+ <span className="text-sm text-muted-foreground font-medium">Completeness</span>
190
+ <Tooltip>
191
+ <TooltipTrigger asChild>
192
+ <div
193
+ className={`px-2 py-1 rounded text-sm font-semibold cursor-help ${getCompletenessColor(completenessScore)}`}
194
+ >
195
+ {completenessScore}%
196
+ </div>
197
+ </TooltipTrigger>
198
+ <TooltipContent className="max-w-xs">
199
+ <p className="text-xs">
200
+ Weighted average: (Strong×4 + Adequate×3 + Weak×2 + Insufficient×1) ÷ (Total×4) × 100
201
+ <br />
202
+ Capability: {evaluation.capabilityEval.totalApplicable} categories
203
+ <br />
204
+ Risk: {evaluation.riskEval.totalApplicable} categories
205
+ </p>
206
+ </TooltipContent>
207
+ </Tooltip>
208
+ </div>
209
+ <div className="space-y-1">
210
+ <span className="text-sm text-muted-foreground font-medium">Date</span>
211
+ <p className="text-sm font-semibold">{evaluation.completedDate}</p>
212
+ </div>
213
+ </div>
214
+
215
+ <div className="space-y-3">
216
+ <div className="space-y-2">
217
+ <p className="text-sm text-muted-foreground">
218
+ Capability Eval ({evaluation.capabilityEval.totalApplicable} applicable)
219
+ </p>
220
+ <div className="grid grid-cols-4 gap-4 text-xs items-end">
221
+ {/* Vertical histogram bars for Capability */}
222
+ <Tooltip>
223
+ <TooltipTrigger asChild>
224
+ <div className="text-center cursor-help">
225
+ <div className="w-full h-20 flex items-end justify-center mb-2">
226
+ <div
227
+ className="w-full rounded-t-md bg-emerald-500"
228
+ style={{
229
+ height: `${Math.round(
230
+ (evaluation.capabilityEval.strong / Math.max(1, capTotalComputed)) * 100
231
+ )}%`,
232
+ }}
233
+ />
234
+ </div>
235
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Strong</div>
236
+ <p className="font-medium">{evaluation.capabilityEval.strong}</p>
237
+ </div>
238
+ </TooltipTrigger>
239
+ <TooltipContent>
240
+ <p className="text-xs">
241
+ {evaluation.capabilityEval.strongCategories.length > 0
242
+ ? evaluation.capabilityEval.strongCategories.join(", ")
243
+ : "No categories"}
244
+ </p>
245
+ </TooltipContent>
246
+ </Tooltip>
247
+
248
+ <Tooltip>
249
+ <TooltipTrigger asChild>
250
+ <div className="text-center cursor-help">
251
+ <div className="w-full h-20 flex items-end justify-center mb-2">
252
+ <div
253
+ className="w-full rounded-t-md bg-blue-500"
254
+ style={{
255
+ height: `${Math.round(
256
+ (evaluation.capabilityEval.adequate / Math.max(1, capTotalComputed)) * 100
257
+ )}%`,
258
+ }}
259
+ />
260
+ </div>
261
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Adequate</div>
262
+ <p className="font-medium">{evaluation.capabilityEval.adequate}</p>
263
+ </div>
264
+ </TooltipTrigger>
265
+ <TooltipContent>
266
+ <p className="text-xs">
267
+ {evaluation.capabilityEval.adequateCategories.length > 0
268
+ ? evaluation.capabilityEval.adequateCategories.join(", ")
269
+ : "No categories"}
270
+ </p>
271
+ </TooltipContent>
272
+ </Tooltip>
273
+
274
+ <Tooltip>
275
+ <TooltipTrigger asChild>
276
+ <div className="text-center cursor-help">
277
+ <div className="w-full h-20 flex items-end justify-center mb-2">
278
+ <div
279
+ className="w-full rounded-t-md bg-amber-500"
280
+ style={{
281
+ height: `${Math.round(
282
+ (evaluation.capabilityEval.weak / Math.max(1, capTotalComputed)) * 100
283
+ )}%`,
284
+ }}
285
+ />
286
+ </div>
287
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Weak</div>
288
+ <p className="font-medium">{evaluation.capabilityEval.weak}</p>
289
+ </div>
290
+ </TooltipTrigger>
291
+ <TooltipContent>
292
+ <p className="text-xs">
293
+ {evaluation.capabilityEval.weakCategories.length > 0
294
+ ? evaluation.capabilityEval.weakCategories.join(", ")
295
+ : "No categories"}
296
+ </p>
297
+ </TooltipContent>
298
+ </Tooltip>
299
+
300
+ <Tooltip>
301
+ <TooltipTrigger asChild>
302
+ <div className="text-center cursor-help">
303
+ <div className="w-full h-20 flex items-end justify-center mb-2">
304
+ <div
305
+ className="w-full rounded-t-md bg-red-500"
306
+ style={{
307
+ height: `${Math.round(
308
+ (evaluation.capabilityEval.insufficient / Math.max(1, capTotalComputed)) * 100
309
+ )}%`,
310
+ }}
311
+ />
312
+ </div>
313
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Insufficient</div>
314
+ <p className="font-medium">{evaluation.capabilityEval.insufficient}</p>
315
+ </div>
316
+ </TooltipTrigger>
317
+ <TooltipContent>
318
+ <p className="text-xs">
319
+ {evaluation.capabilityEval.insufficientCategories.length > 0
320
+ ? evaluation.capabilityEval.insufficientCategories.join(", ")
321
+ : "No categories"}
322
+ </p>
323
+ </TooltipContent>
324
+ </Tooltip>
325
+ </div>
326
+ </div>
327
+
328
+ <div className="space-y-2">
329
+ <p className="text-sm text-muted-foreground">
330
+ Risk Eval ({evaluation.riskEval.totalApplicable} applicable)
331
+ </p>
332
+ <div className="grid grid-cols-4 gap-4 text-xs items-end">
333
+ {/* Vertical histogram bars for Risk */}
334
+ <Tooltip>
335
+ <TooltipTrigger asChild>
336
+ <div className="text-center cursor-help">
337
+ <div className="w-full h-20 flex items-end justify-center mb-2">
338
+ <div
339
+ className="w-full rounded-t-md bg-emerald-500"
340
+ style={{
341
+ height: `${Math.round(
342
+ (evaluation.riskEval.strong / Math.max(1, riskTotalComputed)) * 100
343
+ )}%`,
344
+ }}
345
+ />
346
+ </div>
347
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Strong</div>
348
+ <p className="font-medium">{evaluation.riskEval.strong}</p>
349
+ </div>
350
+ </TooltipTrigger>
351
+ <TooltipContent>
352
+ <p className="text-xs">
353
+ {evaluation.riskEval.strongCategories.length > 0
354
+ ? evaluation.riskEval.strongCategories.join(", ")
355
+ : "No categories"}
356
+ </p>
357
+ </TooltipContent>
358
+ </Tooltip>
359
+
360
+ <Tooltip>
361
+ <TooltipTrigger asChild>
362
+ <div className="text-center cursor-help">
363
+ <div className="w-full h-20 flex items-end justify-center mb-2">
364
+ <div
365
+ className="w-full rounded-t-md bg-blue-500"
366
+ style={{
367
+ height: `${Math.round(
368
+ (evaluation.riskEval.adequate / Math.max(1, riskTotalComputed)) * 100
369
+ )}%`,
370
+ }}
371
+ />
372
+ </div>
373
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Adequate</div>
374
+ <p className="font-medium">{evaluation.riskEval.adequate}</p>
375
+ </div>
376
+ </TooltipTrigger>
377
+ <TooltipContent>
378
+ <p className="text-xs">
379
+ {evaluation.riskEval.adequateCategories.length > 0
380
+ ? evaluation.riskEval.adequateCategories.join(", ")
381
+ : "No categories"}
382
+ </p>
383
+ </TooltipContent>
384
+ </Tooltip>
385
+
386
+ <Tooltip>
387
+ <TooltipTrigger asChild>
388
+ <div className="text-center cursor-help">
389
+ <div className="w-full h-20 flex items-end justify-center mb-2">
390
+ <div
391
+ className="w-full rounded-t-md bg-amber-500"
392
+ style={{
393
+ height: `${Math.round(
394
+ (evaluation.riskEval.weak / Math.max(1, riskTotalComputed)) * 100
395
+ )}%`,
396
+ }}
397
+ />
398
+ </div>
399
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Weak</div>
400
+ <p className="font-medium">{evaluation.riskEval.weak}</p>
401
+ </div>
402
+ </TooltipTrigger>
403
+ <TooltipContent>
404
+ <p className="text-xs">
405
+ {evaluation.riskEval.weakCategories.length > 0
406
+ ? evaluation.riskEval.weakCategories.join(", ")
407
+ : "No categories"}
408
+ </p>
409
+ </TooltipContent>
410
+ </Tooltip>
411
+
412
+ <Tooltip>
413
+ <TooltipTrigger asChild>
414
+ <div className="text-center cursor-help">
415
+ <div className="w-full h-20 flex items-end justify-center mb-2">
416
+ <div
417
+ className="w-full rounded-t-md bg-red-500"
418
+ style={{
419
+ height: `${Math.round(
420
+ (evaluation.riskEval.insufficient / Math.max(1, riskTotalComputed)) * 100
421
+ )}%`,
422
+ }}
423
+ />
424
+ </div>
425
+ <div className="text-[10px] text-muted-foreground uppercase tracking-wide mb-1">Insufficient</div>
426
+ <p className="font-medium">{evaluation.riskEval.insufficient}</p>
427
+ </div>
428
+ </TooltipTrigger>
429
+ <TooltipContent>
430
+ <p className="text-xs">
431
+ {evaluation.riskEval.insufficientCategories.length > 0
432
+ ? evaluation.riskEval.insufficientCategories.join(", ")
433
+ : "No categories"}
434
+ </p>
435
+ </TooltipContent>
436
+ </Tooltip>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </CardContent>
441
+ {/* priorityAreas/details intentionally removed from summary card — shown on details page */}
442
+ </Card>
443
+ </TooltipProvider>
444
+ )
445
+ }
components/evaluation-form.tsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Badge } from "@/components/ui/badge"
7
+ import { Progress } from "@/components/ui/progress"
8
+ import { CategoryEvaluation } from "@/components/category-evaluation"
9
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
10
+ import type { CategoryScore } from "@/components/ai-evaluation-dashboard"
11
+ import { CheckCircle, Circle, ArrowRight, ArrowLeft } from "lucide-react"
12
+
13
+ interface Category {
14
+ id: string
15
+ name: string
16
+ type: "capability" | "risk"
17
+ description: string
18
+ detailedGuidance: string
19
+ }
20
+
21
+ interface EvaluationFormProps {
22
+ categories: Category[]
23
+ selectedCategories: string[]
24
+ categoryScores: Record<string, CategoryScore>
25
+ onScoreUpdate: (categoryId: string, score: CategoryScore) => void
26
+ onComplete: () => void
27
+ }
28
+
29
+ export function EvaluationForm({
30
+ categories,
31
+ selectedCategories,
32
+ categoryScores,
33
+ onScoreUpdate,
34
+ onComplete,
35
+ }: EvaluationFormProps) {
36
+ const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0)
37
+
38
+ const selectedCategoryObjects = categories.filter((c) => selectedCategories.includes(c.id))
39
+ const currentCategory = selectedCategoryObjects[currentCategoryIndex]
40
+
41
+ const completedCount = Object.keys(categoryScores).length
42
+ const totalCount = selectedCategories.length
43
+ const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
44
+
45
+ const handleNext = () => {
46
+ if (currentCategoryIndex < selectedCategoryObjects.length - 1) {
47
+ setCurrentCategoryIndex((prev) => prev + 1)
48
+ }
49
+ }
50
+
51
+ const handlePrevious = () => {
52
+ if (currentCategoryIndex > 0) {
53
+ setCurrentCategoryIndex((prev) => prev - 1)
54
+ }
55
+ }
56
+
57
+ const handleCategorySelect = (categoryId: string) => {
58
+ const index = selectedCategoryObjects.findIndex((c) => c.id === categoryId)
59
+ if (index !== -1) {
60
+ setCurrentCategoryIndex(index)
61
+ }
62
+ }
63
+
64
+ const isCurrentCategoryCompleted = currentCategory && categoryScores[currentCategory.id]
65
+ const allCompleted = completedCount === totalCount
66
+
67
+ return (
68
+ <div className="space-y-6 h-full min-h-0 flex flex-col">
69
+ {/* Progress Header */}
70
+ <Card>
71
+ <CardHeader>
72
+ <div className="flex items-center justify-between">
73
+ <div>
74
+ <CardTitle className="font-heading">Detailed Evaluation</CardTitle>
75
+ <CardDescription>Complete the evaluation for each selected category</CardDescription>
76
+ </div>
77
+ <div className="text-right">
78
+ <div className="flex items-center gap-2 mb-2">
79
+ <span className="text-sm font-medium">Progress:</span>
80
+ <Badge variant="secondary">
81
+ {completedCount}/{totalCount}
82
+ </Badge>
83
+ </div>
84
+ <Progress value={progress} className="w-32" />
85
+ </div>
86
+ </div>
87
+ </CardHeader>
88
+ </Card>
89
+
90
+ <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 flex-1 min-h-0">
91
+ {/* Category Navigation Sidebar as Tabs */}
92
+ <Card className="lg:col-span-1 h-full">
93
+ <CardHeader>
94
+ <CardTitle className="text-lg">Categories</CardTitle>
95
+ </CardHeader>
96
+ <CardContent className="h-full min-h-0 flex flex-col">
97
+ <Tabs defaultValue={selectedCategoryObjects[0]?.id || ""} orientation="vertical" value={selectedCategoryObjects[currentCategoryIndex]?.id || ""} onValueChange={(val) => handleCategorySelect(val)}>
98
+ <TabsList className="flex-col w-full flex-1 min-h-0 overflow-auto">
99
+ {selectedCategoryObjects.map((category, index) => {
100
+ const isCompleted = categoryScores[category.id]
101
+ return (
102
+ <TabsTrigger
103
+ key={category.id}
104
+ value={category.id}
105
+ className={`w-full text-left p-3 rounded-md ${
106
+ category.type === "capability"
107
+ ? "bg-purple-50 data-[state=active]:bg-purple-200 data-[state=active]:border-l-4 data-[state=active]:border-l-purple-600"
108
+ : "bg-red-50 data-[state=active]:bg-red-200 data-[state=active]:border-l-4 data-[state=active]:border-l-red-600"
109
+ }`}
110
+ >
111
+ <div className="flex items-start gap-3 w-full">
112
+ <div className="flex items-center">
113
+ {isCompleted ? <CheckCircle className="h-4 w-4 text-green-600" /> : <Circle className="h-4 w-4 text-muted-foreground" />}
114
+ </div>
115
+
116
+ <div className="flex-1 min-w-0">
117
+ <div className="flex flex-col">
118
+ <span className="text-sm font-medium leading-snug truncate">{category.name}</span>
119
+ <span
120
+ className={`inline-block text-xs font-medium mt-1 px-2 py-0.5 rounded-full w-max ${
121
+ category.type === "capability" ? "bg-purple-100 text-purple-800" : "bg-red-100 text-red-800"
122
+ }`}
123
+ >
124
+ {category.type}
125
+ </span>
126
+ </div>
127
+ </div>
128
+
129
+ {isCompleted && (
130
+ <div className="flex items-center">
131
+ <span className="text-xs text-muted-foreground">
132
+ {categoryScores[category.id].totalScore}/{categoryScores[category.id].totalApplicable || categoryScores[category.id].totalQuestions || 0}
133
+ </span>
134
+ </div>
135
+ )}
136
+ </div>
137
+ </TabsTrigger>
138
+ )
139
+ })}
140
+ </TabsList>
141
+ </Tabs>
142
+ </CardContent>
143
+ </Card>
144
+
145
+ {/* Current Category Evaluation */}
146
+ <div className="lg:col-span-3">
147
+ {currentCategory && (
148
+ <CategoryEvaluation
149
+ key={currentCategory.id}
150
+ category={currentCategory}
151
+ score={categoryScores[currentCategory.id]}
152
+ onScoreUpdate={(score) => onScoreUpdate(currentCategory.id, score)}
153
+ />
154
+ )}
155
+
156
+ {/* Navigation Controls */}
157
+ <Card className="mt-6">
158
+ <CardContent className="pt-6">
159
+ <div className="flex items-center justify-between">
160
+ <Button variant="outline" onClick={handlePrevious} disabled={currentCategoryIndex === 0}>
161
+ <ArrowLeft className="h-4 w-4 mr-2" />
162
+ Previous
163
+ </Button>
164
+
165
+ <div className="text-center">
166
+ <p className="text-sm text-muted-foreground">
167
+ Category {currentCategoryIndex + 1} of {totalCount}
168
+ </p>
169
+ {currentCategory && <p className="font-medium">{currentCategory.name}</p>}
170
+ </div>
171
+
172
+ {currentCategoryIndex < selectedCategoryObjects.length - 1 ? (
173
+ <Button onClick={handleNext} disabled={!isCurrentCategoryCompleted}>
174
+ Next
175
+ <ArrowRight className="h-4 w-4 ml-2" />
176
+ </Button>
177
+ ) : (
178
+ <Button onClick={onComplete} disabled={!allCompleted} className="bg-green-600 hover:bg-green-700">
179
+ View Results
180
+ <ArrowRight className="h-4 w-4 ml-2" />
181
+ </Button>
182
+ )}
183
+ </div>
184
+ </CardContent>
185
+ </Card>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ )
190
+ }
components/results-dashboard.tsx ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { Button } from "@/components/ui/button"
6
+ import type { SystemInfo, CategoryScore } from "@/components/ai-evaluation-dashboard"
7
+ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from "recharts"
8
+ import { Download, AlertTriangle, CheckCircle, AlertCircle, XCircle } from "lucide-react"
9
+
10
+ interface Category {
11
+ id: string
12
+ name: string
13
+ type: "capability" | "risk"
14
+ }
15
+
16
+ interface ResultsDashboardProps {
17
+ systemInfo: SystemInfo | null
18
+ categories: Category[]
19
+ selectedCategories: string[]
20
+ categoryScores: Record<string, CategoryScore>
21
+ excludedCategoryReasons?: Record<string, string>
22
+ }
23
+
24
+ const STATUS_COLORS = {
25
+ strong: "#22c55e",
26
+ adequate: "#3b82f6",
27
+ weak: "#f59e0b",
28
+ insufficient: "#ef4444",
29
+ }
30
+
31
+ const STATUS_ICONS = {
32
+ strong: CheckCircle,
33
+ adequate: CheckCircle,
34
+ weak: AlertCircle,
35
+ insufficient: XCircle,
36
+ }
37
+
38
+ export function ResultsDashboard({
39
+ systemInfo,
40
+ categories,
41
+ selectedCategories,
42
+ categoryScores,
43
+ excludedCategoryReasons,
44
+ }: ResultsDashboardProps) {
45
+ const safeCategories = categories || []
46
+ const safeSelectedCategories = selectedCategories || []
47
+ const safeCategoryScores = categoryScores || {}
48
+
49
+ console.log("[v0] ResultsDashboard rendering with:", {
50
+ systemInfo,
51
+ categoriesCount: safeCategories.length,
52
+ selectedCount: safeSelectedCategories.length,
53
+ scoresCount: Object.keys(safeCategoryScores).length,
54
+ scores: safeCategoryScores,
55
+ })
56
+
57
+ const selectedCategoryObjects = safeCategories.filter((c) => safeSelectedCategories.includes(c.id))
58
+
59
+ const getStatusCounts = () => {
60
+ const scores = Object.values(safeCategoryScores)
61
+ return {
62
+ strong: scores.filter((s) => s.status === "strong").length,
63
+ adequate: scores.filter((s) => s.status === "adequate").length,
64
+ weak: scores.filter((s) => s.status === "weak").length,
65
+ insufficient: scores.filter((s) => s.status === "insufficient").length,
66
+ }
67
+ }
68
+
69
+ const getCapabilityRiskBreakdown = () => {
70
+ const capability = selectedCategoryObjects.filter((c) => c.type === "capability")
71
+ const risk = selectedCategoryObjects.filter((c) => c.type === "risk")
72
+
73
+ const capabilityScores = capability.map((c) => safeCategoryScores[c.id]).filter(Boolean)
74
+ const riskScores = risk.map((c) => safeCategoryScores[c.id]).filter(Boolean)
75
+
76
+ const avgCapability =
77
+ capabilityScores.length > 0
78
+ ? capabilityScores.reduce((sum, s) => sum + (s.totalScore || 0), 0) / capabilityScores.length
79
+ : 0
80
+ const avgRisk =
81
+ riskScores.length > 0 ? riskScores.reduce((sum, s) => sum + (s.totalScore || 0), 0) / riskScores.length : 0
82
+
83
+ const safeCapability = isNaN(avgCapability) || !isFinite(avgCapability) ? 0 : avgCapability
84
+ const safeRisk = isNaN(avgRisk) || !isFinite(avgRisk) ? 0 : avgRisk
85
+
86
+ console.log("[v0] Capability/Risk breakdown:", {
87
+ capabilityScores: capabilityScores.length,
88
+ riskScores: riskScores.length,
89
+ avgCapability,
90
+ avgRisk,
91
+ safeCapability,
92
+ safeRisk,
93
+ })
94
+
95
+ return {
96
+ capability: safeCapability,
97
+ risk: safeRisk,
98
+ }
99
+ }
100
+
101
+ const getChartData = () => {
102
+ return selectedCategoryObjects
103
+ .map((category) => {
104
+ const score = safeCategoryScores[category.id]
105
+ return {
106
+ name: category.name.length > 20 ? category.name.substring(0, 20) + "..." : category.name,
107
+ fullName: category.name,
108
+ benchmarkScore: score?.benchmarkScore || 0,
109
+ processScore: score?.processScore || 0,
110
+ totalScore: score?.totalScore || 0,
111
+ type: category.type,
112
+ }
113
+ })
114
+ .sort((a, b) => b.totalScore - a.totalScore)
115
+ }
116
+
117
+ const getPieData = () => {
118
+ const counts = getStatusCounts()
119
+ return [
120
+ { name: "Strong (12-15)", value: counts.strong, color: STATUS_COLORS.strong },
121
+ { name: "Adequate (8-11)", value: counts.adequate, color: STATUS_COLORS.adequate },
122
+ { name: "Weak (4-7)", value: counts.weak, color: STATUS_COLORS.weak },
123
+ { name: "Insufficient (0-3)", value: counts.insufficient, color: STATUS_COLORS.insufficient },
124
+ ].filter((item) => item.value > 0)
125
+ }
126
+
127
+ const statusCounts = getStatusCounts()
128
+ const breakdown = getCapabilityRiskBreakdown()
129
+ const chartData = getChartData()
130
+ const pieData = getPieData()
131
+
132
+ const totalEvaluated = Object.keys(safeCategoryScores).length
133
+ const overallAverage =
134
+ totalEvaluated > 0
135
+ ? Object.values(safeCategoryScores).reduce((sum, s) => sum + (s.totalScore || 0), 0) / totalEvaluated
136
+ : 0
137
+
138
+ const safeOverallAverage = isNaN(overallAverage) || !isFinite(overallAverage) ? 0 : overallAverage
139
+
140
+ console.log("[v0] Overall average calculation:", {
141
+ totalEvaluated,
142
+ overallAverage,
143
+ safeOverallAverage,
144
+ })
145
+
146
+ const safeToFixed = (value: number, digits = 1): string => {
147
+ if (isNaN(value) || !isFinite(value)) {
148
+ console.log("[v0] Warning: Invalid value for toFixed:", value)
149
+ return "0.0"
150
+ }
151
+ return value.toFixed(digits)
152
+ }
153
+
154
+ const exportResults = () => {
155
+ const results = {
156
+ systemInfo,
157
+ evaluationDate: new Date().toISOString(),
158
+ summary: {
159
+ totalCategories: safeSelectedCategories.length,
160
+ evaluatedCategories: totalEvaluated,
161
+ overallAverage: safeOverallAverage.toFixed(1),
162
+ statusBreakdown: statusCounts,
163
+ },
164
+ categoryResults: safeCategories.map((category) => ({
165
+ ...category,
166
+ score: safeCategoryScores[category.id] || null,
167
+ excludedReason: !safeSelectedCategories.includes(category.id)
168
+ ? excludedCategoryReasons?.[category.id] || null
169
+ : null,
170
+ })),
171
+ }
172
+
173
+ const blob = new Blob([JSON.stringify(results, null, 2)], { type: "application/json" })
174
+ const url = URL.createObjectURL(blob)
175
+ const a = document.createElement("a")
176
+ a.href = url
177
+ a.download = `ai-evaluation-${systemInfo?.name || "system"}-${new Date().toISOString().split("T")[0]}.json`
178
+ a.click()
179
+ URL.revokeObjectURL(url)
180
+ }
181
+
182
+ return (
183
+ <div className="space-y-6">
184
+ {/* System Overview */}
185
+ <Card>
186
+ <CardHeader>
187
+ <div className="flex items-center justify-between">
188
+ <div>
189
+ <CardTitle className="font-heading">Evaluation Results</CardTitle>
190
+ <CardDescription>Comprehensive assessment results for {systemInfo?.name}</CardDescription>
191
+ </div>
192
+ <Button onClick={exportResults} variant="outline">
193
+ <Download className="h-4 w-4 mr-2" />
194
+ Export Results
195
+ </Button>
196
+ </div>
197
+ </CardHeader>
198
+ <CardContent>
199
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
200
+ <div className="text-center">
201
+ <div className="text-2xl font-bold text-foreground">{totalEvaluated}</div>
202
+ <div className="text-sm text-muted-foreground">Categories Evaluated</div>
203
+ </div>
204
+ <div className="text-center">
205
+ <div className="text-2xl font-bold text-foreground">{safeToFixed(safeOverallAverage)}/15</div>
206
+ <div className="text-sm text-muted-foreground">Overall Average</div>
207
+ </div>
208
+ <div className="text-center">
209
+ <div className="text-2xl font-bold text-foreground">{safeToFixed(breakdown.capability)}/15</div>
210
+ <div className="text-sm text-muted-foreground">Capability Average</div>
211
+ </div>
212
+ <div className="text-center">
213
+ <div className="text-2xl font-bold text-foreground">{safeToFixed(breakdown.risk)}/15</div>
214
+ <div className="text-sm text-muted-foreground">Risk Average</div>
215
+ </div>
216
+ </div>
217
+ </CardContent>
218
+ </Card>
219
+
220
+ {/* Status Overview */}
221
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
222
+ {Object.entries(statusCounts).map(([status, count]) => {
223
+ const key = (status as string) as keyof typeof STATUS_ICONS
224
+ const Icon = STATUS_ICONS[key] ?? AlertTriangle
225
+ return (
226
+ <Card key={status}>
227
+ <CardContent className="pt-6">
228
+ <div className="flex items-center justify-between">
229
+ <div>
230
+ <div className="text-2xl font-bold">{count}</div>
231
+ <div className="text-sm text-muted-foreground capitalize">{status}</div>
232
+ </div>
233
+ <Icon className="h-8 w-8" style={{ color: STATUS_COLORS[key] ?? STATUS_COLORS.insufficient }} />
234
+ </div>
235
+ </CardContent>
236
+ </Card>
237
+ )
238
+ })}
239
+ </div>
240
+
241
+ {/* Charts */}
242
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
243
+ {/* Score Distribution */}
244
+ <Card>
245
+ <CardHeader>
246
+ <CardTitle>Score Distribution</CardTitle>
247
+ <CardDescription>Categories by evaluation status</CardDescription>
248
+ </CardHeader>
249
+ <CardContent>
250
+ <ResponsiveContainer width="100%" height={300}>
251
+ <PieChart>
252
+ <Pie
253
+ data={pieData}
254
+ cx="50%"
255
+ cy="50%"
256
+ outerRadius={80}
257
+ dataKey="value"
258
+ label={({ name, value }) => `${name}: ${value}`}
259
+ >
260
+ {pieData.map((entry, index) => (
261
+ <Cell key={`cell-${index}`} fill={entry.color} />
262
+ ))}
263
+ </Pie>
264
+ <Tooltip />
265
+ </PieChart>
266
+ </ResponsiveContainer>
267
+ </CardContent>
268
+ </Card>
269
+
270
+ {/* Category Scores */}
271
+ <Card>
272
+ <CardHeader>
273
+ <CardTitle>Category Scores</CardTitle>
274
+ <CardDescription>Benchmark vs Process scores by category</CardDescription>
275
+ </CardHeader>
276
+ <CardContent>
277
+ <ResponsiveContainer width="100%" height={300}>
278
+ <BarChart data={chartData.slice(0, 8)} layout="horizontal">
279
+ <CartesianGrid strokeDasharray="3 3" />
280
+ <XAxis type="number" domain={[0, 15]} />
281
+ <YAxis dataKey="name" type="category" width={100} />
282
+ <Tooltip labelFormatter={(label) => chartData.find((d) => d.name === label)?.fullName || label} />
283
+ <Bar dataKey="benchmarkScore" stackId="a" fill="#3b82f6" name="Benchmark" />
284
+ <Bar dataKey="processScore" stackId="a" fill="#8b5cf6" name="Process" />
285
+ </BarChart>
286
+ </ResponsiveContainer>
287
+ </CardContent>
288
+ </Card>
289
+ </div>
290
+
291
+ {/* Detailed Results */}
292
+ <Card>
293
+ <CardHeader>
294
+ <CardTitle>Detailed Category Results</CardTitle>
295
+ <CardDescription>Complete breakdown of all evaluated categories</CardDescription>
296
+ </CardHeader>
297
+ <CardContent>
298
+ <div className="space-y-4">
299
+ {selectedCategoryObjects.map((category) => {
300
+ const score = safeCategoryScores[category.id]
301
+ if (!score) return null
302
+
303
+ const key = (score.status as string) as keyof typeof STATUS_ICONS
304
+ const Icon = (STATUS_ICONS as any)[key] ?? AlertTriangle
305
+ const color = (STATUS_COLORS as any)[key] ?? STATUS_COLORS.insufficient
306
+
307
+ return (
308
+ <div key={category.id} className="flex items-center justify-between p-4 border rounded-lg">
309
+ <div className="flex items-center gap-3">
310
+ <Icon className="h-5 w-5" style={{ color }} />
311
+ <div>
312
+ <div className="font-medium">{category.name}</div>
313
+ <div className="flex items-center gap-2 mt-1">
314
+ <Badge
315
+ variant={category.type === "capability" ? "secondary" : "destructive"}
316
+ className="text-xs"
317
+ >
318
+ {category.type}
319
+ </Badge>
320
+ <span className="text-sm text-muted-foreground">
321
+ Benchmark: {score.benchmarkScore}/7 | Process: {score.processScore}/8
322
+ </span>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ <div className="text-right">
327
+ <div className="text-lg font-bold">{score.totalScore}/15</div>
328
+ <Badge
329
+ variant={
330
+ score.status === "strong"
331
+ ? "default"
332
+ : score.status === "adequate"
333
+ ? "secondary"
334
+ : score.status === "weak"
335
+ ? "outline"
336
+ : "destructive"
337
+ }
338
+ className="text-xs"
339
+ >
340
+ {score.status}
341
+ </Badge>
342
+ </div>
343
+ </div>
344
+ )
345
+ })}
346
+ </div>
347
+ </CardContent>
348
+ </Card>
349
+
350
+ {/* Excluded Categories with Reasons */}
351
+ <Card>
352
+ <CardHeader>
353
+ <CardTitle>Excluded Categories & Reasons</CardTitle>
354
+ <CardDescription>Categories you marked as not applicable and the rationale provided</CardDescription>
355
+ </CardHeader>
356
+ <CardContent>
357
+ <div className="space-y-3">
358
+ {safeCategories
359
+ .filter((c) => !safeSelectedCategories.includes(c.id))
360
+ .map((c) => (
361
+ <div key={c.id} className="p-3 border rounded-md">
362
+ <div className="font-medium">{c.name}</div>
363
+ <div className="text-sm text-muted-foreground mt-1">
364
+ {excludedCategoryReasons?.[c.id] || "No reason provided"}
365
+ </div>
366
+ </div>
367
+ ))}
368
+ </div>
369
+ </CardContent>
370
+ </Card>
371
+
372
+ {/* Priority Actions */}
373
+ <Card>
374
+ <CardHeader>
375
+ <CardTitle className="flex items-center gap-2">
376
+ <AlertTriangle className="h-5 w-5 text-amber-500" />
377
+ Priority Action Areas
378
+ </CardTitle>
379
+ <CardDescription>Categories requiring immediate attention</CardDescription>
380
+ </CardHeader>
381
+ <CardContent>
382
+ <div className="space-y-4">
383
+ {statusCounts.insufficient > 0 && (
384
+ <div>
385
+ <h4 className="font-medium text-destructive mb-2">
386
+ Critical - Insufficient Categories ({statusCounts.insufficient})
387
+ </h4>
388
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
389
+ {selectedCategoryObjects
390
+ .filter((c) => safeCategoryScores[c.id]?.status === "insufficient")
391
+ .map((category) => (
392
+ <Badge key={category.id} variant="destructive" className="justify-start">
393
+ {category.name}
394
+ </Badge>
395
+ ))}
396
+ </div>
397
+ </div>
398
+ )}
399
+
400
+ {statusCounts.weak > 0 && (
401
+ <div>
402
+ <h4 className="font-medium text-amber-600 mb-2">
403
+ High Priority - Weak Categories ({statusCounts.weak})
404
+ </h4>
405
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
406
+ {selectedCategoryObjects
407
+ .filter((c) => safeCategoryScores[c.id]?.status === "weak")
408
+ .map((category) => (
409
+ <Badge key={category.id} variant="outline" className="justify-start">
410
+ {category.name}
411
+ </Badge>
412
+ ))}
413
+ </div>
414
+ </div>
415
+ )}
416
+
417
+ {statusCounts.insufficient === 0 && statusCounts.weak === 0 && (
418
+ <div className="text-center py-8 text-muted-foreground">
419
+ <CheckCircle className="h-12 w-12 mx-auto mb-2 text-green-600" />
420
+ <p>No critical priority areas identified. All categories scored adequate or above.</p>
421
+ </div>
422
+ )}
423
+ </div>
424
+ </CardContent>
425
+ </Card>
426
+ </div>
427
+ )
428
+ }
components/system-info-form.tsx ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import type React from "react"
4
+
5
+ import { useState } from "react"
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Input } from "@/components/ui/input"
9
+ import { Label } from "@/components/ui/label"
10
+ import { Checkbox } from "@/components/ui/checkbox"
11
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
12
+ import type { SystemInfo } from "@/components/ai-evaluation-dashboard"
13
+
14
+ interface SystemInfoFormProps {
15
+ onSubmit: (data: SystemInfo) => void
16
+ initialData: SystemInfo | null
17
+ }
18
+
19
+ const SYSTEM_TYPES = [
20
+ "Text-to-Text (e.g., chatbots, language models)",
21
+ "Text-to-Image (e.g., image generation)",
22
+ "Image-to-Text (e.g., image captioning, OCR)",
23
+ "Image-to-Image (e.g., image editing, style transfer)",
24
+ "Audio/Speech (e.g., speech recognition, text-to-speech)",
25
+ "Video (e.g., video generation, analysis)",
26
+ "Multimodal",
27
+ "Robotic/Embodied AI",
28
+ "Other",
29
+ ]
30
+
31
+ const DEPLOYMENT_CONTEXTS = [
32
+ "Research/Academic",
33
+ "Internal/Enterprise Use",
34
+ "Public/Consumer-Facing",
35
+ "High-Risk Applications",
36
+ "Other",
37
+ ]
38
+
39
+ export function SystemInfoForm({ onSubmit, initialData }: SystemInfoFormProps) {
40
+ const [formData, setFormData] = useState<SystemInfo>({
41
+ name: initialData?.name || "",
42
+ url: initialData?.url || "",
43
+ provider: initialData?.provider || "",
44
+ systemTypes: initialData?.systemTypes || [],
45
+ deploymentContexts: initialData?.deploymentContexts || [],
46
+ modality: initialData?.modality || "text",
47
+ modelTag: (initialData as any)?.modelTag || "",
48
+ knowledgeCutoff: (initialData as any)?.knowledgeCutoff || "",
49
+ modelType: (initialData as any)?.modelType || "na",
50
+ inputModalities: (initialData as any)?.inputModalities || [],
51
+ outputModalities: (initialData as any)?.outputModalities || [],
52
+ })
53
+
54
+ const handleSubmit = (e: React.FormEvent) => {
55
+ e.preventDefault()
56
+ onSubmit(formData)
57
+ }
58
+
59
+ const handleSystemTypeChange = (type: string, checked: boolean) => {
60
+ setFormData((prev: SystemInfo) => ({
61
+ ...prev,
62
+ systemTypes: checked ? [...prev.systemTypes, type] : prev.systemTypes.filter((t) => t !== type),
63
+ }))
64
+ }
65
+
66
+ const handleDeploymentContextChange = (context: string, checked: boolean) => {
67
+ setFormData((prev: SystemInfo) => ({
68
+ ...prev,
69
+ deploymentContexts: checked
70
+ ? [...prev.deploymentContexts, context]
71
+ : prev.deploymentContexts.filter((c) => c !== context),
72
+ }))
73
+ }
74
+
75
+ return (
76
+ <Card>
77
+ <CardHeader>
78
+ <CardTitle className="font-heading">System Information</CardTitle>
79
+ <CardDescription>Provide basic information about the AI system you want to evaluate</CardDescription>
80
+ </CardHeader>
81
+ <CardContent>
82
+ <form onSubmit={handleSubmit} className="space-y-6">
83
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
84
+ <div className="space-y-2">
85
+ <Label htmlFor="name">AI System Name *</Label>
86
+ <Input
87
+ id="name"
88
+ value={formData.name}
89
+ onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
90
+ placeholder="e.g., GPT-4, Claude, Custom Model"
91
+ required
92
+ />
93
+ </div>
94
+ <div className="space-y-2">
95
+ <Label htmlFor="url">AI System URL</Label>
96
+ <Input
97
+ id="url"
98
+ type="url"
99
+ value={formData.url}
100
+ onChange={(e) => setFormData((prev) => ({ ...prev, url: e.target.value }))}
101
+ placeholder="https://example.com"
102
+ />
103
+ </div>
104
+ </div>
105
+
106
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
107
+ <div className="space-y-2">
108
+ <Label htmlFor="modelTag">Model Tag / Version</Label>
109
+ <Input
110
+ id="modelTag"
111
+ value={formData.modelTag}
112
+ onChange={(e) => setFormData((prev) => ({ ...prev, modelTag: e.target.value }))}
113
+ placeholder="e.g., gpt-4-1-2025-04-14"
114
+ />
115
+ </div>
116
+ <div className="space-y-2">
117
+ <Label htmlFor="knowledgeCutoff">Knowledge Cutoff Date</Label>
118
+ <Input
119
+ id="knowledgeCutoff"
120
+ value={formData.knowledgeCutoff}
121
+ onChange={(e) => setFormData((prev) => ({ ...prev, knowledgeCutoff: e.target.value }))}
122
+ placeholder="YYYY-MM-DD"
123
+ />
124
+ </div>
125
+ </div>
126
+
127
+ <div className="space-y-2">
128
+ <Label htmlFor="provider">Provider/Organization *</Label>
129
+ <Input
130
+ id="provider"
131
+ value={formData.provider}
132
+ onChange={(e) => setFormData((prev) => ({ ...prev, provider: e.target.value }))}
133
+ placeholder="e.g., OpenAI, Anthropic, Internal Team"
134
+ required
135
+ />
136
+ </div>
137
+
138
+ <div className="space-y-4">
139
+ <div>
140
+ <Label className="text-base font-medium">Model Type</Label>
141
+ <div className="mt-3">
142
+ <RadioGroup value={formData.modelType} onValueChange={(val) => setFormData((prev) => ({ ...prev, modelType: val as any }))}>
143
+ <div className="flex items-center gap-4">
144
+ <div className="flex items-center gap-2">
145
+ <RadioGroupItem value="foundational" id="mt-foundational" />
146
+ <Label htmlFor="mt-foundational" className="text-sm">Foundational Model</Label>
147
+ </div>
148
+ <div className="flex items-center gap-2">
149
+ <RadioGroupItem value="fine-tuned" id="mt-finetuned" />
150
+ <Label htmlFor="mt-finetuned" className="text-sm">Fine-tuned Model</Label>
151
+ </div>
152
+ <div className="flex items-center gap-2">
153
+ <RadioGroupItem value="na" id="mt-na" />
154
+ <Label htmlFor="mt-na" className="text-sm">Doesn't apply</Label>
155
+ </div>
156
+ </div>
157
+ </RadioGroup>
158
+ </div>
159
+ </div>
160
+
161
+ <div>
162
+ <Label className="text-base font-medium">Input modalities (select all that apply) *</Label>
163
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
164
+ {['Text','Image','Audio','Video','Tabular','Robotics/Action','Other'].map((m) => (
165
+ <div key={m} className="flex items-center gap-2">
166
+ <Checkbox
167
+ id={`in-${m}`}
168
+ checked={(formData.inputModalities || []).includes(m)}
169
+ onCheckedChange={(checked) =>
170
+ setFormData((prev) => ({
171
+ ...prev,
172
+ inputModalities: checked
173
+ ? [...(prev.inputModalities || []), m]
174
+ : (prev.inputModalities || []).filter((x) => x !== m),
175
+ }))
176
+ }
177
+ />
178
+ <Label htmlFor={`in-${m}`} className="text-sm">
179
+ {m}
180
+ </Label>
181
+ </div>
182
+ ))}
183
+ </div>
184
+ </div>
185
+
186
+ <div>
187
+ <Label className="text-base font-medium">Output modalities (select all that apply) *</Label>
188
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
189
+ {['Text','Image','Audio','Video','Tabular','Robotics/Action','Other'].map((m) => (
190
+ <div key={m} className="flex items-center gap-2">
191
+ <Checkbox
192
+ id={`out-${m}`}
193
+ checked={(formData.outputModalities || []).includes(m)}
194
+ onCheckedChange={(checked) =>
195
+ setFormData((prev) => ({
196
+ ...prev,
197
+ outputModalities: checked
198
+ ? [...(prev.outputModalities || []), m]
199
+ : (prev.outputModalities || []).filter((x) => x !== m),
200
+ }))
201
+ }
202
+ />
203
+ <Label htmlFor={`out-${m}`} className="text-sm">
204
+ {m}
205
+ </Label>
206
+ </div>
207
+ ))}
208
+ </div>
209
+ </div>
210
+
211
+ <div>
212
+ <Label className="text-base font-medium">Deployment Context (select all that apply) *</Label>
213
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
214
+ {DEPLOYMENT_CONTEXTS.map((context) => (
215
+ <div key={context} className="flex items-center space-x-2">
216
+ <Checkbox
217
+ id={`context-${context}`}
218
+ checked={formData.deploymentContexts.includes(context)}
219
+ onCheckedChange={(checked) => handleDeploymentContextChange(context, checked as boolean)}
220
+ />
221
+ <Label htmlFor={`context-${context}`} className="text-sm font-normal">
222
+ {context}
223
+ </Label>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ </div>
228
+ </div>
229
+
230
+
231
+
232
+ <Button
233
+ type="submit"
234
+ className="w-full"
235
+ disabled={
236
+ !formData.name ||
237
+ !formData.provider ||
238
+ (formData.inputModalities || []).length === 0 ||
239
+ (formData.outputModalities || []).length === 0 ||
240
+ formData.deploymentContexts.length === 0
241
+ }
242
+ >
243
+ Continue to Category Selection
244
+ </Button>
245
+ </form>
246
+ </CardContent>
247
+ </Card>
248
+ )
249
+ }
components/theme-provider.tsx ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ "use client"
2
+ import { ThemeProvider as NextThemesProvider } from "next-themes"
3
+ import type { ThemeProviderProps } from "next-themes"
4
+
5
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
6
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
7
+ }
components/ui/accordion.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDownIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("border-b last:border-b-0", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ )
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ )
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { buttonVariants } from "@/components/ui/button"
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18
+ return (
19
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ )
21
+ }
22
+
23
+ function AlertDialogPortal({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26
+ return (
27
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28
+ )
29
+ }
30
+
31
+ function AlertDialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35
+ return (
36
+ <AlertDialogPrimitive.Overlay
37
+ data-slot="alert-dialog-overlay"
38
+ className={cn(
39
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function AlertDialogContent({
48
+ className,
49
+ ...props
50
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
51
+ return (
52
+ <AlertDialogPortal>
53
+ <AlertDialogOverlay />
54
+ <AlertDialogPrimitive.Content
55
+ data-slot="alert-dialog-content"
56
+ className={cn(
57
+ "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
58
+ className
59
+ )}
60
+ {...props}
61
+ />
62
+ </AlertDialogPortal>
63
+ )
64
+ }
65
+
66
+ function AlertDialogHeader({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div
72
+ data-slot="alert-dialog-header"
73
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ function AlertDialogFooter({
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="alert-dialog-footer"
86
+ className={cn(
87
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function AlertDialogTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
99
+ return (
100
+ <AlertDialogPrimitive.Title
101
+ data-slot="alert-dialog-title"
102
+ className={cn("text-lg font-semibold", className)}
103
+ {...props}
104
+ />
105
+ )
106
+ }
107
+
108
+ function AlertDialogDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
112
+ return (
113
+ <AlertDialogPrimitive.Description
114
+ data-slot="alert-dialog-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props}
117
+ />
118
+ )
119
+ }
120
+
121
+ function AlertDialogAction({
122
+ className,
123
+ ...props
124
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
125
+ return (
126
+ <AlertDialogPrimitive.Action
127
+ className={cn(buttonVariants(), className)}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function AlertDialogCancel({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
137
+ return (
138
+ <AlertDialogPrimitive.Cancel
139
+ className={cn(buttonVariants({ variant: "outline" }), className)}
140
+ {...props}
141
+ />
142
+ )
143
+ }
144
+
145
+ export {
146
+ AlertDialog,
147
+ AlertDialogPortal,
148
+ AlertDialogOverlay,
149
+ AlertDialogTrigger,
150
+ AlertDialogContent,
151
+ AlertDialogHeader,
152
+ AlertDialogFooter,
153
+ AlertDialogTitle,
154
+ AlertDialogDescription,
155
+ AlertDialogAction,
156
+ AlertDialogCancel,
157
+ }
components/ui/alert.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }
components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4
+
5
+ function AspectRatio({
6
+ ...props
7
+ }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
8
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
9
+ }
10
+
11
+ export { AspectRatio }
components/ui/avatar.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback }
components/ui/badge.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }
components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
9
+ }
10
+
11
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12
+ return (
13
+ <ol
14
+ data-slot="breadcrumb-list"
15
+ className={cn(
16
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25
+ return (
26
+ <li
27
+ data-slot="breadcrumb-item"
28
+ className={cn("inline-flex items-center gap-1.5", className)}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function BreadcrumbLink({
35
+ asChild,
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<"a"> & {
39
+ asChild?: boolean
40
+ }) {
41
+ const Comp = asChild ? Slot : "a"
42
+
43
+ return (
44
+ <Comp
45
+ data-slot="breadcrumb-link"
46
+ className={cn("hover:text-foreground transition-colors", className)}
47
+ {...props}
48
+ />
49
+ )
50
+ }
51
+
52
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53
+ return (
54
+ <span
55
+ data-slot="breadcrumb-page"
56
+ role="link"
57
+ aria-disabled="true"
58
+ aria-current="page"
59
+ className={cn("text-foreground font-normal", className)}
60
+ {...props}
61
+ />
62
+ )
63
+ }
64
+
65
+ function BreadcrumbSeparator({
66
+ children,
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"li">) {
70
+ return (
71
+ <li
72
+ data-slot="breadcrumb-separator"
73
+ role="presentation"
74
+ aria-hidden="true"
75
+ className={cn("[&>svg]:size-3.5", className)}
76
+ {...props}
77
+ >
78
+ {children ?? <ChevronRight />}
79
+ </li>
80
+ )
81
+ }
82
+
83
+ function BreadcrumbEllipsis({
84
+ className,
85
+ ...props
86
+ }: React.ComponentProps<"span">) {
87
+ return (
88
+ <span
89
+ data-slot="breadcrumb-ellipsis"
90
+ role="presentation"
91
+ aria-hidden="true"
92
+ className={cn("flex size-9 items-center justify-center", className)}
93
+ {...props}
94
+ >
95
+ <MoreHorizontal className="size-4" />
96
+ <span className="sr-only">More</span>
97
+ </span>
98
+ )
99
+ }
100
+
101
+ export {
102
+ Breadcrumb,
103
+ BreadcrumbList,
104
+ BreadcrumbItem,
105
+ BreadcrumbLink,
106
+ BreadcrumbPage,
107
+ BreadcrumbSeparator,
108
+ BreadcrumbEllipsis,
109
+ }
components/ui/button.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
+ outline:
17
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
+ ghost:
21
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
26
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ }
36
+ )
37
+
38
+ function Button({
39
+ className,
40
+ variant,
41
+ size,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"button"> &
45
+ VariantProps<typeof buttonVariants> & {
46
+ asChild?: boolean
47
+ }) {
48
+ const Comp = asChild ? Slot : "button"
49
+
50
+ return (
51
+ <Comp
52
+ data-slot="button"
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ export { Button, buttonVariants }
components/ui/calendar.tsx ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ ChevronDownIcon,
6
+ ChevronLeftIcon,
7
+ ChevronRightIcon,
8
+ } from "lucide-react"
9
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
10
+
11
+ import { cn } from "@/lib/utils"
12
+ import { Button, buttonVariants } from "@/components/ui/button"
13
+
14
+ function Calendar({
15
+ className,
16
+ classNames,
17
+ showOutsideDays = true,
18
+ captionLayout = "label",
19
+ buttonVariant = "ghost",
20
+ formatters,
21
+ components,
22
+ ...props
23
+ }: React.ComponentProps<typeof DayPicker> & {
24
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"]
25
+ }) {
26
+ const defaultClassNames = getDefaultClassNames()
27
+
28
+ return (
29
+ <DayPicker
30
+ showOutsideDays={showOutsideDays}
31
+ className={cn(
32
+ "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
33
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
34
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
35
+ className
36
+ )}
37
+ captionLayout={captionLayout}
38
+ formatters={{
39
+ formatMonthDropdown: (date) =>
40
+ date.toLocaleString("default", { month: "short" }),
41
+ ...formatters,
42
+ }}
43
+ classNames={{
44
+ root: cn("w-fit", defaultClassNames.root),
45
+ months: cn(
46
+ "flex gap-4 flex-col md:flex-row relative",
47
+ defaultClassNames.months
48
+ ),
49
+ month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
50
+ nav: cn(
51
+ "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
52
+ defaultClassNames.nav
53
+ ),
54
+ button_previous: cn(
55
+ buttonVariants({ variant: buttonVariant }),
56
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
57
+ defaultClassNames.button_previous
58
+ ),
59
+ button_next: cn(
60
+ buttonVariants({ variant: buttonVariant }),
61
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
62
+ defaultClassNames.button_next
63
+ ),
64
+ month_caption: cn(
65
+ "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
66
+ defaultClassNames.month_caption
67
+ ),
68
+ dropdowns: cn(
69
+ "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
70
+ defaultClassNames.dropdowns
71
+ ),
72
+ dropdown_root: cn(
73
+ "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
74
+ defaultClassNames.dropdown_root
75
+ ),
76
+ dropdown: cn(
77
+ "absolute bg-popover inset-0 opacity-0",
78
+ defaultClassNames.dropdown
79
+ ),
80
+ caption_label: cn(
81
+ "select-none font-medium",
82
+ captionLayout === "label"
83
+ ? "text-sm"
84
+ : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
85
+ defaultClassNames.caption_label
86
+ ),
87
+ table: "w-full border-collapse",
88
+ weekdays: cn("flex", defaultClassNames.weekdays),
89
+ weekday: cn(
90
+ "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
91
+ defaultClassNames.weekday
92
+ ),
93
+ week: cn("flex w-full mt-2", defaultClassNames.week),
94
+ week_number_header: cn(
95
+ "select-none w-(--cell-size)",
96
+ defaultClassNames.week_number_header
97
+ ),
98
+ week_number: cn(
99
+ "text-[0.8rem] select-none text-muted-foreground",
100
+ defaultClassNames.week_number
101
+ ),
102
+ day: cn(
103
+ "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
104
+ defaultClassNames.day
105
+ ),
106
+ range_start: cn(
107
+ "rounded-l-md bg-accent",
108
+ defaultClassNames.range_start
109
+ ),
110
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
111
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
112
+ today: cn(
113
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
114
+ defaultClassNames.today
115
+ ),
116
+ outside: cn(
117
+ "text-muted-foreground aria-selected:text-muted-foreground",
118
+ defaultClassNames.outside
119
+ ),
120
+ disabled: cn(
121
+ "text-muted-foreground opacity-50",
122
+ defaultClassNames.disabled
123
+ ),
124
+ hidden: cn("invisible", defaultClassNames.hidden),
125
+ ...classNames,
126
+ }}
127
+ components={{
128
+ Root: ({ className, rootRef, ...props }) => {
129
+ return (
130
+ <div
131
+ data-slot="calendar"
132
+ ref={rootRef}
133
+ className={cn(className)}
134
+ {...props}
135
+ />
136
+ )
137
+ },
138
+ Chevron: ({ className, orientation, ...props }) => {
139
+ if (orientation === "left") {
140
+ return (
141
+ <ChevronLeftIcon className={cn("size-4", className)} {...props} />
142
+ )
143
+ }
144
+
145
+ if (orientation === "right") {
146
+ return (
147
+ <ChevronRightIcon
148
+ className={cn("size-4", className)}
149
+ {...props}
150
+ />
151
+ )
152
+ }
153
+
154
+ return (
155
+ <ChevronDownIcon className={cn("size-4", className)} {...props} />
156
+ )
157
+ },
158
+ DayButton: CalendarDayButton,
159
+ WeekNumber: ({ children, ...props }) => {
160
+ return (
161
+ <td {...props}>
162
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
163
+ {children}
164
+ </div>
165
+ </td>
166
+ )
167
+ },
168
+ ...components,
169
+ }}
170
+ {...props}
171
+ />
172
+ )
173
+ }
174
+
175
+ function CalendarDayButton({
176
+ className,
177
+ day,
178
+ modifiers,
179
+ ...props
180
+ }: React.ComponentProps<typeof DayButton>) {
181
+ const defaultClassNames = getDefaultClassNames()
182
+
183
+ const ref = React.useRef<HTMLButtonElement>(null)
184
+ React.useEffect(() => {
185
+ if (modifiers.focused) ref.current?.focus()
186
+ }, [modifiers.focused])
187
+
188
+ return (
189
+ <Button
190
+ ref={ref}
191
+ variant="ghost"
192
+ size="icon"
193
+ data-day={day.date.toLocaleDateString()}
194
+ data-selected-single={
195
+ modifiers.selected &&
196
+ !modifiers.range_start &&
197
+ !modifiers.range_end &&
198
+ !modifiers.range_middle
199
+ }
200
+ data-range-start={modifiers.range_start}
201
+ data-range-end={modifiers.range_end}
202
+ data-range-middle={modifiers.range_middle}
203
+ className={cn(
204
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
205
+ defaultClassNames.day,
206
+ className
207
+ )}
208
+ {...props}
209
+ />
210
+ )
211
+ }
212
+
213
+ export { Calendar, CalendarDayButton }
components/ui/card.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
components/ui/carousel.tsx ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import useEmblaCarousel, {
5
+ type UseEmblaCarouselType,
6
+ } from "embla-carousel-react"
7
+ import { ArrowLeft, ArrowRight } from "lucide-react"
8
+
9
+ import { cn } from "@/lib/utils"
10
+ import { Button } from "@/components/ui/button"
11
+
12
+ type CarouselApi = UseEmblaCarouselType[1]
13
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
14
+ type CarouselOptions = UseCarouselParameters[0]
15
+ type CarouselPlugin = UseCarouselParameters[1]
16
+
17
+ type CarouselProps = {
18
+ opts?: CarouselOptions
19
+ plugins?: CarouselPlugin
20
+ orientation?: "horizontal" | "vertical"
21
+ setApi?: (api: CarouselApi) => void
22
+ }
23
+
24
+ type CarouselContextProps = {
25
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0]
26
+ api: ReturnType<typeof useEmblaCarousel>[1]
27
+ scrollPrev: () => void
28
+ scrollNext: () => void
29
+ canScrollPrev: boolean
30
+ canScrollNext: boolean
31
+ } & CarouselProps
32
+
33
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null)
34
+
35
+ function useCarousel() {
36
+ const context = React.useContext(CarouselContext)
37
+
38
+ if (!context) {
39
+ throw new Error("useCarousel must be used within a <Carousel />")
40
+ }
41
+
42
+ return context
43
+ }
44
+
45
+ function Carousel({
46
+ orientation = "horizontal",
47
+ opts,
48
+ setApi,
49
+ plugins,
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<"div"> & CarouselProps) {
54
+ const [carouselRef, api] = useEmblaCarousel(
55
+ {
56
+ ...opts,
57
+ axis: orientation === "horizontal" ? "x" : "y",
58
+ },
59
+ plugins
60
+ )
61
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
62
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
63
+
64
+ const onSelect = React.useCallback((api: CarouselApi) => {
65
+ if (!api) return
66
+ setCanScrollPrev(api.canScrollPrev())
67
+ setCanScrollNext(api.canScrollNext())
68
+ }, [])
69
+
70
+ const scrollPrev = React.useCallback(() => {
71
+ api?.scrollPrev()
72
+ }, [api])
73
+
74
+ const scrollNext = React.useCallback(() => {
75
+ api?.scrollNext()
76
+ }, [api])
77
+
78
+ const handleKeyDown = React.useCallback(
79
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
80
+ if (event.key === "ArrowLeft") {
81
+ event.preventDefault()
82
+ scrollPrev()
83
+ } else if (event.key === "ArrowRight") {
84
+ event.preventDefault()
85
+ scrollNext()
86
+ }
87
+ },
88
+ [scrollPrev, scrollNext]
89
+ )
90
+
91
+ React.useEffect(() => {
92
+ if (!api || !setApi) return
93
+ setApi(api)
94
+ }, [api, setApi])
95
+
96
+ React.useEffect(() => {
97
+ if (!api) return
98
+ onSelect(api)
99
+ api.on("reInit", onSelect)
100
+ api.on("select", onSelect)
101
+
102
+ return () => {
103
+ api?.off("select", onSelect)
104
+ }
105
+ }, [api, onSelect])
106
+
107
+ return (
108
+ <CarouselContext.Provider
109
+ value={{
110
+ carouselRef,
111
+ api: api,
112
+ opts,
113
+ orientation:
114
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
115
+ scrollPrev,
116
+ scrollNext,
117
+ canScrollPrev,
118
+ canScrollNext,
119
+ }}
120
+ >
121
+ <div
122
+ onKeyDownCapture={handleKeyDown}
123
+ className={cn("relative", className)}
124
+ role="region"
125
+ aria-roledescription="carousel"
126
+ data-slot="carousel"
127
+ {...props}
128
+ >
129
+ {children}
130
+ </div>
131
+ </CarouselContext.Provider>
132
+ )
133
+ }
134
+
135
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136
+ const { carouselRef, orientation } = useCarousel()
137
+
138
+ return (
139
+ <div
140
+ ref={carouselRef}
141
+ className="overflow-hidden"
142
+ data-slot="carousel-content"
143
+ >
144
+ <div
145
+ className={cn(
146
+ "flex",
147
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
148
+ className
149
+ )}
150
+ {...props}
151
+ />
152
+ </div>
153
+ )
154
+ }
155
+
156
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157
+ const { orientation } = useCarousel()
158
+
159
+ return (
160
+ <div
161
+ role="group"
162
+ aria-roledescription="slide"
163
+ data-slot="carousel-item"
164
+ className={cn(
165
+ "min-w-0 shrink-0 grow-0 basis-full",
166
+ orientation === "horizontal" ? "pl-4" : "pt-4",
167
+ className
168
+ )}
169
+ {...props}
170
+ />
171
+ )
172
+ }
173
+
174
+ function CarouselPrevious({
175
+ className,
176
+ variant = "outline",
177
+ size = "icon",
178
+ ...props
179
+ }: React.ComponentProps<typeof Button>) {
180
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
181
+
182
+ return (
183
+ <Button
184
+ data-slot="carousel-previous"
185
+ variant={variant}
186
+ size={size}
187
+ className={cn(
188
+ "absolute size-8 rounded-full",
189
+ orientation === "horizontal"
190
+ ? "top-1/2 -left-12 -translate-y-1/2"
191
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
192
+ className
193
+ )}
194
+ disabled={!canScrollPrev}
195
+ onClick={scrollPrev}
196
+ {...props}
197
+ >
198
+ <ArrowLeft />
199
+ <span className="sr-only">Previous slide</span>
200
+ </Button>
201
+ )
202
+ }
203
+
204
+ function CarouselNext({
205
+ className,
206
+ variant = "outline",
207
+ size = "icon",
208
+ ...props
209
+ }: React.ComponentProps<typeof Button>) {
210
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
211
+
212
+ return (
213
+ <Button
214
+ data-slot="carousel-next"
215
+ variant={variant}
216
+ size={size}
217
+ className={cn(
218
+ "absolute size-8 rounded-full",
219
+ orientation === "horizontal"
220
+ ? "top-1/2 -right-12 -translate-y-1/2"
221
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
222
+ className
223
+ )}
224
+ disabled={!canScrollNext}
225
+ onClick={scrollNext}
226
+ {...props}
227
+ >
228
+ <ArrowRight />
229
+ <span className="sr-only">Next slide</span>
230
+ </Button>
231
+ )
232
+ }
233
+
234
+ export {
235
+ type CarouselApi,
236
+ Carousel,
237
+ CarouselContent,
238
+ CarouselItem,
239
+ CarouselPrevious,
240
+ CarouselNext,
241
+ }
components/ui/chart.tsx ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RechartsPrimitive from "recharts"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const
10
+
11
+ export type ChartConfig = {
12
+ [k in string]: {
13
+ label?: React.ReactNode
14
+ icon?: React.ComponentType
15
+ } & (
16
+ | { color?: string; theme?: never }
17
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
+ )
19
+ }
20
+
21
+ type ChartContextProps = {
22
+ config: ChartConfig
23
+ }
24
+
25
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
26
+
27
+ function useChart() {
28
+ const context = React.useContext(ChartContext)
29
+
30
+ if (!context) {
31
+ throw new Error("useChart must be used within a <ChartContainer />")
32
+ }
33
+
34
+ return context
35
+ }
36
+
37
+ function ChartContainer({
38
+ id,
39
+ className,
40
+ children,
41
+ config,
42
+ ...props
43
+ }: React.ComponentProps<"div"> & {
44
+ config: ChartConfig
45
+ children: React.ComponentProps<
46
+ typeof RechartsPrimitive.ResponsiveContainer
47
+ >["children"]
48
+ }) {
49
+ const uniqueId = React.useId()
50
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
51
+
52
+ return (
53
+ <ChartContext.Provider value={{ config }}>
54
+ <div
55
+ data-slot="chart"
56
+ data-chart={chartId}
57
+ className={cn(
58
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
59
+ className
60
+ )}
61
+ {...props}
62
+ >
63
+ <ChartStyle id={chartId} config={config} />
64
+ <RechartsPrimitive.ResponsiveContainer>
65
+ {children}
66
+ </RechartsPrimitive.ResponsiveContainer>
67
+ </div>
68
+ </ChartContext.Provider>
69
+ )
70
+ }
71
+
72
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
73
+ const colorConfig = Object.entries(config).filter(
74
+ ([, config]) => config.theme || config.color
75
+ )
76
+
77
+ if (!colorConfig.length) {
78
+ return null
79
+ }
80
+
81
+ return (
82
+ <style
83
+ dangerouslySetInnerHTML={{
84
+ __html: Object.entries(THEMES)
85
+ .map(
86
+ ([theme, prefix]) => `
87
+ ${prefix} [data-chart=${id}] {
88
+ ${colorConfig
89
+ .map(([key, itemConfig]) => {
90
+ const color =
91
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
92
+ itemConfig.color
93
+ return color ? ` --color-${key}: ${color};` : null
94
+ })
95
+ .join("\n")}
96
+ }
97
+ `
98
+ )
99
+ .join("\n"),
100
+ }}
101
+ />
102
+ )
103
+ }
104
+
105
+ const ChartTooltip = RechartsPrimitive.Tooltip
106
+
107
+ function ChartTooltipContent(props: any) {
108
+ const {
109
+ active,
110
+ payload,
111
+ className,
112
+ indicator = "dot",
113
+ hideLabel = false,
114
+ hideIndicator = false,
115
+ label,
116
+ labelFormatter,
117
+ labelClassName,
118
+ formatter,
119
+ color,
120
+ nameKey,
121
+ labelKey,
122
+ } = props
123
+
124
+ const { config } = useChart()
125
+
126
+ const tooltipLabel = React.useMemo(() => {
127
+ if (hideLabel || !payload?.length) {
128
+ return null
129
+ }
130
+
131
+ const [item] = payload
132
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`
133
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
134
+ const value =
135
+ !labelKey && typeof label === "string"
136
+ ? config[label as keyof typeof config]?.label || label
137
+ : itemConfig?.label
138
+
139
+ if (labelFormatter) {
140
+ return (
141
+ <div className={cn("font-medium", labelClassName)}>
142
+ {labelFormatter(value, payload)}
143
+ </div>
144
+ )
145
+ }
146
+
147
+ if (!value) {
148
+ return null
149
+ }
150
+
151
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
152
+ }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
153
+
154
+ if (!active || !payload?.length) {
155
+ return null
156
+ }
157
+
158
+ const nestLabel = payload.length === 1 && indicator !== "dot"
159
+
160
+ return (
161
+ <div
162
+ className={cn(
163
+ "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
164
+ className
165
+ )}
166
+ >
167
+ {!nestLabel ? tooltipLabel : null}
168
+ <div className="grid gap-1.5">
169
+ {payload.map((item: any, index: number) => {
170
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
171
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
172
+ const indicatorColor = color || item?.payload?.fill || item?.color
173
+
174
+ return (
175
+ <div
176
+ key={item.dataKey ?? index}
177
+ className={cn(
178
+ "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
179
+ indicator === "dot" && "items-center"
180
+ )}
181
+ >
182
+ {formatter && item?.value !== undefined && item.name ? (
183
+ formatter(item.value, item.name, item, index, item.payload)
184
+ ) : (
185
+ <>
186
+ {itemConfig?.icon ? (
187
+ <itemConfig.icon />
188
+ ) : (
189
+ !hideIndicator && (
190
+ <div
191
+ className={cn(
192
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
193
+ {
194
+ "h-2.5 w-2.5": indicator === "dot",
195
+ "w-1": indicator === "line",
196
+ "w-0 border-[1.5px] border-dashed bg-transparent":
197
+ indicator === "dashed",
198
+ "my-0.5": nestLabel && indicator === "dashed",
199
+ }
200
+ )}
201
+ style={
202
+ {
203
+ "--color-bg": indicatorColor,
204
+ "--color-border": indicatorColor,
205
+ } as React.CSSProperties
206
+ }
207
+ />
208
+ )
209
+ )}
210
+ <div
211
+ className={cn(
212
+ "flex flex-1 justify-between leading-none",
213
+ nestLabel ? "items-end" : "items-center"
214
+ )}
215
+ >
216
+ <div className="grid gap-1.5">
217
+ {nestLabel ? tooltipLabel : null}
218
+ <span className="text-muted-foreground">
219
+ {itemConfig?.label || item.name}
220
+ </span>
221
+ </div>
222
+ {item.value !== undefined && (
223
+ <span className="text-foreground font-mono font-medium tabular-nums">
224
+ {typeof item.value === "number"
225
+ ? item.value.toLocaleString()
226
+ : String(item.value)}
227
+ </span>
228
+ )}
229
+ </div>
230
+ </>
231
+ )}
232
+ </div>
233
+ )
234
+ })}
235
+ </div>
236
+ </div>
237
+ )
238
+ }
239
+
240
+ const ChartLegend = RechartsPrimitive.Legend
241
+
242
+ function ChartLegendContent(props: any) {
243
+ const { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey } = props
244
+ const { config } = useChart()
245
+
246
+ if (!payload?.length) {
247
+ return null
248
+ }
249
+
250
+ return (
251
+ <div
252
+ className={cn(
253
+ "flex items-center justify-center gap-4",
254
+ verticalAlign === "top" ? "pb-3" : "pt-3",
255
+ className
256
+ )}
257
+ >
258
+ {payload.map((item: any) => {
259
+ const key = `${nameKey || item.dataKey || "value"}`
260
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
261
+
262
+ return (
263
+ <div
264
+ key={item.value}
265
+ className={cn(
266
+ "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
267
+ )}
268
+ >
269
+ {itemConfig?.icon && !hideIcon ? (
270
+ <itemConfig.icon />
271
+ ) : (
272
+ <div
273
+ className="h-2 w-2 shrink-0 rounded-[2px]"
274
+ style={{
275
+ backgroundColor: item.color,
276
+ }}
277
+ />
278
+ )}
279
+ {itemConfig?.label}
280
+ </div>
281
+ )
282
+ })}
283
+ </div>
284
+ )
285
+ }
286
+
287
+ // Helper to extract item config from a payload.
288
+ function getPayloadConfigFromPayload(
289
+ config: ChartConfig,
290
+ payload: unknown,
291
+ key: string
292
+ ) {
293
+ if (typeof payload !== "object" || payload === null) {
294
+ return undefined
295
+ }
296
+
297
+ // Attempt to find a label key inside the payload or payload.payload
298
+ const payloadPayload =
299
+ typeof (payload as any).payload === "object" && (payload as any).payload !== null
300
+ ? (payload as any).payload
301
+ : undefined
302
+
303
+ let configLabelKey: string = key
304
+
305
+ if (key in (payload as any) && typeof (payload as any)[key] === "string") {
306
+ configLabelKey = (payload as any)[key] as string
307
+ } else if (
308
+ payloadPayload &&
309
+ key in payloadPayload &&
310
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
311
+ ) {
312
+ configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
313
+ }
314
+
315
+ return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
316
+ }
317
+
318
+ export {
319
+ ChartContainer,
320
+ ChartTooltip,
321
+ ChartTooltipContent,
322
+ ChartLegend,
323
+ ChartLegendContent,
324
+ ChartStyle,
325
+ }
components/ui/checkbox.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5
+ import { CheckIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Checkbox({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
13
+ return (
14
+ <CheckboxPrimitive.Root
15
+ data-slot="checkbox"
16
+ className={cn(
17
+ "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
18
+ className
19
+ )}
20
+ {...props}
21
+ >
22
+ <CheckboxPrimitive.Indicator
23
+ data-slot="checkbox-indicator"
24
+ className="flex items-center justify-center text-current transition-none"
25
+ >
26
+ <CheckIcon className="size-3.5" />
27
+ </CheckboxPrimitive.Indicator>
28
+ </CheckboxPrimitive.Root>
29
+ )
30
+ }
31
+
32
+ export { Checkbox }
components/ui/collapsible.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4
+
5
+ function Collapsible({
6
+ ...props
7
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
9
+ }
10
+
11
+ function CollapsibleTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14
+ return (
15
+ <CollapsiblePrimitive.CollapsibleTrigger
16
+ data-slot="collapsible-trigger"
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function CollapsibleContent({
23
+ ...props
24
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25
+ return (
26
+ <CollapsiblePrimitive.CollapsibleContent
27
+ data-slot="collapsible-content"
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
components/ui/command.tsx ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Command as CommandPrimitive } from "cmdk"
5
+ import { SearchIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "@/components/ui/dialog"
15
+
16
+ function Command({
17
+ className,
18
+ ...props
19
+ }: React.ComponentProps<typeof CommandPrimitive>) {
20
+ return (
21
+ <CommandPrimitive
22
+ data-slot="command"
23
+ className={cn(
24
+ "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function CommandDialog({
33
+ title = "Command Palette",
34
+ description = "Search for a command to run...",
35
+ children,
36
+ className,
37
+ showCloseButton = true,
38
+ ...props
39
+ }: React.ComponentProps<typeof Dialog> & {
40
+ title?: string
41
+ description?: string
42
+ className?: string
43
+ showCloseButton?: boolean
44
+ }) {
45
+ return (
46
+ <Dialog {...props}>
47
+ <DialogHeader className="sr-only">
48
+ <DialogTitle>{title}</DialogTitle>
49
+ <DialogDescription>{description}</DialogDescription>
50
+ </DialogHeader>
51
+ <DialogContent
52
+ className={cn("overflow-hidden p-0", className)}
53
+ showCloseButton={showCloseButton}
54
+ >
55
+ <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
56
+ {children}
57
+ </Command>
58
+ </DialogContent>
59
+ </Dialog>
60
+ )
61
+ }
62
+
63
+ function CommandInput({
64
+ className,
65
+ ...props
66
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
67
+ return (
68
+ <div
69
+ data-slot="command-input-wrapper"
70
+ className="flex h-9 items-center gap-2 border-b px-3"
71
+ >
72
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
73
+ <CommandPrimitive.Input
74
+ data-slot="command-input"
75
+ className={cn(
76
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ </div>
82
+ )
83
+ }
84
+
85
+ function CommandList({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
89
+ return (
90
+ <CommandPrimitive.List
91
+ data-slot="command-list"
92
+ className={cn(
93
+ "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
94
+ className
95
+ )}
96
+ {...props}
97
+ />
98
+ )
99
+ }
100
+
101
+ function CommandEmpty({
102
+ ...props
103
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
104
+ return (
105
+ <CommandPrimitive.Empty
106
+ data-slot="command-empty"
107
+ className="py-6 text-center text-sm"
108
+ {...props}
109
+ />
110
+ )
111
+ }
112
+
113
+ function CommandGroup({
114
+ className,
115
+ ...props
116
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
117
+ return (
118
+ <CommandPrimitive.Group
119
+ data-slot="command-group"
120
+ className={cn(
121
+ "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ )
127
+ }
128
+
129
+ function CommandSeparator({
130
+ className,
131
+ ...props
132
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
133
+ return (
134
+ <CommandPrimitive.Separator
135
+ data-slot="command-separator"
136
+ className={cn("bg-border -mx-1 h-px", className)}
137
+ {...props}
138
+ />
139
+ )
140
+ }
141
+
142
+ function CommandItem({
143
+ className,
144
+ ...props
145
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
146
+ return (
147
+ <CommandPrimitive.Item
148
+ data-slot="command-item"
149
+ className={cn(
150
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
151
+ className
152
+ )}
153
+ {...props}
154
+ />
155
+ )
156
+ }
157
+
158
+ function CommandShortcut({
159
+ className,
160
+ ...props
161
+ }: React.ComponentProps<"span">) {
162
+ return (
163
+ <span
164
+ data-slot="command-shortcut"
165
+ className={cn(
166
+ "text-muted-foreground ml-auto text-xs tracking-widest",
167
+ className
168
+ )}
169
+ {...props}
170
+ />
171
+ )
172
+ }
173
+
174
+ export {
175
+ Command,
176
+ CommandDialog,
177
+ CommandInput,
178
+ CommandList,
179
+ CommandEmpty,
180
+ CommandGroup,
181
+ CommandItem,
182
+ CommandShortcut,
183
+ CommandSeparator,
184
+ }
components/ui/context-menu.tsx ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function ContextMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
12
+ return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
13
+ }
14
+
15
+ function ContextMenuTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
18
+ return (
19
+ <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
20
+ )
21
+ }
22
+
23
+ function ContextMenuGroup({
24
+ ...props
25
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
26
+ return (
27
+ <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
28
+ )
29
+ }
30
+
31
+ function ContextMenuPortal({
32
+ ...props
33
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
34
+ return (
35
+ <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
36
+ )
37
+ }
38
+
39
+ function ContextMenuSub({
40
+ ...props
41
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
42
+ return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
43
+ }
44
+
45
+ function ContextMenuRadioGroup({
46
+ ...props
47
+ }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
48
+ return (
49
+ <ContextMenuPrimitive.RadioGroup
50
+ data-slot="context-menu-radio-group"
51
+ {...props}
52
+ />
53
+ )
54
+ }
55
+
56
+ function ContextMenuSubTrigger({
57
+ className,
58
+ inset,
59
+ children,
60
+ ...props
61
+ }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
62
+ inset?: boolean
63
+ }) {
64
+ return (
65
+ <ContextMenuPrimitive.SubTrigger
66
+ data-slot="context-menu-sub-trigger"
67
+ data-inset={inset}
68
+ className={cn(
69
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
70
+ className
71
+ )}
72
+ {...props}
73
+ >
74
+ {children}
75
+ <ChevronRightIcon className="ml-auto" />
76
+ </ContextMenuPrimitive.SubTrigger>
77
+ )
78
+ }
79
+
80
+ function ContextMenuSubContent({
81
+ className,
82
+ ...props
83
+ }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
84
+ return (
85
+ <ContextMenuPrimitive.SubContent
86
+ data-slot="context-menu-sub-content"
87
+ className={cn(
88
+ "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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ )
94
+ }
95
+
96
+ function ContextMenuContent({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
100
+ return (
101
+ <ContextMenuPrimitive.Portal>
102
+ <ContextMenuPrimitive.Content
103
+ data-slot="context-menu-content"
104
+ className={cn(
105
+ "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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
106
+ className
107
+ )}
108
+ {...props}
109
+ />
110
+ </ContextMenuPrimitive.Portal>
111
+ )
112
+ }
113
+
114
+ function ContextMenuItem({
115
+ className,
116
+ inset,
117
+ variant = "default",
118
+ ...props
119
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
120
+ inset?: boolean
121
+ variant?: "default" | "destructive"
122
+ }) {
123
+ return (
124
+ <ContextMenuPrimitive.Item
125
+ data-slot="context-menu-item"
126
+ data-inset={inset}
127
+ data-variant={variant}
128
+ className={cn(
129
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
130
+ className
131
+ )}
132
+ {...props}
133
+ />
134
+ )
135
+ }
136
+
137
+ function ContextMenuCheckboxItem({
138
+ className,
139
+ children,
140
+ checked,
141
+ ...props
142
+ }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
143
+ return (
144
+ <ContextMenuPrimitive.CheckboxItem
145
+ data-slot="context-menu-checkbox-item"
146
+ className={cn(
147
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
148
+ className
149
+ )}
150
+ checked={checked}
151
+ {...props}
152
+ >
153
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
154
+ <ContextMenuPrimitive.ItemIndicator>
155
+ <CheckIcon className="size-4" />
156
+ </ContextMenuPrimitive.ItemIndicator>
157
+ </span>
158
+ {children}
159
+ </ContextMenuPrimitive.CheckboxItem>
160
+ )
161
+ }
162
+
163
+ function ContextMenuRadioItem({
164
+ className,
165
+ children,
166
+ ...props
167
+ }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
168
+ return (
169
+ <ContextMenuPrimitive.RadioItem
170
+ data-slot="context-menu-radio-item"
171
+ className={cn(
172
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
173
+ className
174
+ )}
175
+ {...props}
176
+ >
177
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
178
+ <ContextMenuPrimitive.ItemIndicator>
179
+ <CircleIcon className="size-2 fill-current" />
180
+ </ContextMenuPrimitive.ItemIndicator>
181
+ </span>
182
+ {children}
183
+ </ContextMenuPrimitive.RadioItem>
184
+ )
185
+ }
186
+
187
+ function ContextMenuLabel({
188
+ className,
189
+ inset,
190
+ ...props
191
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
192
+ inset?: boolean
193
+ }) {
194
+ return (
195
+ <ContextMenuPrimitive.Label
196
+ data-slot="context-menu-label"
197
+ data-inset={inset}
198
+ className={cn(
199
+ "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
200
+ className
201
+ )}
202
+ {...props}
203
+ />
204
+ )
205
+ }
206
+
207
+ function ContextMenuSeparator({
208
+ className,
209
+ ...props
210
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
211
+ return (
212
+ <ContextMenuPrimitive.Separator
213
+ data-slot="context-menu-separator"
214
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
215
+ {...props}
216
+ />
217
+ )
218
+ }
219
+
220
+ function ContextMenuShortcut({
221
+ className,
222
+ ...props
223
+ }: React.ComponentProps<"span">) {
224
+ return (
225
+ <span
226
+ data-slot="context-menu-shortcut"
227
+ className={cn(
228
+ "text-muted-foreground ml-auto text-xs tracking-widest",
229
+ className
230
+ )}
231
+ {...props}
232
+ />
233
+ )
234
+ }
235
+
236
+ export {
237
+ ContextMenu,
238
+ ContextMenuTrigger,
239
+ ContextMenuContent,
240
+ ContextMenuItem,
241
+ ContextMenuCheckboxItem,
242
+ ContextMenuRadioItem,
243
+ ContextMenuLabel,
244
+ ContextMenuSeparator,
245
+ ContextMenuShortcut,
246
+ ContextMenuGroup,
247
+ ContextMenuPortal,
248
+ ContextMenuSub,
249
+ ContextMenuSubContent,
250
+ ContextMenuSubTrigger,
251
+ ContextMenuRadioGroup,
252
+ }
components/ui/dialog.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { XIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Dialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
12
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
13
+ }
14
+
15
+ function DialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
19
+ }
20
+
21
+ function DialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
25
+ }
26
+
27
+ function DialogClose({
28
+ ...props
29
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
30
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
31
+ }
32
+
33
+ function DialogOverlay({
34
+ className,
35
+ ...props
36
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37
+ return (
38
+ <DialogPrimitive.Overlay
39
+ data-slot="dialog-overlay"
40
+ className={cn(
41
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function DialogContent({
50
+ className,
51
+ children,
52
+ showCloseButton = true,
53
+ ...props
54
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
55
+ showCloseButton?: boolean
56
+ }) {
57
+ return (
58
+ <DialogPortal data-slot="dialog-portal">
59
+ <DialogOverlay />
60
+ <DialogPrimitive.Content
61
+ data-slot="dialog-content"
62
+ className={cn(
63
+ "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
64
+ className
65
+ )}
66
+ {...props}
67
+ >
68
+ {children}
69
+ {showCloseButton && (
70
+ <DialogPrimitive.Close
71
+ data-slot="dialog-close"
72
+ className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
73
+ >
74
+ <XIcon />
75
+ <span className="sr-only">Close</span>
76
+ </DialogPrimitive.Close>
77
+ )}
78
+ </DialogPrimitive.Content>
79
+ </DialogPortal>
80
+ )
81
+ }
82
+
83
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="dialog-header"
87
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
94
+ return (
95
+ <div
96
+ data-slot="dialog-footer"
97
+ className={cn(
98
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
99
+ className
100
+ )}
101
+ {...props}
102
+ />
103
+ )
104
+ }
105
+
106
+ function DialogTitle({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
110
+ return (
111
+ <DialogPrimitive.Title
112
+ data-slot="dialog-title"
113
+ className={cn("text-lg leading-none font-semibold", className)}
114
+ {...props}
115
+ />
116
+ )
117
+ }
118
+
119
+ function DialogDescription({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
123
+ return (
124
+ <DialogPrimitive.Description
125
+ data-slot="dialog-description"
126
+ className={cn("text-muted-foreground text-sm", className)}
127
+ {...props}
128
+ />
129
+ )
130
+ }
131
+
132
+ export {
133
+ Dialog,
134
+ DialogClose,
135
+ DialogContent,
136
+ DialogDescription,
137
+ DialogFooter,
138
+ DialogHeader,
139
+ DialogOverlay,
140
+ DialogPortal,
141
+ DialogTitle,
142
+ DialogTrigger,
143
+ }
components/ui/drawer.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Drawer as DrawerPrimitive } from "vaul"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Drawer({
9
+ ...props
10
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
11
+ return <DrawerPrimitive.Root data-slot="drawer" {...props} />
12
+ }
13
+
14
+ function DrawerTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
17
+ return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
18
+ }
19
+
20
+ function DrawerPortal({
21
+ ...props
22
+ }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
23
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
24
+ }
25
+
26
+ function DrawerClose({
27
+ ...props
28
+ }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
29
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
30
+ }
31
+
32
+ function DrawerOverlay({
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
36
+ return (
37
+ <DrawerPrimitive.Overlay
38
+ data-slot="drawer-overlay"
39
+ className={cn(
40
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ function DrawerContent({
49
+ className,
50
+ children,
51
+ ...props
52
+ }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
53
+ return (
54
+ <DrawerPortal data-slot="drawer-portal">
55
+ <DrawerOverlay />
56
+ <DrawerPrimitive.Content
57
+ data-slot="drawer-content"
58
+ className={cn(
59
+ "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
60
+ "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
61
+ "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
62
+ "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
63
+ "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
64
+ className
65
+ )}
66
+ {...props}
67
+ >
68
+ <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
69
+ {children}
70
+ </DrawerPrimitive.Content>
71
+ </DrawerPortal>
72
+ )
73
+ }
74
+
75
+ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ data-slot="drawer-header"
79
+ className={cn(
80
+ "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
81
+ className
82
+ )}
83
+ {...props}
84
+ />
85
+ )
86
+ }
87
+
88
+ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
89
+ return (
90
+ <div
91
+ data-slot="drawer-footer"
92
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ function DrawerTitle({
99
+ className,
100
+ ...props
101
+ }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
102
+ return (
103
+ <DrawerPrimitive.Title
104
+ data-slot="drawer-title"
105
+ className={cn("text-foreground font-semibold", className)}
106
+ {...props}
107
+ />
108
+ )
109
+ }
110
+
111
+ function DrawerDescription({
112
+ className,
113
+ ...props
114
+ }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
115
+ return (
116
+ <DrawerPrimitive.Description
117
+ data-slot="drawer-description"
118
+ className={cn("text-muted-foreground text-sm", className)}
119
+ {...props}
120
+ />
121
+ )
122
+ }
123
+
124
+ export {
125
+ Drawer,
126
+ DrawerPortal,
127
+ DrawerOverlay,
128
+ DrawerTrigger,
129
+ DrawerClose,
130
+ DrawerContent,
131
+ DrawerHeader,
132
+ DrawerFooter,
133
+ DrawerTitle,
134
+ DrawerDescription,
135
+ }
components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function DropdownMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
13
+ }
14
+
15
+ function DropdownMenuPortal({
16
+ ...props
17
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18
+ return (
19
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20
+ )
21
+ }
22
+
23
+ function DropdownMenuTrigger({
24
+ ...props
25
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26
+ return (
27
+ <DropdownMenuPrimitive.Trigger
28
+ data-slot="dropdown-menu-trigger"
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function DropdownMenuContent({
35
+ className,
36
+ sideOffset = 4,
37
+ ...props
38
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
39
+ return (
40
+ <DropdownMenuPrimitive.Portal>
41
+ <DropdownMenuPrimitive.Content
42
+ data-slot="dropdown-menu-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ "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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </DropdownMenuPrimitive.Portal>
51
+ )
52
+ }
53
+
54
+ function DropdownMenuGroup({
55
+ ...props
56
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
57
+ return (
58
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
59
+ )
60
+ }
61
+
62
+ function DropdownMenuItem({
63
+ className,
64
+ inset,
65
+ variant = "default",
66
+ ...props
67
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
68
+ inset?: boolean
69
+ variant?: "default" | "destructive"
70
+ }) {
71
+ return (
72
+ <DropdownMenuPrimitive.Item
73
+ data-slot="dropdown-menu-item"
74
+ data-inset={inset}
75
+ data-variant={variant}
76
+ className={cn(
77
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ )
83
+ }
84
+
85
+ function DropdownMenuCheckboxItem({
86
+ className,
87
+ children,
88
+ checked,
89
+ ...props
90
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
91
+ return (
92
+ <DropdownMenuPrimitive.CheckboxItem
93
+ data-slot="dropdown-menu-checkbox-item"
94
+ className={cn(
95
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
96
+ className
97
+ )}
98
+ checked={checked}
99
+ {...props}
100
+ >
101
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
102
+ <DropdownMenuPrimitive.ItemIndicator>
103
+ <CheckIcon className="size-4" />
104
+ </DropdownMenuPrimitive.ItemIndicator>
105
+ </span>
106
+ {children}
107
+ </DropdownMenuPrimitive.CheckboxItem>
108
+ )
109
+ }
110
+
111
+ function DropdownMenuRadioGroup({
112
+ ...props
113
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
114
+ return (
115
+ <DropdownMenuPrimitive.RadioGroup
116
+ data-slot="dropdown-menu-radio-group"
117
+ {...props}
118
+ />
119
+ )
120
+ }
121
+
122
+ function DropdownMenuRadioItem({
123
+ className,
124
+ children,
125
+ ...props
126
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
127
+ return (
128
+ <DropdownMenuPrimitive.RadioItem
129
+ data-slot="dropdown-menu-radio-item"
130
+ className={cn(
131
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
132
+ className
133
+ )}
134
+ {...props}
135
+ >
136
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
137
+ <DropdownMenuPrimitive.ItemIndicator>
138
+ <CircleIcon className="size-2 fill-current" />
139
+ </DropdownMenuPrimitive.ItemIndicator>
140
+ </span>
141
+ {children}
142
+ </DropdownMenuPrimitive.RadioItem>
143
+ )
144
+ }
145
+
146
+ function DropdownMenuLabel({
147
+ className,
148
+ inset,
149
+ ...props
150
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
151
+ inset?: boolean
152
+ }) {
153
+ return (
154
+ <DropdownMenuPrimitive.Label
155
+ data-slot="dropdown-menu-label"
156
+ data-inset={inset}
157
+ className={cn(
158
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
159
+ className
160
+ )}
161
+ {...props}
162
+ />
163
+ )
164
+ }
165
+
166
+ function DropdownMenuSeparator({
167
+ className,
168
+ ...props
169
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
170
+ return (
171
+ <DropdownMenuPrimitive.Separator
172
+ data-slot="dropdown-menu-separator"
173
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
174
+ {...props}
175
+ />
176
+ )
177
+ }
178
+
179
+ function DropdownMenuShortcut({
180
+ className,
181
+ ...props
182
+ }: React.ComponentProps<"span">) {
183
+ return (
184
+ <span
185
+ data-slot="dropdown-menu-shortcut"
186
+ className={cn(
187
+ "text-muted-foreground ml-auto text-xs tracking-widest",
188
+ className
189
+ )}
190
+ {...props}
191
+ />
192
+ )
193
+ }
194
+
195
+ function DropdownMenuSub({
196
+ ...props
197
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
198
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
199
+ }
200
+
201
+ function DropdownMenuSubTrigger({
202
+ className,
203
+ inset,
204
+ children,
205
+ ...props
206
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
207
+ inset?: boolean
208
+ }) {
209
+ return (
210
+ <DropdownMenuPrimitive.SubTrigger
211
+ data-slot="dropdown-menu-sub-trigger"
212
+ data-inset={inset}
213
+ className={cn(
214
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
215
+ className
216
+ )}
217
+ {...props}
218
+ >
219
+ {children}
220
+ <ChevronRightIcon className="ml-auto size-4" />
221
+ </DropdownMenuPrimitive.SubTrigger>
222
+ )
223
+ }
224
+
225
+ function DropdownMenuSubContent({
226
+ className,
227
+ ...props
228
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
229
+ return (
230
+ <DropdownMenuPrimitive.SubContent
231
+ data-slot="dropdown-menu-sub-content"
232
+ className={cn(
233
+ "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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
234
+ className
235
+ )}
236
+ {...props}
237
+ />
238
+ )
239
+ }
240
+
241
+ export {
242
+ DropdownMenu,
243
+ DropdownMenuPortal,
244
+ DropdownMenuTrigger,
245
+ DropdownMenuContent,
246
+ DropdownMenuGroup,
247
+ DropdownMenuLabel,
248
+ DropdownMenuItem,
249
+ DropdownMenuCheckboxItem,
250
+ DropdownMenuRadioGroup,
251
+ DropdownMenuRadioItem,
252
+ DropdownMenuSeparator,
253
+ DropdownMenuShortcut,
254
+ DropdownMenuSub,
255
+ DropdownMenuSubTrigger,
256
+ DropdownMenuSubContent,
257
+ }
components/ui/form.tsx ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+ import { Slot } from "@radix-ui/react-slot"
6
+ import {
7
+ Controller,
8
+ FormProvider,
9
+ useFormContext,
10
+ useFormState,
11
+ type ControllerProps,
12
+ type FieldPath,
13
+ type FieldValues,
14
+ } from "react-hook-form"
15
+
16
+ import { cn } from "@/lib/utils"
17
+ import { Label } from "@/components/ui/label"
18
+
19
+ const Form = FormProvider
20
+
21
+ type FormFieldContextValue<
22
+ TFieldValues extends FieldValues = FieldValues,
23
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
24
+ > = {
25
+ name: TName
26
+ }
27
+
28
+ const FormFieldContext = React.createContext<FormFieldContextValue>(
29
+ {} as FormFieldContextValue
30
+ )
31
+
32
+ const FormField = <
33
+ TFieldValues extends FieldValues = FieldValues,
34
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
35
+ >({
36
+ ...props
37
+ }: ControllerProps<TFieldValues, TName>) => {
38
+ return (
39
+ <FormFieldContext.Provider value={{ name: props.name }}>
40
+ <Controller {...props} />
41
+ </FormFieldContext.Provider>
42
+ )
43
+ }
44
+
45
+ const useFormField = () => {
46
+ const fieldContext = React.useContext(FormFieldContext)
47
+ const itemContext = React.useContext(FormItemContext)
48
+ const { getFieldState } = useFormContext()
49
+ const formState = useFormState({ name: fieldContext.name })
50
+ const fieldState = getFieldState(fieldContext.name, formState)
51
+
52
+ if (!fieldContext) {
53
+ throw new Error("useFormField should be used within <FormField>")
54
+ }
55
+
56
+ const { id } = itemContext
57
+
58
+ return {
59
+ id,
60
+ name: fieldContext.name,
61
+ formItemId: `${id}-form-item`,
62
+ formDescriptionId: `${id}-form-item-description`,
63
+ formMessageId: `${id}-form-item-message`,
64
+ ...fieldState,
65
+ }
66
+ }
67
+
68
+ type FormItemContextValue = {
69
+ id: string
70
+ }
71
+
72
+ const FormItemContext = React.createContext<FormItemContextValue>(
73
+ {} as FormItemContextValue
74
+ )
75
+
76
+ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77
+ const id = React.useId()
78
+
79
+ return (
80
+ <FormItemContext.Provider value={{ id }}>
81
+ <div
82
+ data-slot="form-item"
83
+ className={cn("grid gap-2", className)}
84
+ {...props}
85
+ />
86
+ </FormItemContext.Provider>
87
+ )
88
+ }
89
+
90
+ function FormLabel({
91
+ className,
92
+ ...props
93
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
94
+ const { error, formItemId } = useFormField()
95
+
96
+ return (
97
+ <Label
98
+ data-slot="form-label"
99
+ data-error={!!error}
100
+ className={cn("data-[error=true]:text-destructive", className)}
101
+ htmlFor={formItemId}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
108
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109
+
110
+ return (
111
+ <Slot
112
+ data-slot="form-control"
113
+ id={formItemId}
114
+ aria-describedby={
115
+ !error
116
+ ? `${formDescriptionId}`
117
+ : `${formDescriptionId} ${formMessageId}`
118
+ }
119
+ aria-invalid={!!error}
120
+ {...props}
121
+ />
122
+ )
123
+ }
124
+
125
+ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
126
+ const { formDescriptionId } = useFormField()
127
+
128
+ return (
129
+ <p
130
+ data-slot="form-description"
131
+ id={formDescriptionId}
132
+ className={cn("text-muted-foreground text-sm", className)}
133
+ {...props}
134
+ />
135
+ )
136
+ }
137
+
138
+ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
139
+ const { error, formMessageId } = useFormField()
140
+ const body = error ? String(error?.message ?? "") : props.children
141
+
142
+ if (!body) {
143
+ return null
144
+ }
145
+
146
+ return (
147
+ <p
148
+ data-slot="form-message"
149
+ id={formMessageId}
150
+ className={cn("text-destructive text-sm", className)}
151
+ {...props}
152
+ >
153
+ {body}
154
+ </p>
155
+ )
156
+ }
157
+
158
+ export {
159
+ useFormField,
160
+ Form,
161
+ FormItem,
162
+ FormLabel,
163
+ FormControl,
164
+ FormDescription,
165
+ FormMessage,
166
+ FormField,
167
+ }
components/ui/hover-card.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function HoverCard({
9
+ ...props
10
+ }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
11
+ return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
12
+ }
13
+
14
+ function HoverCardTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
17
+ return (
18
+ <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
19
+ )
20
+ }
21
+
22
+ function HoverCardContent({
23
+ className,
24
+ align = "center",
25
+ sideOffset = 4,
26
+ ...props
27
+ }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
28
+ return (
29
+ <HoverCardPrimitive.Portal data-slot="hover-card-portal">
30
+ <HoverCardPrimitive.Content
31
+ data-slot="hover-card-content"
32
+ align={align}
33
+ sideOffset={sideOffset}
34
+ className={cn(
35
+ "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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ </HoverCardPrimitive.Portal>
41
+ )
42
+ }
43
+
44
+ export { HoverCard, HoverCardTrigger, HoverCardContent }
components/ui/input-otp.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { OTPInput, OTPInputContext } from "input-otp"
5
+ import { MinusIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function InputOTP({
10
+ className,
11
+ containerClassName,
12
+ ...props
13
+ }: React.ComponentProps<typeof OTPInput> & {
14
+ containerClassName?: string
15
+ }) {
16
+ return (
17
+ <OTPInput
18
+ data-slot="input-otp"
19
+ containerClassName={cn(
20
+ "flex items-center gap-2 has-disabled:opacity-50",
21
+ containerClassName
22
+ )}
23
+ className={cn("disabled:cursor-not-allowed", className)}
24
+ {...props}
25
+ />
26
+ )
27
+ }
28
+
29
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
30
+ return (
31
+ <div
32
+ data-slot="input-otp-group"
33
+ className={cn("flex items-center", className)}
34
+ {...props}
35
+ />
36
+ )
37
+ }
38
+
39
+ function InputOTPSlot({
40
+ index,
41
+ className,
42
+ ...props
43
+ }: React.ComponentProps<"div"> & {
44
+ index: number
45
+ }) {
46
+ const inputOTPContext = React.useContext(OTPInputContext)
47
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
48
+
49
+ return (
50
+ <div
51
+ data-slot="input-otp-slot"
52
+ data-active={isActive}
53
+ className={cn(
54
+ "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
55
+ className
56
+ )}
57
+ {...props}
58
+ >
59
+ {char}
60
+ {hasFakeCaret && (
61
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
62
+ <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
63
+ </div>
64
+ )}
65
+ </div>
66
+ )
67
+ }
68
+
69
+ function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div data-slot="input-otp-separator" role="separator" {...props}>
72
+ <MinusIcon />
73
+ </div>
74
+ )
75
+ }
76
+
77
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
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 flex 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 }
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 }
components/ui/menubar.tsx ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as MenubarPrimitive from "@radix-ui/react-menubar"
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Menubar({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof MenubarPrimitive.Root>) {
13
+ return (
14
+ <MenubarPrimitive.Root
15
+ data-slot="menubar"
16
+ className={cn(
17
+ "bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ function MenubarMenu({
26
+ ...props
27
+ }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
28
+ return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
29
+ }
30
+
31
+ function MenubarGroup({
32
+ ...props
33
+ }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
34
+ return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
35
+ }
36
+
37
+ function MenubarPortal({
38
+ ...props
39
+ }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
40
+ return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
41
+ }
42
+
43
+ function MenubarRadioGroup({
44
+ ...props
45
+ }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
46
+ return (
47
+ <MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
48
+ )
49
+ }
50
+
51
+ function MenubarTrigger({
52
+ className,
53
+ ...props
54
+ }: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
55
+ return (
56
+ <MenubarPrimitive.Trigger
57
+ data-slot="menubar-trigger"
58
+ className={cn(
59
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
60
+ className
61
+ )}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ function MenubarContent({
68
+ className,
69
+ align = "start",
70
+ alignOffset = -4,
71
+ sideOffset = 8,
72
+ ...props
73
+ }: React.ComponentProps<typeof MenubarPrimitive.Content>) {
74
+ return (
75
+ <MenubarPortal>
76
+ <MenubarPrimitive.Content
77
+ data-slot="menubar-content"
78
+ align={align}
79
+ alignOffset={alignOffset}
80
+ sideOffset={sideOffset}
81
+ className={cn(
82
+ "bg-popover text-popover-foreground data-[state=open]:animate-in 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ </MenubarPortal>
88
+ )
89
+ }
90
+
91
+ function MenubarItem({
92
+ className,
93
+ inset,
94
+ variant = "default",
95
+ ...props
96
+ }: React.ComponentProps<typeof MenubarPrimitive.Item> & {
97
+ inset?: boolean
98
+ variant?: "default" | "destructive"
99
+ }) {
100
+ return (
101
+ <MenubarPrimitive.Item
102
+ data-slot="menubar-item"
103
+ data-inset={inset}
104
+ data-variant={variant}
105
+ className={cn(
106
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
107
+ className
108
+ )}
109
+ {...props}
110
+ />
111
+ )
112
+ }
113
+
114
+ function MenubarCheckboxItem({
115
+ className,
116
+ children,
117
+ checked,
118
+ ...props
119
+ }: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
120
+ return (
121
+ <MenubarPrimitive.CheckboxItem
122
+ data-slot="menubar-checkbox-item"
123
+ className={cn(
124
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 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",
125
+ className
126
+ )}
127
+ checked={checked}
128
+ {...props}
129
+ >
130
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
131
+ <MenubarPrimitive.ItemIndicator>
132
+ <CheckIcon className="size-4" />
133
+ </MenubarPrimitive.ItemIndicator>
134
+ </span>
135
+ {children}
136
+ </MenubarPrimitive.CheckboxItem>
137
+ )
138
+ }
139
+
140
+ function MenubarRadioItem({
141
+ className,
142
+ children,
143
+ ...props
144
+ }: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
145
+ return (
146
+ <MenubarPrimitive.RadioItem
147
+ data-slot="menubar-radio-item"
148
+ className={cn(
149
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 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",
150
+ className
151
+ )}
152
+ {...props}
153
+ >
154
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
155
+ <MenubarPrimitive.ItemIndicator>
156
+ <CircleIcon className="size-2 fill-current" />
157
+ </MenubarPrimitive.ItemIndicator>
158
+ </span>
159
+ {children}
160
+ </MenubarPrimitive.RadioItem>
161
+ )
162
+ }
163
+
164
+ function MenubarLabel({
165
+ className,
166
+ inset,
167
+ ...props
168
+ }: React.ComponentProps<typeof MenubarPrimitive.Label> & {
169
+ inset?: boolean
170
+ }) {
171
+ return (
172
+ <MenubarPrimitive.Label
173
+ data-slot="menubar-label"
174
+ data-inset={inset}
175
+ className={cn(
176
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
177
+ className
178
+ )}
179
+ {...props}
180
+ />
181
+ )
182
+ }
183
+
184
+ function MenubarSeparator({
185
+ className,
186
+ ...props
187
+ }: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
188
+ return (
189
+ <MenubarPrimitive.Separator
190
+ data-slot="menubar-separator"
191
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
192
+ {...props}
193
+ />
194
+ )
195
+ }
196
+
197
+ function MenubarShortcut({
198
+ className,
199
+ ...props
200
+ }: React.ComponentProps<"span">) {
201
+ return (
202
+ <span
203
+ data-slot="menubar-shortcut"
204
+ className={cn(
205
+ "text-muted-foreground ml-auto text-xs tracking-widest",
206
+ className
207
+ )}
208
+ {...props}
209
+ />
210
+ )
211
+ }
212
+
213
+ function MenubarSub({
214
+ ...props
215
+ }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
216
+ return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
217
+ }
218
+
219
+ function MenubarSubTrigger({
220
+ className,
221
+ inset,
222
+ children,
223
+ ...props
224
+ }: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
225
+ inset?: boolean
226
+ }) {
227
+ return (
228
+ <MenubarPrimitive.SubTrigger
229
+ data-slot="menubar-sub-trigger"
230
+ data-inset={inset}
231
+ className={cn(
232
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
233
+ className
234
+ )}
235
+ {...props}
236
+ >
237
+ {children}
238
+ <ChevronRightIcon className="ml-auto h-4 w-4" />
239
+ </MenubarPrimitive.SubTrigger>
240
+ )
241
+ }
242
+
243
+ function MenubarSubContent({
244
+ className,
245
+ ...props
246
+ }: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
247
+ return (
248
+ <MenubarPrimitive.SubContent
249
+ data-slot="menubar-sub-content"
250
+ className={cn(
251
+ "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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
252
+ className
253
+ )}
254
+ {...props}
255
+ />
256
+ )
257
+ }
258
+
259
+ export {
260
+ Menubar,
261
+ MenubarPortal,
262
+ MenubarMenu,
263
+ MenubarTrigger,
264
+ MenubarContent,
265
+ MenubarGroup,
266
+ MenubarSeparator,
267
+ MenubarLabel,
268
+ MenubarItem,
269
+ MenubarShortcut,
270
+ MenubarCheckboxItem,
271
+ MenubarRadioGroup,
272
+ MenubarRadioItem,
273
+ MenubarSub,
274
+ MenubarSubTrigger,
275
+ MenubarSubContent,
276
+ }
components/ui/navigation-menu.tsx ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3
+ import { cva } from "class-variance-authority"
4
+ import { ChevronDownIcon } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function NavigationMenu({
9
+ className,
10
+ children,
11
+ viewport = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
14
+ viewport?: boolean
15
+ }) {
16
+ return (
17
+ <NavigationMenuPrimitive.Root
18
+ data-slot="navigation-menu"
19
+ data-viewport={viewport}
20
+ className={cn(
21
+ "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
22
+ className
23
+ )}
24
+ {...props}
25
+ >
26
+ {children}
27
+ {viewport && <NavigationMenuViewport />}
28
+ </NavigationMenuPrimitive.Root>
29
+ )
30
+ }
31
+
32
+ function NavigationMenuList({
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
36
+ return (
37
+ <NavigationMenuPrimitive.List
38
+ data-slot="navigation-menu-list"
39
+ className={cn(
40
+ "group flex flex-1 list-none items-center justify-center gap-1",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ function NavigationMenuItem({
49
+ className,
50
+ ...props
51
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
52
+ return (
53
+ <NavigationMenuPrimitive.Item
54
+ data-slot="navigation-menu-item"
55
+ className={cn("relative", className)}
56
+ {...props}
57
+ />
58
+ )
59
+ }
60
+
61
+ const navigationMenuTriggerStyle = cva(
62
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
63
+ )
64
+
65
+ function NavigationMenuTrigger({
66
+ className,
67
+ children,
68
+ ...props
69
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
70
+ return (
71
+ <NavigationMenuPrimitive.Trigger
72
+ data-slot="navigation-menu-trigger"
73
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
74
+ {...props}
75
+ >
76
+ {children}{" "}
77
+ <ChevronDownIcon
78
+ className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
79
+ aria-hidden="true"
80
+ />
81
+ </NavigationMenuPrimitive.Trigger>
82
+ )
83
+ }
84
+
85
+ function NavigationMenuContent({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
89
+ return (
90
+ <NavigationMenuPrimitive.Content
91
+ data-slot="navigation-menu-content"
92
+ className={cn(
93
+ "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
94
+ "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
95
+ className
96
+ )}
97
+ {...props}
98
+ />
99
+ )
100
+ }
101
+
102
+ function NavigationMenuViewport({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
106
+ return (
107
+ <div
108
+ className={cn(
109
+ "absolute top-full left-0 isolate z-50 flex justify-center"
110
+ )}
111
+ >
112
+ <NavigationMenuPrimitive.Viewport
113
+ data-slot="navigation-menu-viewport"
114
+ className={cn(
115
+ "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
116
+ className
117
+ )}
118
+ {...props}
119
+ />
120
+ </div>
121
+ )
122
+ }
123
+
124
+ function NavigationMenuLink({
125
+ className,
126
+ ...props
127
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
128
+ return (
129
+ <NavigationMenuPrimitive.Link
130
+ data-slot="navigation-menu-link"
131
+ className={cn(
132
+ "data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
133
+ className
134
+ )}
135
+ {...props}
136
+ />
137
+ )
138
+ }
139
+
140
+ function NavigationMenuIndicator({
141
+ className,
142
+ ...props
143
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
144
+ return (
145
+ <NavigationMenuPrimitive.Indicator
146
+ data-slot="navigation-menu-indicator"
147
+ className={cn(
148
+ "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
149
+ className
150
+ )}
151
+ {...props}
152
+ >
153
+ <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
154
+ </NavigationMenuPrimitive.Indicator>
155
+ )
156
+ }
157
+
158
+ export {
159
+ NavigationMenu,
160
+ NavigationMenuList,
161
+ NavigationMenuItem,
162
+ NavigationMenuContent,
163
+ NavigationMenuTrigger,
164
+ NavigationMenuLink,
165
+ NavigationMenuIndicator,
166
+ NavigationMenuViewport,
167
+ navigationMenuTriggerStyle,
168
+ }
components/ui/pagination.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import {
3
+ ChevronLeftIcon,
4
+ ChevronRightIcon,
5
+ MoreHorizontalIcon,
6
+ } from "lucide-react"
7
+
8
+ import { cn } from "@/lib/utils"
9
+ import { Button, buttonVariants } from "@/components/ui/button"
10
+
11
+ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
12
+ return (
13
+ <nav
14
+ role="navigation"
15
+ aria-label="pagination"
16
+ data-slot="pagination"
17
+ className={cn("mx-auto flex w-full justify-center", className)}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function PaginationContent({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<"ul">) {
27
+ return (
28
+ <ul
29
+ data-slot="pagination-content"
30
+ className={cn("flex flex-row items-center gap-1", className)}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function PaginationItem({ ...props }: React.ComponentProps<"li">) {
37
+ return <li data-slot="pagination-item" {...props} />
38
+ }
39
+
40
+ type PaginationLinkProps = {
41
+ isActive?: boolean
42
+ } & Pick<React.ComponentProps<typeof Button>, "size"> &
43
+ React.ComponentProps<"a">
44
+
45
+ function PaginationLink({
46
+ className,
47
+ isActive,
48
+ size = "icon",
49
+ ...props
50
+ }: PaginationLinkProps) {
51
+ return (
52
+ <a
53
+ aria-current={isActive ? "page" : undefined}
54
+ data-slot="pagination-link"
55
+ data-active={isActive}
56
+ className={cn(
57
+ buttonVariants({
58
+ variant: isActive ? "outline" : "ghost",
59
+ size,
60
+ }),
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function PaginationPrevious({
69
+ className,
70
+ ...props
71
+ }: React.ComponentProps<typeof PaginationLink>) {
72
+ return (
73
+ <PaginationLink
74
+ aria-label="Go to previous page"
75
+ size="default"
76
+ className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
77
+ {...props}
78
+ >
79
+ <ChevronLeftIcon />
80
+ <span className="hidden sm:block">Previous</span>
81
+ </PaginationLink>
82
+ )
83
+ }
84
+
85
+ function PaginationNext({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof PaginationLink>) {
89
+ return (
90
+ <PaginationLink
91
+ aria-label="Go to next page"
92
+ size="default"
93
+ className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
94
+ {...props}
95
+ >
96
+ <span className="hidden sm:block">Next</span>
97
+ <ChevronRightIcon />
98
+ </PaginationLink>
99
+ )
100
+ }
101
+
102
+ function PaginationEllipsis({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<"span">) {
106
+ return (
107
+ <span
108
+ aria-hidden
109
+ data-slot="pagination-ellipsis"
110
+ className={cn("flex size-9 items-center justify-center", className)}
111
+ {...props}
112
+ >
113
+ <MoreHorizontalIcon className="size-4" />
114
+ <span className="sr-only">More pages</span>
115
+ </span>
116
+ )
117
+ }
118
+
119
+ export {
120
+ Pagination,
121
+ PaginationContent,
122
+ PaginationLink,
123
+ PaginationItem,
124
+ PaginationPrevious,
125
+ PaginationNext,
126
+ PaginationEllipsis,
127
+ }
components/ui/popover.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Popover({
9
+ ...props
10
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
11
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />
12
+ }
13
+
14
+ function PopoverTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
17
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
18
+ }
19
+
20
+ function PopoverContent({
21
+ className,
22
+ align = "center",
23
+ sideOffset = 4,
24
+ ...props
25
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
26
+ return (
27
+ <PopoverPrimitive.Portal>
28
+ <PopoverPrimitive.Content
29
+ data-slot="popover-content"
30
+ align={align}
31
+ sideOffset={sideOffset}
32
+ className={cn(
33
+ "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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34
+ className
35
+ )}
36
+ {...props}
37
+ />
38
+ </PopoverPrimitive.Portal>
39
+ )
40
+ }
41
+
42
+ function PopoverAnchor({
43
+ ...props
44
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
45
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
46
+ }
47
+
48
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
components/ui/progress.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as ProgressPrimitive from "@radix-ui/react-progress"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Progress({
9
+ className,
10
+ value,
11
+ ...props
12
+ }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
13
+ return (
14
+ <ProgressPrimitive.Root
15
+ data-slot="progress"
16
+ className={cn(
17
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
18
+ className
19
+ )}
20
+ {...props}
21
+ >
22
+ <ProgressPrimitive.Indicator
23
+ data-slot="progress-indicator"
24
+ className="bg-primary h-full w-full flex-1 transition-all"
25
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26
+ />
27
+ </ProgressPrimitive.Root>
28
+ )
29
+ }
30
+
31
+ export { Progress }
components/ui/radio-group.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5
+ import { CircleIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function RadioGroup({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
13
+ return (
14
+ <RadioGroupPrimitive.Root
15
+ data-slot="radio-group"
16
+ className={cn("grid gap-3", className)}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function RadioGroupItem({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
26
+ return (
27
+ <RadioGroupPrimitive.Item
28
+ data-slot="radio-group-item"
29
+ className={cn(
30
+ "border-input text-primary 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 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
31
+ className
32
+ )}
33
+ {...props}
34
+ >
35
+ <RadioGroupPrimitive.Indicator
36
+ data-slot="radio-group-indicator"
37
+ className="relative flex items-center justify-center"
38
+ >
39
+ <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
40
+ </RadioGroupPrimitive.Indicator>
41
+ </RadioGroupPrimitive.Item>
42
+ )
43
+ }
44
+
45
+ export { RadioGroup, RadioGroupItem }