SarahXia0405 commited on
Commit
00e2e56
·
verified ·
1 Parent(s): 346f022

Delete web

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. web/README.md +0 -11
  2. web/index.html +0 -15
  3. web/package.json +0 -61
  4. web/src/.DS_Store +0 -0
  5. web/src/App.tsx +0 -563
  6. web/src/Attributions.md +0 -3
  7. web/src/components/.DS_Store +0 -0
  8. web/src/components/ChatArea.tsx +0 -282
  9. web/src/components/FileUploadArea.tsx +0 -300
  10. web/src/components/FloatingActionButtons.tsx +0 -111
  11. web/src/components/GroupMembers.tsx +0 -57
  12. web/src/components/Header.tsx +0 -79
  13. web/src/components/LearningModeSelector.tsx +0 -94
  14. web/src/components/LeftSidebar.tsx +0 -121
  15. web/src/components/MemoryLine.tsx +0 -108
  16. web/src/components/Message.tsx +0 -100
  17. web/src/components/RightPanel.tsx +0 -307
  18. web/src/components/UserGuide.tsx +0 -151
  19. web/src/components/figma/ImageWithFallback.tsx +0 -27
  20. web/src/components/ui/accordion.tsx +0 -66
  21. web/src/components/ui/alert-dialog.tsx +0 -157
  22. web/src/components/ui/alert.tsx +0 -66
  23. web/src/components/ui/aspect-ratio.tsx +0 -11
  24. web/src/components/ui/avatar.tsx +0 -53
  25. web/src/components/ui/badge.tsx +0 -46
  26. web/src/components/ui/breadcrumb.tsx +0 -109
  27. web/src/components/ui/button.tsx +0 -58
  28. web/src/components/ui/calendar.tsx +0 -75
  29. web/src/components/ui/card.tsx +0 -92
  30. web/src/components/ui/carousel.tsx +0 -241
  31. web/src/components/ui/chart.tsx +0 -353
  32. web/src/components/ui/checkbox.tsx +0 -32
  33. web/src/components/ui/collapsible.tsx +0 -33
  34. web/src/components/ui/command.tsx +0 -177
  35. web/src/components/ui/context-menu.tsx +0 -252
  36. web/src/components/ui/dialog.tsx +0 -137
  37. web/src/components/ui/drawer.tsx +0 -132
  38. web/src/components/ui/dropdown-menu.tsx +0 -257
  39. web/src/components/ui/form.tsx +0 -168
  40. web/src/components/ui/hover-card.tsx +0 -44
  41. web/src/components/ui/input-otp.tsx +0 -77
  42. web/src/components/ui/input.tsx +0 -21
  43. web/src/components/ui/label.tsx +0 -24
  44. web/src/components/ui/menubar.tsx +0 -276
  45. web/src/components/ui/navigation-menu.tsx +0 -168
  46. web/src/components/ui/pagination.tsx +0 -127
  47. web/src/components/ui/popover.tsx +0 -48
  48. web/src/components/ui/progress.tsx +0 -31
  49. web/src/components/ui/radio-group.tsx +0 -45
  50. web/src/components/ui/resizable.tsx +0 -56
web/README.md DELETED
@@ -1,11 +0,0 @@
1
-
2
- # Clare AI Tutor UI Redesign (Copy)
3
-
4
- This is a code bundle for Clare AI Tutor UI Redesign (Copy). The original project is available at https://www.figma.com/design/yC1iBsNtBGpBH8sXO43uah/Clare-AI-Tutor-UI-Redesign--Copy-.
5
-
6
- ## Running the code
7
-
8
- Run `npm i` to install the dependencies.
9
-
10
- Run `npm run dev` to start the development server.
11
-
 
 
 
 
 
 
 
 
 
 
 
 
web/index.html DELETED
@@ -1,15 +0,0 @@
1
-
2
- <!DOCTYPE html>
3
- <html lang="en">
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Clare AI Tutor UI Redesign (Copy)</title>
8
- </head>
9
-
10
- <body>
11
- <div id="root"></div>
12
- <script type="module" src="/src/main.tsx"></script>
13
- </body>
14
- </html>
15
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/package.json DELETED
@@ -1,61 +0,0 @@
1
-
2
- {
3
- "name": "Clare AI Tutor UI Redesign (Copy)",
4
- "version": "0.1.0",
5
- "private": true,
6
- "dependencies": {
7
- "@radix-ui/react-accordion": "^1.2.3",
8
- "@radix-ui/react-alert-dialog": "^1.1.6",
9
- "@radix-ui/react-aspect-ratio": "^1.1.2",
10
- "@radix-ui/react-avatar": "^1.1.3",
11
- "@radix-ui/react-checkbox": "^1.1.4",
12
- "@radix-ui/react-collapsible": "^1.1.3",
13
- "@radix-ui/react-context-menu": "^2.2.6",
14
- "@radix-ui/react-dialog": "^1.1.6",
15
- "@radix-ui/react-dropdown-menu": "^2.1.6",
16
- "@radix-ui/react-hover-card": "^1.1.6",
17
- "@radix-ui/react-label": "^2.1.2",
18
- "@radix-ui/react-menubar": "^1.1.6",
19
- "@radix-ui/react-navigation-menu": "^1.2.5",
20
- "@radix-ui/react-popover": "^1.1.6",
21
- "@radix-ui/react-progress": "^1.1.2",
22
- "@radix-ui/react-radio-group": "^1.2.3",
23
- "@radix-ui/react-scroll-area": "^1.2.3",
24
- "@radix-ui/react-select": "^2.1.6",
25
- "@radix-ui/react-separator": "^1.1.2",
26
- "@radix-ui/react-slider": "^1.2.3",
27
- "@radix-ui/react-slot": "^1.1.2",
28
- "@radix-ui/react-switch": "^1.1.3",
29
- "@radix-ui/react-tabs": "^1.1.3",
30
- "@radix-ui/react-toggle": "^1.1.2",
31
- "@radix-ui/react-toggle-group": "^1.1.2",
32
- "@radix-ui/react-tooltip": "^1.1.8",
33
- "class-variance-authority": "^0.7.1",
34
- "clsx": "*",
35
- "cmdk": "^1.1.1",
36
- "embla-carousel-react": "^8.6.0",
37
- "input-otp": "^1.4.2",
38
- "lucide-react": "^0.487.0",
39
- "next-themes": "^0.4.6",
40
- "react": "^18.3.1",
41
- "react-day-picker": "^8.10.1",
42
- "react-dom": "^18.3.1",
43
- "react-hook-form": "^7.55.0",
44
- "react-resizable-panels": "^2.1.7",
45
- "react-markdown": "^9.0.1",
46
- "remark-gfm": "^4.0.0",
47
- "recharts": "^2.15.2",
48
- "sonner": "^2.0.3",
49
- "tailwind-merge": "*",
50
- "vaul": "^1.1.2"
51
- },
52
- "devDependencies": {
53
- "@types/node": "^20.10.0",
54
- "@vitejs/plugin-react-swc": "^3.10.2",
55
- "vite": "6.3.5"
56
- },
57
- "scripts": {
58
- "dev": "vite",
59
- "build": "vite build"
60
- }
61
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/.DS_Store DELETED
Binary file (6.15 kB)
 
web/src/App.tsx DELETED
@@ -1,563 +0,0 @@
1
- // web/src/App.tsx
2
- import React, { useState, useEffect, useMemo } from 'react';
3
- import { Header } from './components/Header';
4
- import { LeftSidebar } from './components/LeftSidebar';
5
- import { ChatArea } from './components/ChatArea';
6
- import { RightPanel } from './components/RightPanel';
7
- import { FloatingActionButtons } from './components/FloatingActionButtons';
8
- import { X, ChevronLeft, ChevronRight } from 'lucide-react';
9
- import { Button } from './components/ui/button';
10
- import { Toaster } from './components/ui/sonner';
11
- import { toast } from 'sonner';
12
-
13
- export interface Message {
14
- id: string;
15
- role: 'user' | 'assistant';
16
- content: string;
17
- timestamp: Date;
18
- references?: string[];
19
- sender?: GroupMember;
20
- }
21
-
22
- export interface User {
23
- name: string;
24
- email: string; // login 输入(原始输入)
25
- user_id: string; // 实际传后端的 user_id(这里等同 email/ID)
26
- }
27
-
28
- export interface GroupMember {
29
- id: string;
30
- name: string;
31
- email: string;
32
- avatar?: string;
33
- isAI?: boolean;
34
- }
35
-
36
- export type SpaceType = 'individual' | 'group';
37
- export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';
38
-
39
- export interface UploadedFile {
40
- file: File;
41
- type: FileType;
42
- uploaded?: boolean;
43
- uploadedChunks?: number;
44
- }
45
-
46
- export type LearningMode = 'concept' | 'socratic' | 'exam' | 'assignment' | 'summary';
47
- export type Language = 'auto' | 'en' | 'zh';
48
-
49
- type ChatApiResp = {
50
- reply: string;
51
- session_status_md?: string;
52
- refs?: Array<{ source_file?: string; section?: string }>;
53
- latency_ms?: number;
54
- };
55
-
56
- type UploadApiResp = {
57
- ok: boolean;
58
- added_chunks: number;
59
- status_md: string;
60
- };
61
-
62
- type LoginApiResp = {
63
- ok: boolean;
64
- user: { name: string; user_id: string };
65
- };
66
-
67
- function mapFileTypeToDocType(t: FileType): string {
68
- switch (t) {
69
- case 'syllabus':
70
- return 'Syllabus';
71
- case 'lecture-slides':
72
- return 'Lecture Slides';
73
- case 'literature-review':
74
- return 'Literature Review / Paper';
75
- default:
76
- return 'Other';
77
- }
78
- }
79
-
80
- async function apiPostJson<T>(path: string, payload: any): Promise<T> {
81
- const res = await fetch(path, {
82
- method: 'POST',
83
- headers: { 'Content-Type': 'application/json' },
84
- body: JSON.stringify(payload),
85
- });
86
- if (!res.ok) {
87
- const txt = await res.text().catch(() => '');
88
- throw new Error(`HTTP ${res.status}: ${txt || res.statusText}`);
89
- }
90
- return (await res.json()) as T;
91
- }
92
-
93
- async function apiPostForm<T>(path: string, form: FormData): Promise<T> {
94
- const res = await fetch(path, { method: 'POST', body: form });
95
- if (!res.ok) {
96
- const txt = await res.text().catch(() => '');
97
- throw new Error(`HTTP ${res.status}: ${txt || res.statusText}`);
98
- }
99
- return (await res.json()) as T;
100
- }
101
-
102
- function App() {
103
- const [isDarkMode, setIsDarkMode] = useState(() => {
104
- const saved = localStorage.getItem('theme');
105
- return (
106
- saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)
107
- );
108
- });
109
-
110
- const [user, setUser] = useState<User | null>(null);
111
-
112
- const [messages, setMessages] = useState<Message[]>([
113
- {
114
- id: '1',
115
- role: 'assistant',
116
- content:
117
- "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Please log in on the right, upload materials (optional), and ask me anything.",
118
- timestamp: new Date(),
119
- },
120
- ]);
121
-
122
- const [learningMode, setLearningMode] = useState<LearningMode>('concept');
123
- const [language, setLanguage] = useState<Language>('auto');
124
-
125
- const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
126
- const [memoryProgress, setMemoryProgress] = useState(40);
127
-
128
- const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
129
- const [rightPanelOpen, setRightPanelOpen] = useState(false);
130
- const [rightPanelVisible, setRightPanelVisible] = useState(true);
131
-
132
- const [spaceType, setSpaceType] = useState<SpaceType>('individual');
133
-
134
- const [exportResult, setExportResult] = useState('');
135
- const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
136
-
137
- const [groupMembers] = useState<GroupMember[]>([
138
- { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
139
- { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
140
- { id: '2', name: 'Michael Chen', email: 'michael.c@university.edu' },
141
- { id: '3', name: 'Emma Williams', email: 'emma.w@university.edu' },
142
- ]);
143
-
144
- useEffect(() => {
145
- document.documentElement.classList.toggle('dark', isDarkMode);
146
- localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
147
- }, [isDarkMode]);
148
-
149
- // ✅ 彻底去掉 hardcode:未登录 userId 为空
150
- const userId = useMemo(() => (user?.user_id || '').trim(), [user]);
151
-
152
- // ✅ 未登录不可聊天/上传/feedback
153
- const isLoggedIn = useMemo(() => !!user && !!userId, [user, userId]);
154
-
155
- const currentDocTypeForChat = useMemo(() => {
156
- const hasSyllabus = uploadedFiles.some((f) => f.type === 'syllabus' && f.uploaded);
157
- if (hasSyllabus) return 'Syllabus';
158
- const hasSlides = uploadedFiles.some((f) => f.type === 'lecture-slides' && f.uploaded);
159
- if (hasSlides) return 'Lecture Slides';
160
- const hasLit = uploadedFiles.some((f) => f.type === 'literature-review' && f.uploaded);
161
- if (hasLit) return 'Literature Review / Paper';
162
- return 'Other';
163
- }, [uploadedFiles]);
164
-
165
- // ✅ 登录必须打后端 /api/login,确保 server session 有 name
166
- const handleLogin = async (name: string, emailOrId: string) => {
167
- const nameTrim = name.trim();
168
- const idTrim = emailOrId.trim();
169
-
170
- if (!nameTrim || !idTrim) {
171
- toast.error('Please fill in both name and Email/ID');
172
- return;
173
- }
174
-
175
- try {
176
- const resp = await apiPostJson<LoginApiResp>('/api/login', {
177
- name: nameTrim,
178
- user_id: idTrim,
179
- });
180
-
181
- if (!resp?.ok) throw new Error('Login failed (ok=false)');
182
-
183
- // 后端回来的 user_id 为准(保持一致)
184
- setUser({
185
- name: resp.user.name,
186
- email: idTrim,
187
- user_id: resp.user.user_id,
188
- });
189
-
190
- toast.success('Logged in');
191
- } catch (e: any) {
192
- console.error(e);
193
- toast.error(`Login failed: ${e?.message || 'unknown error'}`);
194
- }
195
- };
196
-
197
- const handleLogout = () => {
198
- setUser(null);
199
- toast.message('Logged out');
200
- };
201
-
202
- const handleSendMessage = async (content: string) => {
203
- if (!content.trim()) return;
204
-
205
- if (!isLoggedIn) {
206
- toast.error('Please log in first');
207
- return;
208
- }
209
-
210
- const shouldAIRespond =
211
- spaceType === 'individual' || content.toLowerCase().includes('@clare');
212
-
213
- const sender: GroupMember | undefined =
214
- spaceType === 'group' && user
215
- ? { id: user.user_id, name: user.name, email: user.email }
216
- : undefined;
217
-
218
- const userMessage: Message = {
219
- id: Date.now().toString(),
220
- role: 'user',
221
- content,
222
- timestamp: new Date(),
223
- sender,
224
- };
225
- setMessages((prev) => [...prev, userMessage]);
226
-
227
- if (!shouldAIRespond) return;
228
-
229
- try {
230
- const resp = await apiPostJson<ChatApiResp>('/api/chat', {
231
- user_id: userId,
232
- message: content,
233
- learning_mode: learningMode,
234
- language_preference: language === 'auto' ? 'Auto' : language,
235
- doc_type: currentDocTypeForChat,
236
- });
237
-
238
- const refs = (resp.refs || [])
239
- .map((r) => [r.source_file, r.section].filter(Boolean).join(' / '))
240
- .filter(Boolean);
241
-
242
- const assistantMessage: Message = {
243
- id: (Date.now() + 1).toString(),
244
- role: 'assistant',
245
- content: resp.reply || '(empty reply)',
246
- timestamp: new Date(),
247
- references: refs.length ? refs : undefined,
248
- sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
249
- };
250
-
251
- setMessages((prev) => [...prev, assistantMessage]);
252
- } catch (e: any) {
253
- console.error(e);
254
- toast.error(`Chat failed: ${e?.message || 'unknown error'}`);
255
- setMessages((prev) => [
256
- ...prev,
257
- {
258
- id: (Date.now() + 1).toString(),
259
- role: 'assistant',
260
- content: `Sorry — chat request failed. ${e?.message || ''}`,
261
- timestamp: new Date(),
262
- },
263
- ]);
264
- }
265
- };
266
-
267
- // ✅ 选文件:只入库,不上传
268
- const handleFileUpload = (files: File[]) => {
269
- if (!isLoggedIn) {
270
- toast.error('Please log in first');
271
- return;
272
- }
273
- const newFiles: UploadedFile[] = files.map((file) => ({
274
- file,
275
- type: 'other',
276
- uploaded: false,
277
- }));
278
- setUploadedFiles((prev) => [...prev, ...newFiles]);
279
- toast.message('Files added. Select a File Type, then click Upload.');
280
- };
281
-
282
- const handleRemoveFile = (index: number) => {
283
- setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
284
- };
285
-
286
- const handleFileTypeChange = (index: number, type: FileType) => {
287
- setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
288
- };
289
-
290
- // ✅ 上传单个文件
291
- const handleUploadSingle = async (index: number) => {
292
- if (!isLoggedIn) {
293
- toast.error('Please log in first');
294
- return;
295
- }
296
-
297
- const target = uploadedFiles[index];
298
- if (!target) return;
299
-
300
- try {
301
- const form = new FormData();
302
- form.append('user_id', userId);
303
- form.append('doc_type', mapFileTypeToDocType(target.type));
304
- form.append('file', target.file);
305
-
306
- const r = await apiPostForm<UploadApiResp>('/api/upload', form);
307
- if (!r.ok) throw new Error('Upload response ok=false');
308
-
309
- setUploadedFiles((prev) =>
310
- prev.map((x, i) => (i === index ? { ...x, uploaded: true, uploadedChunks: r.added_chunks } : x))
311
- );
312
-
313
- toast.success(`Uploaded: ${target.file.name} (+${r.added_chunks} chunks)`);
314
- } catch (e: any) {
315
- console.error(e);
316
- toast.error(`Upload failed: ${e?.message || 'unknown error'}`);
317
- }
318
- };
319
-
320
- const handleUploadAllPending = async () => {
321
- if (!isLoggedIn) {
322
- toast.error('Please log in first');
323
- return;
324
- }
325
-
326
- const pendingIdx = uploadedFiles
327
- .map((f, i) => ({ f, i }))
328
- .filter((x) => !x.f.uploaded)
329
- .map((x) => x.i);
330
-
331
- if (!pendingIdx.length) {
332
- toast.message('No pending files to upload.');
333
- return;
334
- }
335
-
336
- for (const idx of pendingIdx) {
337
- // eslint-disable-next-line no-await-in-loop
338
- await handleUploadSingle(idx);
339
- }
340
- };
341
-
342
- const handleClearConversation = () => {
343
- setMessages([
344
- {
345
- id: '1',
346
- role: 'assistant',
347
- content:
348
- "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Please log in on the right, upload materials (optional), and ask me anything.",
349
- timestamp: new Date(),
350
- },
351
- ]);
352
- };
353
-
354
- const handleExport = async () => {
355
- if (!isLoggedIn) {
356
- toast.error('Please log in first');
357
- return;
358
- }
359
- try {
360
- const r = await apiPostJson<{ markdown: string }>('/api/export', {
361
- user_id: userId,
362
- learning_mode: learningMode,
363
- });
364
- setExportResult(r.markdown || '');
365
- setResultType('export');
366
- toast.success('Conversation exported!');
367
- } catch (e: any) {
368
- toast.error(`Export failed: ${e?.message || 'unknown error'}`);
369
- }
370
- };
371
-
372
- const handleSummary = async () => {
373
- if (!isLoggedIn) {
374
- toast.error('Please log in first');
375
- return;
376
- }
377
- try {
378
- const r = await apiPostJson<{ markdown: string }>('/api/summary', {
379
- user_id: userId,
380
- learning_mode: learningMode,
381
- language_preference: language === 'auto' ? 'Auto' : language,
382
- });
383
- setExportResult(r.markdown || '');
384
- setResultType('summary');
385
- toast.success('Summary generated!');
386
- } catch (e: any) {
387
- toast.error(`Summary failed: ${e?.message || 'unknown error'}`);
388
- }
389
- };
390
-
391
- const handleQuiz = () => {
392
- const quiz = `# Micro-Quiz: Responsible AI
393
-
394
- 1) Which is a key principle of Responsible AI?
395
- A) Profit maximization
396
- B) Transparency
397
- C) Rapid deployment
398
- D) Cost reduction
399
- `;
400
- setExportResult(quiz);
401
- setResultType('quiz');
402
- toast.success('Quiz generated!');
403
- };
404
-
405
- useEffect(() => {
406
- if (!isLoggedIn) return;
407
-
408
- const run = async () => {
409
- try {
410
- const res = await fetch(`/api/memoryline?user_id=${encodeURIComponent(userId)}`);
411
- if (!res.ok) return;
412
- const j = await res.json();
413
- const pct = typeof j?.progress_pct === 'number' ? j.progress_pct : null;
414
- if (pct !== null) setMemoryProgress(Math.round(pct * 100));
415
- } catch {
416
- // ignore
417
- }
418
- };
419
- run();
420
- }, [isLoggedIn, userId]);
421
-
422
- return (
423
- <div className="min-h-screen bg-background flex flex-col">
424
- <Toaster />
425
- <Header
426
- user={user}
427
- onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
428
- onUserClick={() => setRightPanelOpen(!rightPanelOpen)}
429
- isDarkMode={isDarkMode}
430
- onToggleDarkMode={() => setIsDarkMode(!isDarkMode)}
431
- />
432
-
433
- <div className="flex-1 flex overflow-hidden">
434
- {leftSidebarOpen && (
435
- <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
436
- )}
437
-
438
- <aside
439
- className={`
440
- fixed lg:static inset-y-0 left-0 z-50
441
- w-80 bg-card border-r border-border
442
- transform transition-transform duration-300 ease-in-out
443
- ${leftSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
444
- lg:translate-x-0
445
- flex flex-col
446
- mt-16 lg:mt-0
447
- `}
448
- >
449
- <div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
450
- <h3>Settings & Guide</h3>
451
- <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
452
- <X className="h-5 w-5" />
453
- </Button>
454
- </div>
455
-
456
- <LeftSidebar
457
- learningMode={learningMode}
458
- language={language}
459
- onLearningModeChange={setLearningMode}
460
- onLanguageChange={setLanguage}
461
- spaceType={spaceType}
462
- onSpaceTypeChange={setSpaceType}
463
- groupMembers={groupMembers}
464
- />
465
- </aside>
466
-
467
- {/* ✅ 右侧 Panel 展开时,为 Chat 留出空间(避免被遮住导致 FAB 只能在收起时可见) */}
468
- <main
469
- className={`
470
- flex-1 flex flex-col min-w-0
471
- transition-all
472
- ${rightPanelVisible ? 'pr-[320px]' : ''}
473
- `}
474
- >
475
- <ChatArea
476
- userId={userId}
477
- docType={currentDocTypeForChat}
478
- messages={messages}
479
- onSendMessage={handleSendMessage}
480
- uploadedFiles={uploadedFiles}
481
- onFileUpload={handleFileUpload}
482
- onRemoveFile={handleRemoveFile}
483
- onFileTypeChange={handleFileTypeChange}
484
- onUploadFile={handleUploadSingle}
485
- onUploadAll={handleUploadAllPending}
486
- memoryProgress={memoryProgress}
487
- isLoggedIn={isLoggedIn}
488
- learningMode={learningMode}
489
- onClearConversation={handleClearConversation}
490
- onLearningModeChange={setLearningMode}
491
- spaceType={spaceType}
492
- />
493
- </main>
494
-
495
- {rightPanelOpen && (
496
- <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
497
- )}
498
-
499
- {rightPanelVisible && (
500
- <aside
501
- className={`
502
- fixed lg:static inset-y-0 right-0 z-50
503
- w-80 bg-card border-l border-border
504
- transform transition-transform duration-300 ease-in-out
505
- ${rightPanelOpen ? 'translate-x-0' : 'translate-x-full'}
506
- lg:translate-x-0
507
- flex flex-col
508
- mt-16 lg:mt-0
509
- `}
510
- >
511
- <div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
512
- <h3>Account & Actions</h3>
513
- <Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
514
- <X className="h-5 w-5" />
515
- </Button>
516
- </div>
517
-
518
- <RightPanel
519
- user={user}
520
- onLogin={handleLogin}
521
- onLogout={handleLogout}
522
- isLoggedIn={isLoggedIn}
523
- onClose={() => setRightPanelVisible(false)}
524
- exportResult={exportResult}
525
- setExportResult={setExportResult}
526
- resultType={resultType}
527
- setResultType={setResultType}
528
- onExport={handleExport}
529
- onQuiz={handleQuiz}
530
- onSummary={handleSummary}
531
- />
532
- </aside>
533
- )}
534
-
535
- <Button
536
- variant="outline"
537
- size="icon"
538
- onClick={() => setRightPanelVisible(!rightPanelVisible)}
539
- className={`hidden lg:flex fixed top-20 z-[70] h-8 w-5 shadow-lg transition-all rounded-l-full rounded-r-none border-r-0 ${
540
- rightPanelVisible ? 'right-[320px]' : 'right-0'
541
- }`}
542
- title={rightPanelVisible ? 'Close panel' : 'Open panel'}
543
- >
544
- {rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
545
- </Button>
546
-
547
- {/* ✅ 右侧 Panel 收起时才显示 FloatingActionButtons(你当前逻辑保持不变) */}
548
- {!rightPanelVisible && (
549
- <FloatingActionButtons
550
- user={user}
551
- isLoggedIn={isLoggedIn}
552
- onOpenPanel={() => setRightPanelVisible(true)}
553
- onExport={handleExport}
554
- onQuiz={handleQuiz}
555
- onSummary={handleSummary}
556
- />
557
- )}
558
- </div>
559
- </div>
560
- );
561
- }
562
-
563
- export default App;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/Attributions.md DELETED
@@ -1,3 +0,0 @@
1
- This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
2
-
3
- This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
 
 
 
 
web/src/components/.DS_Store DELETED
Binary file (6.15 kB)
 
web/src/components/ChatArea.tsx DELETED
@@ -1,282 +0,0 @@
1
- // web/src/components/ChatArea.tsx
2
- import React, { useState, useRef, useEffect, useMemo } from 'react';
3
- import { Button } from './ui/button';
4
- import { Textarea } from './ui/textarea';
5
- import { Send, ArrowDown, Trash2, Share2 } from 'lucide-react';
6
- import { Message } from './Message';
7
- import { FileUploadArea } from './FileUploadArea';
8
- import { MemoryLine } from './MemoryLine';
9
- import type { Message as MessageType, LearningMode, UploadedFile, FileType, SpaceType } from '../App';
10
- import { toast } from 'sonner';
11
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu';
12
-
13
- interface ChatAreaProps {
14
- messages: MessageType[];
15
- onSendMessage: (content: string) => void;
16
-
17
- uploadedFiles: UploadedFile[];
18
- onFileUpload: (files: File[]) => void;
19
- onRemoveFile: (index: number) => void;
20
- onFileTypeChange: (index: number, type: FileType) => void;
21
-
22
- // ✅ feedback 需要 userId
23
- userId?: string;
24
-
25
- // ✅ 由 App.tsx 传入 currentDocTypeForChat
26
- docType?: string;
27
-
28
- memoryProgress: number;
29
- isLoggedIn: boolean;
30
- learningMode: LearningMode;
31
- onClearConversation: () => void;
32
- onLearningModeChange: (mode: LearningMode) => void;
33
- spaceType: SpaceType;
34
- }
35
-
36
- export function ChatArea({
37
- messages,
38
- onSendMessage,
39
- uploadedFiles,
40
- onFileUpload,
41
- onRemoveFile,
42
- onFileTypeChange,
43
- userId,
44
- docType = 'Other',
45
- memoryProgress,
46
- isLoggedIn,
47
- learningMode,
48
- onClearConversation,
49
- onLearningModeChange,
50
- spaceType,
51
- }: ChatAreaProps) {
52
- const [input, setInput] = useState('');
53
- const [isTyping, setIsTyping] = useState(false);
54
- const [showScrollButton, setShowScrollButton] = useState(false);
55
- const messagesEndRef = useRef<HTMLDivElement>(null);
56
- const scrollContainerRef = useRef<HTMLDivElement>(null);
57
-
58
- const lastUserMessageContent = useMemo(() => {
59
- for (let i = messages.length - 1; i >= 0; i--) {
60
- if (messages[i].role === 'user' && messages[i].content?.trim()) return messages[i].content;
61
- }
62
- return '';
63
- }, [messages]);
64
-
65
- const scrollToBottom = () => {
66
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
67
- };
68
-
69
- useEffect(() => {
70
- scrollToBottom();
71
- }, [messages]);
72
-
73
- useEffect(() => {
74
- const handleScroll = () => {
75
- if (!scrollContainerRef.current) return;
76
- const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
77
- setShowScrollButton(scrollHeight - scrollTop - clientHeight > 100);
78
- };
79
-
80
- const container = scrollContainerRef.current;
81
- container?.addEventListener('scroll', handleScroll);
82
- return () => container?.removeEventListener('scroll', handleScroll);
83
- }, []);
84
-
85
- const handleSubmit = (e: React.FormEvent) => {
86
- e.preventDefault();
87
- if (!input.trim() || !isLoggedIn) return;
88
-
89
- onSendMessage(input);
90
- setInput('');
91
- setIsTyping(true);
92
- setTimeout(() => setIsTyping(false), 1200);
93
- };
94
-
95
- const handleKeyDown = (e: React.KeyboardEvent) => {
96
- if (e.key === 'Enter' && !e.shiftKey) {
97
- e.preventDefault();
98
- handleSubmit(e);
99
- }
100
- };
101
-
102
- const modeLabels: Record<LearningMode, string> = {
103
- concept: 'Concept Explainer',
104
- socratic: 'Socratic Tutor',
105
- exam: 'Exam Prep',
106
- assignment: 'Assignment Helper',
107
- summary: 'Quick Summary',
108
- };
109
-
110
- const handleClearClick = () => {
111
- if (messages.length <= 1) {
112
- toast.info('No conversation to clear');
113
- return;
114
- }
115
- if (window.confirm('Are you sure you want to clear the conversation? This cannot be undone.')) {
116
- onClearConversation();
117
- toast.success('Conversation cleared');
118
- }
119
- };
120
-
121
- const handleShareClick = () => {
122
- if (messages.length <= 1) {
123
- toast.info('No conversation to share');
124
- return;
125
- }
126
- const conversationText = messages
127
- .map((msg) => `${msg.role === 'user' ? 'You' : 'Clare'}: ${msg.content}`)
128
- .join('\n\n');
129
-
130
- navigator.clipboard
131
- .writeText(conversationText)
132
- .then(() => toast.success('Conversation copied to clipboard!'))
133
- .catch(() => toast.error('Failed to copy conversation'));
134
- };
135
-
136
- return (
137
- <div className="flex flex-col h-full">
138
- <div className="flex-1 relative border-b-2 border-border">
139
- {messages.length > 1 && (
140
- <div className="absolute top-4 right-12 z-10 flex gap-2">
141
- <Button
142
- variant="ghost"
143
- size="sm"
144
- onClick={handleShareClick}
145
- disabled={!isLoggedIn}
146
- className="gap-2 bg-background/95 backdrop-blur-sm shadow-sm hover:shadow-md transition-all group"
147
- >
148
- <Share2 className="h-4 w-4" />
149
- <span className="hidden group-hover:inline">Share</span>
150
- </Button>
151
- <Button
152
- variant="ghost"
153
- size="sm"
154
- onClick={handleClearClick}
155
- disabled={!isLoggedIn}
156
- className="gap-2 bg-background/95 backdrop-blur-sm shadow-sm hover:shadow-md transition-all group"
157
- >
158
- <Trash2 className="h-4 w-4" />
159
- <span className="hidden group-hover:inline">Clear</span>
160
- </Button>
161
- </div>
162
- )}
163
-
164
- <div ref={scrollContainerRef} className="h-full max-h-[600px] overflow-y-auto px-4 py-6 pb-36">
165
- <div className="max-w-4xl mx-auto space-y-6">
166
- {messages.map((m) => (
167
- <Message
168
- key={m.id}
169
- message={m}
170
- showSenderInfo={spaceType === 'group'}
171
- userId={userId}
172
- isLoggedIn={isLoggedIn}
173
- learningMode={learningMode}
174
- docType={docType}
175
- lastUserText={lastUserMessageContent}
176
- />
177
- ))}
178
-
179
- {isTyping && (
180
- <div className="flex gap-3">
181
- <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center flex-shrink-0">
182
- <span className="text-white text-sm">C</span>
183
- </div>
184
- <div className="bg-muted rounded-2xl px-4 py-3">
185
- <div className="flex gap-1">
186
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '0ms' }} />
187
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '150ms' }} />
188
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '300ms' }} />
189
- </div>
190
- </div>
191
- </div>
192
- )}
193
-
194
- <div ref={messagesEndRef} />
195
- </div>
196
- </div>
197
-
198
- {showScrollButton && (
199
- <div className="absolute bottom-24 left-1/2 -translate-x-1/2 z-20">
200
- <Button
201
- variant="secondary"
202
- size="icon"
203
- className="rounded-full shadow-lg hover:shadow-xl transition-shadow bg-background"
204
- onClick={scrollToBottom}
205
- >
206
- <ArrowDown className="h-4 w-4" />
207
- </Button>
208
- </div>
209
- )}
210
-
211
- <div className="absolute bottom-0 left-0 right-0 bg-background/95 backdrop-blur-sm z-10">
212
- <div className="max-w-4xl mx-auto px-4 py-4">
213
- <form onSubmit={handleSubmit}>
214
- <div className="relative">
215
- <DropdownMenu>
216
- <DropdownMenuTrigger asChild>
217
- <Button
218
- variant="ghost"
219
- size="sm"
220
- className="absolute bottom-3 left-2 gap-1.5 h-8 px-2 text-xs z-10 hover:bg-muted/50"
221
- disabled={!isLoggedIn}
222
- type="button"
223
- >
224
- <span>{modeLabels[learningMode]}</span>
225
- <svg className="h-3 w-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
226
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
227
- </svg>
228
- </Button>
229
- </DropdownMenuTrigger>
230
- <DropdownMenuContent align="start" className="w-56">
231
- {(['concept', 'socratic', 'exam', 'assignment', 'summary'] as LearningMode[]).map((mode) => (
232
- <DropdownMenuItem
233
- key={mode}
234
- onClick={() => onLearningModeChange(mode)}
235
- className={learningMode === mode ? 'bg-accent' : ''}
236
- >
237
- <span className="font-medium">{modeLabels[mode]}</span>
238
- </DropdownMenuItem>
239
- ))}
240
- </DropdownMenuContent>
241
- </DropdownMenu>
242
-
243
- <Textarea
244
- value={input}
245
- onChange={(e) => setInput(e.target.value)}
246
- onKeyDown={handleKeyDown}
247
- placeholder={
248
- isLoggedIn
249
- ? spaceType === 'group'
250
- ? 'Type a message... (mention @Clare to get AI assistance)'
251
- : 'Ask Clare anything about the course...'
252
- : 'Please log in on the right to start chatting...'
253
- }
254
- disabled={!isLoggedIn}
255
- className="min-h-[80px] pl-4 pr-12 resize-none bg-background border-2 border-border"
256
- />
257
- <Button type="submit" size="icon" disabled={!input.trim() || !isLoggedIn} className="absolute bottom-2 right-2 rounded-full">
258
- <Send className="h-4 w-4" />
259
- </Button>
260
- </div>
261
- </form>
262
- </div>
263
- </div>
264
- </div>
265
-
266
- <div className="bg-card">
267
- <div className="max-w-4xl mx-auto px-4 py-4">
268
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
269
- <FileUploadArea
270
- uploadedFiles={uploadedFiles}
271
- onFileUpload={onFileUpload}
272
- onRemoveFile={onRemoveFile}
273
- onFileTypeChange={onFileTypeChange}
274
- disabled={!isLoggedIn}
275
- />
276
- <MemoryLine progress={memoryProgress} />
277
- </div>
278
- </div>
279
- </div>
280
- </div>
281
- );
282
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/FileUploadArea.tsx DELETED
@@ -1,300 +0,0 @@
1
- import React, { useRef, useState } from 'react';
2
- import { Button } from './ui/button';
3
- import { Upload, File, X, FileText, Presentation, CloudUpload } from 'lucide-react';
4
- import { Card } from './ui/card';
5
- import { Badge } from './ui/badge';
6
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
7
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog';
8
- import type { UploadedFile, FileType } from '../App';
9
-
10
- interface FileUploadAreaProps {
11
- uploadedFiles: UploadedFile[];
12
- onFileUpload: (files: File[]) => void;
13
- onRemoveFile: (index: number) => void;
14
- onFileTypeChange: (index: number, type: FileType) => void;
15
-
16
- // ✅ 新增:真正触发后端上传(App 里实现)
17
- onUploadFile?: (index: number) => void;
18
- onUploadAll?: () => void;
19
-
20
- disabled?: boolean;
21
- }
22
-
23
- interface PendingFile {
24
- file: File;
25
- type: FileType;
26
- }
27
-
28
- export function FileUploadArea({
29
- uploadedFiles,
30
- onFileUpload,
31
- onRemoveFile,
32
- onFileTypeChange,
33
- onUploadFile,
34
- onUploadAll,
35
- disabled = false,
36
- }: FileUploadAreaProps) {
37
- const [isDragging, setIsDragging] = useState(false);
38
- const fileInputRef = useRef<HTMLInputElement>(null);
39
- const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
40
- const [showTypeDialog, setShowTypeDialog] = useState(false);
41
-
42
- const handleDragOver = (e: React.DragEvent) => {
43
- e.preventDefault();
44
- if (!disabled) setIsDragging(true);
45
- };
46
-
47
- const handleDragLeave = () => {
48
- setIsDragging(false);
49
- };
50
-
51
- const handleDrop = (e: React.DragEvent) => {
52
- e.preventDefault();
53
- setIsDragging(false);
54
- if (disabled) return;
55
-
56
- const files = Array.from(e.dataTransfer.files).filter((file) =>
57
- ['.pdf', '.docx', '.pptx'].some((ext) => file.name.toLowerCase().endsWith(ext))
58
- );
59
-
60
- if (files.length > 0) {
61
- setPendingFiles(files.map((file) => ({ file, type: 'other' as FileType })));
62
- setShowTypeDialog(true);
63
- }
64
- };
65
-
66
- const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
67
- const files = Array.from(e.target.files || []);
68
- if (files.length > 0) {
69
- setPendingFiles(files.map((file) => ({ file, type: 'other' as FileType })));
70
- setShowTypeDialog(true);
71
- }
72
- e.target.value = '';
73
- };
74
-
75
- const handleConfirmUpload = () => {
76
- // 这里只“入库”,不触发后端上传(符合你现在的逻辑)
77
- onFileUpload(pendingFiles.map((pf) => pf.file));
78
-
79
- // 把用户在弹窗里选的 type 同步到父组件列表(通过 index 偏移)
80
- const startIndex = uploadedFiles.length;
81
- pendingFiles.forEach((pf, idx) => {
82
- setTimeout(() => {
83
- onFileTypeChange(startIndex + idx, pf.type);
84
- }, 0);
85
- });
86
-
87
- setPendingFiles([]);
88
- setShowTypeDialog(false);
89
- };
90
-
91
- const handleCancelUpload = () => {
92
- setPendingFiles([]);
93
- setShowTypeDialog(false);
94
- };
95
-
96
- const handlePendingFileTypeChange = (index: number, type: FileType) => {
97
- setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
98
- };
99
-
100
- const getFileIcon = (filename: string) => {
101
- const lower = filename.toLowerCase();
102
- if (lower.endsWith('.pdf')) return FileText;
103
- if (lower.endsWith('.docx')) return File;
104
- if (lower.endsWith('.pptx')) return Presentation;
105
- return File;
106
- };
107
-
108
- const formatFileSize = (bytes: number) => {
109
- if (bytes < 1024) return bytes + ' B';
110
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
111
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
112
- };
113
-
114
- const hasPendingUploads = uploadedFiles.some((f) => !f.uploaded);
115
-
116
- return (
117
- <Card className="p-4 space-y-3">
118
- <div className="flex items-center justify-between">
119
- <div className="flex items-center gap-2">
120
- <h4 className="text-sm">Course Materials</h4>
121
- {uploadedFiles.length > 0 && <Badge variant="secondary">{uploadedFiles.length} file(s)</Badge>}
122
- </div>
123
-
124
- {/* ✅ Upload All Pending(只在存在 pending 且提供了回调时显示) */}
125
- {uploadedFiles.length > 0 && hasPendingUploads && !!onUploadAll && (
126
- <Button
127
- variant="outline"
128
- size="sm"
129
- className="h-8 text-xs gap-2"
130
- disabled={disabled}
131
- onClick={(e) => {
132
- e.preventDefault();
133
- e.stopPropagation();
134
- onUploadAll();
135
- }}
136
- title="Upload all pending files"
137
- >
138
- <CloudUpload className="h-4 w-4" />
139
- Upload All
140
- </Button>
141
- )}
142
- </div>
143
-
144
- {/* Upload Area */}
145
- <div
146
- onDragOver={handleDragOver}
147
- onDragLeave={handleDragLeave}
148
- onDrop={handleDrop}
149
- className={`
150
- border-2 border-dashed rounded-lg p-4 text-center transition-colors
151
- ${isDragging ? 'border-primary bg-accent' : 'border-border'}
152
- ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
153
- `}
154
- onClick={() => !disabled && fileInputRef.current?.click()}
155
- >
156
- <Upload className="h-6 w-6 mx-auto mb-2 text-muted-foreground" />
157
- <p className="text-sm text-muted-foreground mb-1">{disabled ? 'Please log in to upload' : 'Drop files or click to upload'}</p>
158
- <p className="text-xs text-muted-foreground">.pdf, .docx, .pptx</p>
159
- <input
160
- ref={fileInputRef}
161
- type="file"
162
- multiple
163
- accept=".pdf,.docx,.pptx"
164
- onChange={handleFileSelect}
165
- className="hidden"
166
- disabled={disabled}
167
- />
168
- </div>
169
-
170
- {/* Uploaded Files List */}
171
- {uploadedFiles.length > 0 && (
172
- <div className="space-y-3 max-h-64 overflow-y-auto">
173
- {uploadedFiles.map((uploadedFile, index) => {
174
- const Icon = getFileIcon(uploadedFile.file.name);
175
- const isUploaded = !!uploadedFile.uploaded;
176
-
177
- return (
178
- <div key={index} className="p-3 bg-muted rounded-md space-y-2">
179
- <div className="flex items-center gap-2 group">
180
- <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
181
- <div className="flex-1 min-w-0">
182
- <p className="text-sm truncate">{uploadedFile.file.name}</p>
183
- <p className="text-xs text-muted-foreground">{formatFileSize(uploadedFile.file.size)}</p>
184
- </div>
185
-
186
- {/* ✅ 单文件 Upload(仅未上传时显示 & 必须有回调) */}
187
- {!isUploaded && !!onUploadFile && (
188
- <Button
189
- variant="secondary"
190
- size="sm"
191
- className="h-7 text-xs px-2"
192
- disabled={disabled}
193
- onClick={(e) => {
194
- e.preventDefault();
195
- e.stopPropagation();
196
- onUploadFile(index);
197
- }}
198
- title="Upload this file to backend"
199
- >
200
- Upload
201
- </Button>
202
- )}
203
-
204
- {/* ✅ 已上传状态 */}
205
- {isUploaded && (
206
- <Badge variant="secondary" className="text-[10px]">
207
- Uploaded{typeof uploadedFile.uploadedChunks === 'number' ? ` (+${uploadedFile.uploadedChunks})` : ''}
208
- </Badge>
209
- )}
210
-
211
- <Button
212
- variant="ghost"
213
- size="icon"
214
- className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
215
- onClick={(e) => {
216
- e.stopPropagation();
217
- onRemoveFile(index);
218
- }}
219
- title="Remove"
220
- >
221
- <X className="h-3 w-3" />
222
- </Button>
223
- </div>
224
-
225
- <div className="space-y-1">
226
- <label className="text-xs text-muted-foreground">File Type</label>
227
- <Select value={uploadedFile.type} onValueChange={(value) => onFileTypeChange(index, value as FileType)}>
228
- <SelectTrigger className="h-8 text-xs">
229
- <SelectValue />
230
- </SelectTrigger>
231
- <SelectContent>
232
- <SelectItem value="syllabus">Syllabus</SelectItem>
233
- <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
234
- <SelectItem value="literature-review">Literature Review / Paper</SelectItem>
235
- <SelectItem value="other">Other Course Document</SelectItem>
236
- </SelectContent>
237
- </Select>
238
- </div>
239
- </div>
240
- );
241
- })}
242
- </div>
243
- )}
244
-
245
- {/* Type Selection Dialog */}
246
- {showTypeDialog && (
247
- <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
248
- <DialogContent className="sm:max-w-[425px]">
249
- <DialogHeader>
250
- <DialogTitle>Select File Types</DialogTitle>
251
- <DialogDescription>Please select the type for each file you are uploading.</DialogDescription>
252
- </DialogHeader>
253
-
254
- <div className="space-y-3 max-h-64 overflow-y-auto">
255
- {pendingFiles.map((pendingFile, index) => {
256
- const Icon = getFileIcon(pendingFile.file.name);
257
- return (
258
- <div key={index} className="p-3 bg-muted rounded-md space-y-2">
259
- <div className="flex items-center gap-2 group">
260
- <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
261
- <div className="flex-1 min-w-0">
262
- <p className="text-sm truncate">{pendingFile.file.name}</p>
263
- <p className="text-xs text-muted-foreground">{formatFileSize(pendingFile.file.size)}</p>
264
- </div>
265
- </div>
266
-
267
- <div className="space-y-1">
268
- <label className="text-xs text-muted-foreground">File Type</label>
269
- <Select
270
- value={pendingFile.type}
271
- onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
272
- >
273
- <SelectTrigger className="h-8 text-xs">
274
- <SelectValue />
275
- </SelectTrigger>
276
- <SelectContent>
277
- <SelectItem value="syllabus">Syllabus</SelectItem>
278
- <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
279
- <SelectItem value="literature-review">Literature Review / Paper</SelectItem>
280
- <SelectItem value="other">Other Course Document</SelectItem>
281
- </SelectContent>
282
- </Select>
283
- </div>
284
- </div>
285
- );
286
- })}
287
- </div>
288
-
289
- <DialogFooter>
290
- <Button variant="outline" onClick={handleCancelUpload}>
291
- Cancel
292
- </Button>
293
- <Button onClick={handleConfirmUpload}>Upload</Button>
294
- </DialogFooter>
295
- </DialogContent>
296
- </Dialog>
297
- )}
298
- </Card>
299
- );
300
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/FloatingActionButtons.tsx DELETED
@@ -1,111 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Button } from './ui/button';
3
- import { Download, ClipboardList, Sparkles } from 'lucide-react';
4
- import { toast } from 'sonner@2.0.3';
5
- import type { User } from '../App';
6
-
7
- interface FloatingActionButtonsProps {
8
- user: User | null;
9
- isLoggedIn: boolean;
10
- onOpenPanel: () => void;
11
- onExport: () => void;
12
- onQuiz: () => void;
13
- onSummary: () => void;
14
- }
15
-
16
- export function FloatingActionButtons({
17
- user,
18
- isLoggedIn,
19
- onOpenPanel,
20
- onExport,
21
- onQuiz,
22
- onSummary,
23
- }: FloatingActionButtonsProps) {
24
- const [hoveredButton, setHoveredButton] = useState<string | null>(null);
25
-
26
- const handleAction = (action: () => void, actionName: string, shouldOpenPanel: boolean = false) => {
27
- if (!isLoggedIn) {
28
- toast.error('Please log in to use this feature');
29
- return;
30
- }
31
- action();
32
- if (shouldOpenPanel) {
33
- onOpenPanel();
34
- }
35
- };
36
-
37
- const buttons = [
38
- {
39
- id: 'export',
40
- icon: Download,
41
- label: 'Export Conversation',
42
- action: onExport,
43
- openPanel: true, // Open panel for export
44
- },
45
- {
46
- id: 'quiz',
47
- icon: ClipboardList,
48
- label: "Let's Try (Micro-Quiz)",
49
- action: onQuiz,
50
- openPanel: false, // Don't open panel for quiz
51
- },
52
- {
53
- id: 'summary',
54
- icon: Sparkles,
55
- label: 'Summarization',
56
- action: onSummary,
57
- openPanel: true, // Open panel for summary
58
- },
59
- ];
60
-
61
- return (
62
- <div className="fixed right-4 bottom-[28rem] z-40 flex flex-col gap-2">
63
- {buttons.map((button, index) => {
64
- const Icon = button.icon;
65
- const isHovered = hoveredButton === button.id;
66
-
67
- return (
68
- <div
69
- key={button.id}
70
- className="relative group"
71
- onMouseEnter={() => setHoveredButton(button.id)}
72
- onMouseLeave={() => setHoveredButton(null)}
73
- >
74
- {/* Tooltip */}
75
- <div
76
- className={`
77
- absolute right-full mr-3 top-1/2 -translate-y-1/2
78
- px-3 py-2 rounded-lg bg-popover border border-border
79
- whitespace-nowrap text-sm shadow-lg
80
- transition-all duration-200
81
- ${isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-2 pointer-events-none'}
82
- `}
83
- >
84
- {button.label}
85
- </div>
86
-
87
- {/* Floating Button */}
88
- <Button
89
- size="icon"
90
- className={`
91
- h-6 w-6 rounded-full shadow-md opacity-60 hover:opacity-100
92
- transition-all duration-200
93
- ${isLoggedIn
94
- ? 'bg-primary hover:bg-primary/90 text-primary-foreground'
95
- : 'bg-muted hover:bg-muted/90 text-muted-foreground'
96
- }
97
- ${isHovered ? 'scale-110' : 'scale-100'}
98
- `}
99
- onClick={() => handleAction(button.action, button.label, button.openPanel)}
100
- style={{
101
- animationDelay: `${index * 100}ms`,
102
- }}
103
- >
104
- <Icon className="h-3 w-3" />
105
- </Button>
106
- </div>
107
- );
108
- })}
109
- </div>
110
- );
111
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/GroupMembers.tsx DELETED
@@ -1,57 +0,0 @@
1
- import React from 'react';
2
- import { Users, Bot } from 'lucide-react';
3
- import { Badge } from './ui/badge';
4
- import type { GroupMember } from '../App';
5
-
6
- interface GroupMembersProps {
7
- members: GroupMember[];
8
- }
9
-
10
- export function GroupMembers({ members }: GroupMembersProps) {
11
- return (
12
- <div className="space-y-3">
13
- <div className="flex items-center gap-2">
14
- <Users className="h-4 w-4 text-muted-foreground" />
15
- <h3 className="text-sm">Group Members ({members.length})</h3>
16
- </div>
17
-
18
- <div className="space-y-2">
19
- {members.map((member) => (
20
- <div
21
- key={member.id}
22
- className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors"
23
- >
24
- {/* Avatar */}
25
- <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
26
- member.isAI
27
- ? 'bg-gradient-to-br from-purple-500 to-blue-500'
28
- : 'bg-muted'
29
- }`}>
30
- {member.isAI ? (
31
- <Bot className="h-4 w-4 text-white" />
32
- ) : (
33
- <span className="text-sm">
34
- {member.name.split(' ').map(n => n[0]).join('').toUpperCase()}
35
- </span>
36
- )}
37
- </div>
38
-
39
- {/* Member Info */}
40
- <div className="flex-1 min-w-0">
41
- <div className="flex items-center gap-2">
42
- <p className="text-sm truncate">{member.name}</p>
43
- {member.isAI && (
44
- <Badge variant="secondary" className="text-xs">AI</Badge>
45
- )}
46
- </div>
47
- <p className="text-xs text-muted-foreground truncate">{member.email}</p>
48
- </div>
49
-
50
- {/* Online Status */}
51
- <div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" title="Online" />
52
- </div>
53
- ))}
54
- </div>
55
- </div>
56
- );
57
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/Header.tsx DELETED
@@ -1,79 +0,0 @@
1
- import React from 'react';
2
- import { Menu, User, BookOpen, Moon, Sun } from 'lucide-react';
3
- import { Button } from './ui/button';
4
- import { Badge } from './ui/badge';
5
- import type { User as UserType } from '../App';
6
-
7
- interface HeaderProps {
8
- user: UserType | null;
9
- onMenuClick: () => void;
10
- onUserClick: () => void;
11
- isDarkMode: boolean;
12
- onToggleDarkMode: () => void;
13
- }
14
-
15
- export function Header({ user, onMenuClick, onUserClick, isDarkMode, onToggleDarkMode }: HeaderProps) {
16
- return (
17
- <header className="h-16 border-b border-border bg-card px-4 lg:px-6 flex items-center justify-between sticky top-0 z-[60]">
18
- <div className="flex items-center gap-4">
19
- <Button
20
- variant="ghost"
21
- size="icon"
22
- className="lg:hidden"
23
- onClick={onMenuClick}
24
- >
25
- <Menu className="h-5 w-5" />
26
- </Button>
27
-
28
- <div className="flex items-center gap-3">
29
- <div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center">
30
- <BookOpen className="h-6 w-6 text-white" />
31
- </div>
32
- <div>
33
- <h1 className="text-lg sm:text-xl tracking-tight">
34
- Clare <span className="text-sm font-bold text-muted-foreground hidden sm:inline ml-2">Your Personalized AI Tutor</span>
35
- </h1>
36
- <p className="text-xs text-muted-foreground hidden sm:block">
37
- Personalized guidance, review, and intelligent reinforcement
38
- </p>
39
- </div>
40
- </div>
41
- </div>
42
-
43
- <div className="flex items-center gap-2">
44
- <Badge variant="secondary" className="hidden md:flex">
45
- Module 10 – Responsible AI
46
- </Badge>
47
-
48
- <Button
49
- variant="ghost"
50
- size="icon"
51
- onClick={onToggleDarkMode}
52
- aria-label="Toggle dark mode"
53
- >
54
- {isDarkMode ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
55
- </Button>
56
-
57
- {user ? (
58
- <Button
59
- variant="outline"
60
- className="gap-2"
61
- onClick={onUserClick}
62
- >
63
- <User className="h-4 w-4" />
64
- <span className="hidden sm:inline">{user.name}</span>
65
- </Button>
66
- ) : (
67
- <Button
68
- variant="default"
69
- className="gap-2 lg:hidden"
70
- onClick={onUserClick}
71
- >
72
- <User className="h-4 w-4" />
73
- <span>Login</span>
74
- </Button>
75
- )}
76
- </div>
77
- </header>
78
- );
79
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/LearningModeSelector.tsx DELETED
@@ -1,94 +0,0 @@
1
- import React from 'react';
2
- import { Card } from './ui/card';
3
- import {
4
- Lightbulb,
5
- MessageCircleQuestion,
6
- GraduationCap,
7
- FileEdit,
8
- Zap
9
- } from 'lucide-react';
10
- import type { LearningMode } from '../App';
11
-
12
- interface ModeSelectorProps {
13
- selectedMode: LearningMode;
14
- onModeChange: (mode: LearningMode) => void;
15
- }
16
-
17
- const modes = [
18
- {
19
- id: 'concept' as LearningMode,
20
- icon: Lightbulb,
21
- title: 'Concept Explainer',
22
- description: 'Break down complex topics',
23
- color: 'from-blue-500 to-blue-600',
24
- },
25
- {
26
- id: 'socratic' as LearningMode,
27
- icon: MessageCircleQuestion,
28
- title: 'Socratic Tutor',
29
- description: 'Learn through questions',
30
- color: 'from-purple-500 to-purple-600',
31
- },
32
- {
33
- id: 'exam' as LearningMode,
34
- icon: GraduationCap,
35
- title: 'Exam Prep/Quiz',
36
- description: 'Test your knowledge',
37
- color: 'from-green-500 to-green-600',
38
- },
39
- {
40
- id: 'assignment' as LearningMode,
41
- icon: FileEdit,
42
- title: 'Assignment Helper',
43
- description: 'Get homework guidance',
44
- color: 'from-orange-500 to-orange-600',
45
- },
46
- {
47
- id: 'summary' as LearningMode,
48
- icon: Zap,
49
- title: 'Quick Summary',
50
- description: 'Fast key points review',
51
- color: 'from-pink-500 to-pink-600',
52
- },
53
- ];
54
-
55
- export function LearningModeSelector({ selectedMode, onModeChange }: ModeSelectorProps) {
56
- return (
57
- <div className="space-y-2">
58
- {modes.map((mode) => {
59
- const Icon = mode.icon;
60
- const isSelected = selectedMode === mode.id;
61
-
62
- return (
63
- <Card
64
- key={mode.id}
65
- className={`
66
- p-3 cursor-pointer transition-all duration-200
67
- ${isSelected
68
- ? 'border-primary bg-accent shadow-sm'
69
- : 'hover:border-primary/50 hover:shadow-sm'
70
- }
71
- `}
72
- onClick={() => onModeChange(mode.id)}
73
- >
74
- <div className="flex items-start gap-3">
75
- <div className={`
76
- w-10 h-10 rounded-lg bg-gradient-to-br ${mode.color}
77
- flex items-center justify-center flex-shrink-0
78
- `}>
79
- <Icon className="h-5 w-5 text-white" />
80
- </div>
81
- <div className="flex-1 min-w-0">
82
- <h4 className="text-sm mb-1">{mode.title}</h4>
83
- <p className="text-xs text-muted-foreground">{mode.description}</p>
84
- </div>
85
- {isSelected && (
86
- <div className="w-2 h-2 rounded-full bg-primary flex-shrink-0 mt-2" />
87
- )}
88
- </div>
89
- </Card>
90
- );
91
- })}
92
- </div>
93
- );
94
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/LeftSidebar.tsx DELETED
@@ -1,121 +0,0 @@
1
- import React from 'react';
2
- import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
3
- import { LearningModeSelector } from './LearningModeSelector';
4
- import { UserGuide } from './UserGuide';
5
- import { RadioGroup, RadioGroupItem } from './ui/radio-group';
6
- import { Label } from './ui/label';
7
- import { Button } from './ui/button';
8
- import { RotateCcw, Settings, User, Users } from 'lucide-react';
9
- import { Separator } from './ui/separator';
10
- import { GroupMembers } from './GroupMembers';
11
- import type { LearningMode, Language, SpaceType, GroupMember } from '../App';
12
-
13
- interface LeftSidebarProps {
14
- learningMode: LearningMode;
15
- language: Language;
16
- onLearningModeChange: (mode: LearningMode) => void;
17
- onLanguageChange: (lang: Language) => void;
18
- spaceType: SpaceType;
19
- onSpaceTypeChange: (type: SpaceType) => void;
20
- groupMembers: GroupMember[];
21
- }
22
-
23
- export function LeftSidebar({
24
- learningMode,
25
- language,
26
- onLearningModeChange,
27
- onLanguageChange,
28
- spaceType,
29
- onSpaceTypeChange,
30
- groupMembers,
31
- }: LeftSidebarProps) {
32
- return (
33
- <div className="flex-1 overflow-auto">
34
- {/* Space Selector */}
35
- <div className="p-4 border-b border-border space-y-3">
36
- <Label>Workspace</Label>
37
- <RadioGroup value={spaceType} onValueChange={(value) => onSpaceTypeChange(value as SpaceType)}>
38
- <div className="flex items-center space-x-2 p-2 rounded-lg hover:bg-muted/50 transition-colors">
39
- <RadioGroupItem value="individual" id="individual" />
40
- <Label htmlFor="individual" className="cursor-pointer flex items-center gap-2 flex-1">
41
- <User className="h-4 w-4" />
42
- Individual Space
43
- </Label>
44
- </div>
45
- <div className="flex items-center space-x-2 p-2 rounded-lg hover:bg-muted/50 transition-colors">
46
- <RadioGroupItem value="group" id="group" />
47
- <Label htmlFor="group" className="cursor-pointer flex items-center gap-2 flex-1">
48
- <Users className="h-4 w-4" />
49
- Group Space
50
- </Label>
51
- </div>
52
- </RadioGroup>
53
- </div>
54
-
55
- {/* Group Members - Only show in group mode */}
56
- {spaceType === 'group' && (
57
- <div className="p-4 border-b border-border">
58
- <GroupMembers members={groupMembers} />
59
- </div>
60
- )}
61
-
62
- <Tabs defaultValue="settings" className="h-full flex flex-col">
63
- <div className="px-4 pt-4">
64
- <TabsList className="grid w-full grid-cols-2">
65
- <TabsTrigger value="settings">Settings</TabsTrigger>
66
- <TabsTrigger value="guide">Guide</TabsTrigger>
67
- </TabsList>
68
- </div>
69
-
70
- <TabsContent value="settings" className="flex-1 mt-0 p-4 space-y-6">
71
- <div className="space-y-4">
72
- <div>
73
- <h3>Model Settings</h3>
74
- </div>
75
-
76
- <div className="space-y-2">
77
- <Label>Model</Label>
78
- <div className="px-3 py-2 bg-muted rounded-md">
79
- <code className="text-sm">gpt-4.1-mini</code>
80
- </div>
81
- </div>
82
-
83
- <div className="space-y-2">
84
- <Label>Language</Label>
85
- <RadioGroup value={language} onValueChange={(value) => onLanguageChange(value as Language)}>
86
- <div className="flex items-center space-x-2">
87
- <RadioGroupItem value="auto" id="auto" />
88
- <Label htmlFor="auto" className="cursor-pointer">Auto</Label>
89
- </div>
90
- <div className="flex items-center space-x-2">
91
- <RadioGroupItem value="en" id="en" />
92
- <Label htmlFor="en" className="cursor-pointer">English</Label>
93
- </div>
94
- <div className="flex items-center space-x-2">
95
- <RadioGroupItem value="zh" id="zh" />
96
- <Label htmlFor="zh" className="cursor-pointer">简体中文</Label>
97
- </div>
98
- </RadioGroup>
99
- </div>
100
- </div>
101
-
102
- <Separator />
103
-
104
- <div className="space-y-3">
105
- <Button variant="outline" className="w-full gap-2" disabled>
106
- <Settings className="h-4 w-4" />
107
- System Settings
108
- </Button>
109
- <p className="text-xs text-muted-foreground text-center">
110
- © 2025 Clare AI Teaching Assistant
111
- </p>
112
- </div>
113
- </TabsContent>
114
-
115
- <TabsContent value="guide" className="flex-1 mt-0 p-4">
116
- <UserGuide />
117
- </TabsContent>
118
- </Tabs>
119
- </div>
120
- );
121
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/MemoryLine.tsx DELETED
@@ -1,108 +0,0 @@
1
- import React from 'react';
2
- import { Card } from './ui/card';
3
- import { Button } from './ui/button';
4
- import { Progress } from './ui/progress';
5
- import { Brain, Calendar, Download, Info } from 'lucide-react';
6
- import {
7
- Tooltip,
8
- TooltipContent,
9
- TooltipProvider,
10
- TooltipTrigger,
11
- } from './ui/tooltip';
12
- import { Badge } from './ui/badge';
13
-
14
- interface MemoryLineProps {
15
- progress: number;
16
- }
17
-
18
- export function MemoryLine({ progress }: MemoryLineProps) {
19
- const nextReview = progress < 25 ? 'T+7' : progress < 50 ? 'T+14' : progress < 75 ? 'T+30' : 'Complete';
20
-
21
- return (
22
- <Card className="p-4 space-y-3">
23
- <div className="flex items-center justify-between">
24
- <div className="flex items-center gap-2">
25
- <Brain className="h-4 w-4 text-purple-500" />
26
- <h4 className="text-sm">Memory Line</h4>
27
- <TooltipProvider>
28
- <Tooltip>
29
- <TooltipTrigger asChild>
30
- <Button variant="ghost" size="icon" className="h-5 w-5">
31
- <Info className="h-3 w-3" />
32
- </Button>
33
- </TooltipTrigger>
34
- <TooltipContent className="max-w-xs">
35
- <p className="text-sm">
36
- Track your learning retention using spaced repetition. Regular reviews help solidify understanding!
37
- </p>
38
- </TooltipContent>
39
- </Tooltip>
40
- </TooltipProvider>
41
- </div>
42
- <Badge variant="outline" className="text-xs">
43
- {progress}%
44
- </Badge>
45
- </div>
46
-
47
- {/* Progress Bar with Milestones */}
48
- <div className="space-y-2">
49
- <div className="relative">
50
- <Progress value={progress} className="h-2" />
51
-
52
- {/* Milestone Markers */}
53
- <div className="absolute top-0 left-0 w-full h-2 flex justify-between pointer-events-none">
54
- {[0, 25, 50, 75, 100].map((milestone) => (
55
- <div
56
- key={milestone}
57
- className={`
58
- w-3 h-3 rounded-full border-2 -mt-0.5
59
- ${progress >= milestone
60
- ? 'bg-primary border-primary'
61
- : 'bg-background border-border'
62
- }
63
- `}
64
- />
65
- ))}
66
- </div>
67
- </div>
68
-
69
- {/* Milestone Labels */}
70
- <div className="flex justify-between text-xs text-muted-foreground">
71
- <span>T+0</span>
72
- <span>T+7</span>
73
- <span>T+14</span>
74
- <span>T+30</span>
75
- <span>Done</span>
76
- </div>
77
- </div>
78
-
79
- {/* Actions */}
80
- <div className="flex items-center gap-2">
81
- <Button variant="outline" size="sm" className="flex-1 gap-2">
82
- <Calendar className="h-3 w-3" />
83
- Next: {nextReview}
84
- </Button>
85
- <TooltipProvider>
86
- <Tooltip>
87
- <TooltipTrigger asChild>
88
- <Button variant="ghost" size="sm" className="gap-2">
89
- <Download className="h-3 w-3" />
90
- Report
91
- </Button>
92
- </TooltipTrigger>
93
- <TooltipContent>
94
- <p>Download your learning progress report</p>
95
- </TooltipContent>
96
- </Tooltip>
97
- </TooltipProvider>
98
- </div>
99
-
100
- {/* Review Explanation */}
101
- <div className="pt-2 border-t border-border">
102
- <p className="text-xs text-muted-foreground">
103
- Based on spaced repetition for optimal retention
104
- </p>
105
- </div>
106
- </Card>
107
- );
108
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/Message.tsx DELETED
@@ -1,100 +0,0 @@
1
- // web/src/components/Message.tsx
2
- import React from "react";
3
- import ReactMarkdown from "react-markdown";
4
- import remarkGfm from "remark-gfm";
5
-
6
- import type { Message as MessageType, LearningMode } from "../App";
7
-
8
- interface MessageProps {
9
- message: MessageType;
10
-
11
- // existing props you are already passing in ChatArea
12
- showSenderInfo?: boolean;
13
- userId?: string;
14
- isLoggedIn: boolean;
15
- learningMode: LearningMode;
16
- docType?: string;
17
- lastUserText?: string;
18
- }
19
-
20
- export function Message({
21
- message,
22
- showSenderInfo,
23
- userId,
24
- isLoggedIn,
25
- learningMode,
26
- docType,
27
- lastUserText,
28
- }: MessageProps) {
29
- const isUser = message.role === "user";
30
-
31
- // If you already have avatar / sender rendering logic, keep it.
32
- // The only critical change is content rendering below.
33
- return (
34
- <div className={`flex gap-3 ${isUser ? "justify-end" : "justify-start"}`}>
35
- {!isUser && (
36
- <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center flex-shrink-0">
37
- <span className="text-white text-sm">C</span>
38
- </div>
39
- )}
40
-
41
- <div className={`max-w-[85%] ${isUser ? "text-right" : "text-left"}`}>
42
- {/* Optional sender info (group mode) */}
43
- {showSenderInfo && message.sender && !isUser && (
44
- <div className="text-xs text-muted-foreground mb-1">
45
- {message.sender.name}
46
- </div>
47
- )}
48
-
49
- <div
50
- className={[
51
- "rounded-2xl px-4 py-3 border",
52
- isUser
53
- ? "bg-primary text-primary-foreground border-primary/20"
54
- : "bg-muted text-foreground border-border",
55
- ].join(" ")}
56
- >
57
- {/* ✅ THE FIX: render markdown instead of plain text */}
58
- <ReactMarkdown
59
- remarkPlugins={[remarkGfm]}
60
- className={[
61
- // prose improves markdown typography; max-w-none prevents narrow column
62
- "prose prose-sm max-w-none",
63
- // keep readable in chat bubble
64
- "prose-p:my-2 prose-li:my-1 prose-ul:my-2 prose-ol:my-2",
65
- // make headings not too large inside bubble
66
- "prose-h1:text-base prose-h2:text-base prose-h3:text-sm",
67
- // avoid code blocks overflowing
68
- "prose-pre:overflow-x-auto",
69
- // inherit bubble colors
70
- isUser ? "prose-invert" : "",
71
- ].join(" ")}
72
- >
73
- {message.content || ""}
74
- </ReactMarkdown>
75
-
76
- {/* If you already render references, keep your original block here */}
77
- {message.references && message.references.length > 0 && (
78
- <div className="mt-3 pt-3 border-t border-border/50 text-xs text-muted-foreground space-y-1">
79
- <div className="font-medium">References</div>
80
- {message.references.map((r, idx) => (
81
- <div key={idx} className="truncate">
82
- {r}
83
- </div>
84
- ))}
85
- </div>
86
- )}
87
-
88
- {/* If you already have feedback buttons inside Message, keep them here.
89
- Do not change logic—only keep UI. */}
90
- </div>
91
- </div>
92
-
93
- {isUser && (
94
- <div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
95
- <span className="text-primary-foreground text-sm">U</span>
96
- </div>
97
- )}
98
- </div>
99
- );
100
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/RightPanel.tsx DELETED
@@ -1,307 +0,0 @@
1
- // web/src/components/RightPanel.tsx
2
- import React, { useState } from 'react';
3
- import { Button } from './ui/button';
4
- import { Input } from './ui/input';
5
- import { Label } from './ui/label';
6
- import { Card } from './ui/card';
7
- import { Separator } from './ui/separator';
8
- import { Textarea } from './ui/textarea';
9
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
10
- import { LogIn, LogOut, FileText, MessageSquare, Download, ClipboardList, Sparkles } from 'lucide-react';
11
- import type { User } from '../App';
12
- import { toast } from 'sonner';
13
- import {
14
- Dialog,
15
- DialogContent,
16
- DialogDescription,
17
- DialogHeader,
18
- DialogTitle,
19
- DialogFooter,
20
- } from './ui/dialog';
21
-
22
- // ✅ Markdown 渲染
23
- import ReactMarkdown from 'react-markdown';
24
- import remarkGfm from 'remark-gfm';
25
-
26
- interface RightPanelProps {
27
- user: User | null;
28
- onLogin: (name: string, emailOrId: string) => void;
29
- onLogout: () => void;
30
- isLoggedIn: boolean;
31
- onClose?: () => void;
32
-
33
- exportResult: string;
34
- setExportResult: (result: string) => void;
35
- resultType: 'export' | 'quiz' | 'summary' | null;
36
- setResultType: (type: 'export' | 'quiz' | 'summary' | null) => void;
37
-
38
- onExport: () => void;
39
- onQuiz: () => void;
40
- onSummary: () => void;
41
- }
42
-
43
- export function RightPanel({
44
- user,
45
- onLogin,
46
- onLogout,
47
- isLoggedIn,
48
- exportResult,
49
- setExportResult,
50
- resultType,
51
- setResultType,
52
- onExport,
53
- onQuiz,
54
- onSummary,
55
- }: RightPanelProps) {
56
- const [showLoginForm, setShowLoginForm] = useState(false);
57
- const [name, setName] = useState('');
58
- const [emailOrId, setEmailOrId] = useState('');
59
-
60
- const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false);
61
- const [feedbackText, setFeedbackText] = useState('');
62
- const [feedbackCategory, setFeedbackCategory] = useState<'general' | 'bug' | 'feature'>('general');
63
-
64
- const handleLoginClick = () => {
65
- if (!name.trim() || !emailOrId.trim()) {
66
- toast.error('Please fill in all fields');
67
- return;
68
- }
69
- onLogin(name.trim(), emailOrId.trim());
70
- setShowLoginForm(false);
71
- setName('');
72
- setEmailOrId('');
73
- };
74
-
75
- const handleLogoutClick = () => {
76
- onLogout();
77
- setShowLoginForm(false);
78
- };
79
-
80
- const handleFeedbackSubmit = () => {
81
- if (!feedbackText.trim()) {
82
- toast.error('Please provide feedback text');
83
- return;
84
- }
85
- console.log('Feedback submitted:', feedbackText, feedbackCategory);
86
- setFeedbackDialogOpen(false);
87
- setFeedbackText('');
88
- toast.success('Feedback submitted!');
89
- };
90
-
91
- return (
92
- <div className="flex-1 overflow-auto p-4 space-y-4">
93
- {/* Account */}
94
- <Card className="p-4">
95
- {!isLoggedIn ? (
96
- <div className="space-y-4">
97
- <div className="flex flex-col items-center py-4">
98
- <h3 className="mb-2">Welcome to Clare!</h3>
99
- <p className="text-sm text-muted-foreground text-center mb-4">
100
- Log in to start your learning session
101
- </p>
102
- </div>
103
-
104
- {!showLoginForm ? (
105
- <Button onClick={() => setShowLoginForm(true)} className="w-full gap-2">
106
- <LogIn className="h-4 w-4" />
107
- Student Login
108
- </Button>
109
- ) : (
110
- <div className="space-y-3">
111
- <div className="space-y-2">
112
- <Label htmlFor="name">Name</Label>
113
- <Input
114
- id="name"
115
- value={name}
116
- onChange={(e) => setName(e.target.value)}
117
- placeholder="Enter your name"
118
- />
119
- </div>
120
- <div className="space-y-2">
121
- <Label htmlFor="emailOrId">Email / Student ID</Label>
122
- <Input
123
- id="emailOrId"
124
- value={emailOrId}
125
- onChange={(e) => setEmailOrId(e.target.value)}
126
- placeholder="Enter your email or ID"
127
- />
128
- </div>
129
- <div className="flex gap-2">
130
- <Button onClick={handleLoginClick} className="flex-1">
131
- Enter
132
- </Button>
133
- <Button variant="outline" onClick={() => setShowLoginForm(false)}>
134
- Cancel
135
- </Button>
136
- </div>
137
- </div>
138
- )}
139
- </div>
140
- ) : (
141
- <div className="space-y-4">
142
- <div className="flex items-center gap-3">
143
- <div className="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-white">
144
- {user?.name?.charAt(0).toUpperCase()}
145
- </div>
146
- <div className="flex-1 min-w-0">
147
- <h4 className="truncate">{user?.name}</h4>
148
- <p className="text-sm text-muted-foreground truncate">{user?.email}</p>
149
- <p className="text-xs text-muted-foreground truncate">user_id: {user?.user_id}</p>
150
- </div>
151
- </div>
152
-
153
- <Button variant="destructive" onClick={handleLogoutClick} className="w-full gap-2">
154
- <LogOut className="h-4 w-4" />
155
- Log Out
156
- </Button>
157
- </div>
158
- )}
159
- </Card>
160
-
161
- {/* Actions (3 buttons in one row) */}
162
- <Card className="p-4">
163
- <div className="flex items-center justify-between mb-3">
164
- <h3 className="text-base font-medium">Actions</h3>
165
- </div>
166
-
167
- {/* ✅ 关键点:
168
- - flex + flex-1 保证同宽
169
- - min-w-0 防止内容撑开导致换行
170
- - h-11 / rounded-xl 统一尺寸
171
- */}
172
- <div className="flex gap-2">
173
- <Button
174
- variant="outline"
175
- onClick={onExport}
176
- disabled={!isLoggedIn}
177
- title="Export"
178
- className="flex-1 min-w-0 h-11 px-0 rounded-xl flex items-center justify-center"
179
- >
180
- <Download className="h-4 w-4" />
181
- </Button>
182
-
183
- <Button
184
- variant="outline"
185
- onClick={onQuiz}
186
- disabled={!isLoggedIn}
187
- title="Quiz"
188
- className="flex-1 min-w-0 h-11 px-0 rounded-xl flex items-center justify-center"
189
- >
190
- <ClipboardList className="h-4 w-4" />
191
- </Button>
192
-
193
- <Button
194
- variant="outline"
195
- onClick={onSummary}
196
- disabled={!isLoggedIn}
197
- title="Summary"
198
- className="flex-1 min-w-0 h-11 px-0 rounded-xl flex items-center justify-center"
199
- >
200
- <Sparkles className="h-4 w-4" />
201
- </Button>
202
- </div>
203
- </Card>
204
-
205
- <Separator />
206
-
207
- {/* Results */}
208
- <div className="space-y-3">
209
- <h3>
210
- {resultType === 'export' && 'Exported Conversation'}
211
- {resultType === 'quiz' && 'Micro-Quiz'}
212
- {resultType === 'summary' && 'Summarization'}
213
- {!resultType && 'Results'}
214
- </h3>
215
-
216
- <Card className="p-4 min-h-[200px] bg-muted/30">
217
- {exportResult ? (
218
- <div className="space-y-3">
219
- <div className="flex items-center justify-between">
220
- <FileText className="h-4 w-4 text-muted-foreground" />
221
- <Button
222
- variant="ghost"
223
- size="sm"
224
- onClick={() => {
225
- navigator.clipboard.writeText(exportResult);
226
- toast.success('Copied to clipboard!');
227
- }}
228
- >
229
- Copy
230
- </Button>
231
- </div>
232
-
233
- {/* ✅ Markdown render (fix **bold**, lists, code blocks, etc.) */}
234
- <div className="text-sm text-foreground whitespace-pre-wrap">
235
- <ReactMarkdown remarkPlugins={[remarkGfm]}>
236
- {exportResult}
237
- </ReactMarkdown>
238
- </div>
239
- </div>
240
- ) : (
241
- <div className="flex items-center justify-center h-full text-sm text-muted-foreground text-left">
242
- Results (export / summary / quiz) will appear here after actions run
243
- </div>
244
- )}
245
- </Card>
246
- </div>
247
-
248
- <Separator />
249
-
250
- {/* Feedback */}
251
- <div className="space-y-3">
252
- <h3>Feedback</h3>
253
- <Button
254
- variant="outline"
255
- className="w-full justify-start gap-2"
256
- onClick={() => setFeedbackDialogOpen(true)}
257
- >
258
- <MessageSquare className="h-4 w-4" />
259
- Provide Feedback
260
- </Button>
261
- </div>
262
-
263
- <Dialog open={feedbackDialogOpen} onOpenChange={setFeedbackDialogOpen}>
264
- <DialogContent className="sm:max-w-[425px]">
265
- <DialogHeader>
266
- <DialogTitle>Provide Feedback</DialogTitle>
267
- <DialogDescription>Help us improve Clare by sharing your thoughts and suggestions.</DialogDescription>
268
- </DialogHeader>
269
-
270
- <div className="space-y-3">
271
- <div className="space-y-2">
272
- <Label htmlFor="feedbackCategory">Category</Label>
273
- <Select value={feedbackCategory} onValueChange={(v) => setFeedbackCategory(v as any)}>
274
- <SelectTrigger>
275
- <SelectValue placeholder="Select a category" />
276
- </SelectTrigger>
277
- <SelectContent>
278
- <SelectItem value="general">General Feedback</SelectItem>
279
- <SelectItem value="bug">Bug Report</SelectItem>
280
- <SelectItem value="feature">Feature Request</SelectItem>
281
- </SelectContent>
282
- </Select>
283
- </div>
284
-
285
- <div className="space-y-2">
286
- <Label htmlFor="feedbackText">Feedback</Label>
287
- <Textarea
288
- id="feedbackText"
289
- value={feedbackText}
290
- onChange={(e) => setFeedbackText(e.target.value)}
291
- placeholder="Enter your feedback here..."
292
- className="min-h-[100px]"
293
- />
294
- </div>
295
- </div>
296
-
297
- <DialogFooter>
298
- <Button variant="outline" onClick={() => setFeedbackDialogOpen(false)}>
299
- Cancel
300
- </Button>
301
- <Button onClick={handleFeedbackSubmit}>Submit</Button>
302
- </DialogFooter>
303
- </DialogContent>
304
- </Dialog>
305
- </div>
306
- );
307
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/UserGuide.tsx DELETED
@@ -1,151 +0,0 @@
1
- import React from 'react';
2
- import {
3
- Accordion,
4
- AccordionContent,
5
- AccordionItem,
6
- AccordionTrigger,
7
- } from './ui/accordion';
8
-
9
- export function UserGuide() {
10
- return (
11
- <div className="space-y-3">
12
- <h3 className="mb-4">User Guide</h3>
13
-
14
- <Accordion type="single" collapsible className="space-y-2">
15
- <AccordionItem value="getting-started" className="border rounded-lg px-4">
16
- <AccordionTrigger>Getting Started</AccordionTrigger>
17
- <AccordionContent className="text-sm text-muted-foreground space-y-2">
18
- <p>Welcome to Clare! To begin:</p>
19
- <ol className="list-decimal list-inside space-y-1 ml-2">
20
- <li>Log in using your student credentials</li>
21
- <li>Select your preferred learning mode</li>
22
- <li>Upload course materials (optional)</li>
23
- <li>Start asking questions!</li>
24
- </ol>
25
- </AccordionContent>
26
- </AccordionItem>
27
-
28
- <AccordionItem value="modes" className="border rounded-lg px-4">
29
- <AccordionTrigger>Learning Modes</AccordionTrigger>
30
- <AccordionContent className="text-sm text-muted-foreground space-y-2">
31
- <p><strong>Concept Explainer:</strong> Get detailed explanations of complex topics with examples.</p>
32
- <p><strong>Socratic Tutor:</strong> Learn through guided questions and discovery.</p>
33
- <p><strong>Exam Prep/Quiz:</strong> Test your knowledge with practice questions.</p>
34
- <p><strong>Assignment Helper:</strong> Get step-by-step guidance on homework.</p>
35
- <p><strong>Quick Summary:</strong> Receive concise summaries of key concepts.</p>
36
- </AccordionContent>
37
- </AccordionItem>
38
-
39
- <AccordionItem value="how-it-works" className="border rounded-lg px-4">
40
- <AccordionTrigger>How Clare Works</AccordionTrigger>
41
- <AccordionContent className="text-sm text-muted-foreground space-y-2">
42
- <p>Clare uses advanced AI to provide personalized tutoring:</p>
43
- <ul className="list-disc list-inside space-y-1 ml-2">
44
- <li>Analyzes your questions and learning style</li>
45
- <li>References uploaded course materials</li>
46
- <li>Adapts explanations to your level</li>
47
- <li>Tracks your learning progress over time</li>
48
- </ul>
49
- </AccordionContent>
50
- </AccordionItem>
51
-
52
- <AccordionItem value="memory-line" className="border rounded-lg px-4">
53
- <AccordionTrigger>Memory Line</AccordionTrigger>
54
- <AccordionContent className="text-sm text-muted-foreground space-y-2">
55
- <p>The Memory Line tracks your retention using spaced repetition:</p>
56
- <ul className="list-disc list-inside space-y-1 ml-2">
57
- <li><strong>T+0:</strong> Initial learning</li>
58
- <li><strong>T+7:</strong> First review (1 week)</li>
59
- <li><strong>T+14:</strong> Second review (2 weeks)</li>
60
- <li><strong>T+30:</strong> Final review (1 month)</li>
61
- </ul>
62
- <p>Regular reviews help solidify your understanding!</p>
63
- </AccordionContent>
64
- </AccordionItem>
65
-
66
- <AccordionItem value="progress" className="border rounded-lg px-4">
67
- <AccordionTrigger>Learning Progress Report</AccordionTrigger>
68
- <AccordionContent className="text-sm text-muted-foreground">
69
- <p>Your progress report shows:</p>
70
- <ul className="list-disc list-inside space-y-1 ml-2">
71
- <li>Topics covered and mastered</li>
72
- <li>Time spent learning</li>
73
- <li>Quiz scores and performance</li>
74
- <li>Recommended review topics</li>
75
- </ul>
76
- </AccordionContent>
77
- </AccordionItem>
78
-
79
- <AccordionItem value="files" className="border rounded-lg px-4">
80
- <AccordionTrigger>Using Your Files</AccordionTrigger>
81
- <AccordionContent className="text-sm text-muted-foreground">
82
- <p>Upload course materials to enhance Clare's responses:</p>
83
- <ul className="list-disc list-inside space-y-1 ml-2">
84
- <li>Supported formats: .docx, .pdf, .pptx</li>
85
- <li>Clare will reference your materials in answers</li>
86
- <li>Files are processed securely and privately</li>
87
- <li>You can remove files anytime</li>
88
- </ul>
89
- </AccordionContent>
90
- </AccordionItem>
91
-
92
- <AccordionItem value="quiz" className="border rounded-lg px-4">
93
- <AccordionTrigger>Micro-Quiz</AccordionTrigger>
94
- <AccordionContent className="text-sm text-muted-foreground">
95
- <p>Test your understanding with quick quizzes:</p>
96
- <ul className="list-disc list-inside space-y-1 ml-2">
97
- <li>Generated based on your conversation</li>
98
- <li>Multiple choice and short answer questions</li>
99
- <li>Instant feedback and explanations</li>
100
- <li>Track your quiz performance over time</li>
101
- </ul>
102
- </AccordionContent>
103
- </AccordionItem>
104
-
105
- <AccordionItem value="summarization" className="border rounded-lg px-4">
106
- <AccordionTrigger>Summarization</AccordionTrigger>
107
- <AccordionContent className="text-sm text-muted-foreground">
108
- <p>Get concise summaries of your learning session:</p>
109
- <ul className="list-disc list-inside space-y-1 ml-2">
110
- <li>Key concepts discussed</li>
111
- <li>Important takeaways</li>
112
- <li>Topics to review</li>
113
- <li>Next steps for learning</li>
114
- </ul>
115
- </AccordionContent>
116
- </AccordionItem>
117
-
118
- <AccordionItem value="export" className="border rounded-lg px-4">
119
- <AccordionTrigger>Export Conversation</AccordionTrigger>
120
- <AccordionContent className="text-sm text-muted-foreground">
121
- <p>Save your conversation for later review:</p>
122
- <ul className="list-disc list-inside space-y-1 ml-2">
123
- <li>Export as PDF or text file</li>
124
- <li>Includes all messages and references</li>
125
- <li>Perfect for study notes</li>
126
- <li>Share with study groups (optional)</li>
127
- </ul>
128
- </AccordionContent>
129
- </AccordionItem>
130
-
131
- <AccordionItem value="faq" className="border rounded-lg px-4">
132
- <AccordionTrigger>FAQ</AccordionTrigger>
133
- <AccordionContent className="text-sm text-muted-foreground space-y-3">
134
- <div>
135
- <p className="font-medium text-foreground">Is my data private?</p>
136
- <p>Yes! Your conversations and files are encrypted and never shared.</p>
137
- </div>
138
- <div>
139
- <p className="font-medium text-foreground">Can Clare do my homework?</p>
140
- <p>Clare is designed to help you learn, not do work for you. It provides guidance and explanations to help you understand concepts.</p>
141
- </div>
142
- <div>
143
- <p className="font-medium text-foreground">How accurate is Clare?</p>
144
- <p>Clare is highly accurate but should be used as a learning aid. Always verify important information with course materials.</p>
145
- </div>
146
- </AccordionContent>
147
- </AccordionItem>
148
- </Accordion>
149
- </div>
150
- );
151
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/figma/ImageWithFallback.tsx DELETED
@@ -1,27 +0,0 @@
1
- import React, { useState } from 'react'
2
-
3
- const ERROR_IMG_SRC =
4
- 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
5
-
6
- export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
7
- const [didError, setDidError] = useState(false)
8
-
9
- const handleError = () => {
10
- setDidError(true)
11
- }
12
-
13
- const { src, alt, style, className, ...rest } = props
14
-
15
- return didError ? (
16
- <div
17
- className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
18
- style={style}
19
- >
20
- <div className="flex items-center justify-center w-full h-full">
21
- <img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
22
- </div>
23
- </div>
24
- ) : (
25
- <img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
26
- )
27
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/accordion.tsx DELETED
@@ -1,66 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as AccordionPrimitive from "@radix-ui/react-accordion@1.2.3";
5
- import { ChevronDownIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/alert-dialog.tsx DELETED
@@ -1,157 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog@1.1.6";
5
-
6
- import { cn } from "./utils";
7
- import { buttonVariants } from "./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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/alert.tsx DELETED
@@ -1,66 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority@0.7.1";
3
-
4
- import { cn } from "./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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/aspect-ratio.tsx DELETED
@@ -1,11 +0,0 @@
1
- "use client";
2
-
3
- import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio@1.1.2";
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 };
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/avatar.tsx DELETED
@@ -1,53 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as AvatarPrimitive from "@radix-ui/react-avatar@1.1.3";
5
-
6
- import { cn } from "./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-10 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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/badge.tsx DELETED
@@ -1,46 +0,0 @@
1
- import * as React from "react";
2
- import { Slot } from "@radix-ui/react-slot@1.1.2";
3
- import { cva, type VariantProps } from "class-variance-authority@0.7.1";
4
-
5
- import { cn } from "./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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/breadcrumb.tsx DELETED
@@ -1,109 +0,0 @@
1
- import * as React from "react";
2
- import { Slot } from "@radix-ui/react-slot@1.1.2";
3
- import { ChevronRight, MoreHorizontal } from "lucide-react@0.487.0";
4
-
5
- import { cn } from "./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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/button.tsx DELETED
@@ -1,58 +0,0 @@
1
- import * as React from "react";
2
- import { Slot } from "@radix-ui/react-slot@1.1.2";
3
- import { cva, type VariantProps } from "class-variance-authority@0.7.1";
4
-
5
- import { cn } from "./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: "bg-primary text-primary-foreground hover:bg-primary/90",
13
- destructive:
14
- "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
- outline:
16
- "border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
- secondary:
18
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
- ghost:
20
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
- link: "text-primary underline-offset-4 hover:underline",
22
- },
23
- size: {
24
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
- icon: "size-9 rounded-md",
28
- },
29
- },
30
- defaultVariants: {
31
- variant: "default",
32
- size: "default",
33
- },
34
- },
35
- );
36
-
37
- const Button = React.forwardRef<
38
- HTMLButtonElement,
39
- React.ComponentProps<"button"> &
40
- VariantProps<typeof buttonVariants> & {
41
- asChild?: boolean;
42
- }
43
- >(({ className, variant, size, asChild = false, ...props }, ref) => {
44
- const Comp = asChild ? Slot : "button";
45
-
46
- return (
47
- <Comp
48
- data-slot="button"
49
- className={cn(buttonVariants({ variant, size, className }))}
50
- ref={ref}
51
- {...props}
52
- />
53
- );
54
- });
55
-
56
- Button.displayName = "Button";
57
-
58
- export { Button, buttonVariants };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/calendar.tsx DELETED
@@ -1,75 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { ChevronLeft, ChevronRight } from "lucide-react@0.487.0";
5
- import { DayPicker } from "react-day-picker@8.10.1";
6
-
7
- import { cn } from "./utils";
8
- import { buttonVariants } from "./button";
9
-
10
- function Calendar({
11
- className,
12
- classNames,
13
- showOutsideDays = true,
14
- ...props
15
- }: React.ComponentProps<typeof DayPicker>) {
16
- return (
17
- <DayPicker
18
- showOutsideDays={showOutsideDays}
19
- className={cn("p-3", className)}
20
- classNames={{
21
- months: "flex flex-col sm:flex-row gap-2",
22
- month: "flex flex-col gap-4",
23
- caption: "flex justify-center pt-1 relative items-center w-full",
24
- caption_label: "text-sm font-medium",
25
- nav: "flex items-center gap-1",
26
- nav_button: cn(
27
- buttonVariants({ variant: "outline" }),
28
- "size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
29
- ),
30
- nav_button_previous: "absolute left-1",
31
- nav_button_next: "absolute right-1",
32
- table: "w-full border-collapse space-x-1",
33
- head_row: "flex",
34
- head_cell:
35
- "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
36
- row: "flex w-full mt-2",
37
- cell: cn(
38
- "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
39
- props.mode === "range"
40
- ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
41
- : "[&:has([aria-selected])]:rounded-md",
42
- ),
43
- day: cn(
44
- buttonVariants({ variant: "ghost" }),
45
- "size-8 p-0 font-normal aria-selected:opacity-100",
46
- ),
47
- day_range_start:
48
- "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
49
- day_range_end:
50
- "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
51
- day_selected:
52
- "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
53
- day_today: "bg-accent text-accent-foreground",
54
- day_outside:
55
- "day-outside text-muted-foreground aria-selected:text-muted-foreground",
56
- day_disabled: "text-muted-foreground opacity-50",
57
- day_range_middle:
58
- "aria-selected:bg-accent aria-selected:text-accent-foreground",
59
- day_hidden: "invisible",
60
- ...classNames,
61
- }}
62
- components={{
63
- IconLeft: ({ className, ...props }) => (
64
- <ChevronLeft className={cn("size-4", className)} {...props} />
65
- ),
66
- IconRight: ({ className, ...props }) => (
67
- <ChevronRight className={cn("size-4", className)} {...props} />
68
- ),
69
- }}
70
- {...props}
71
- />
72
- );
73
- }
74
-
75
- export { Calendar };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/card.tsx DELETED
@@ -1,92 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "./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",
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 pt-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
- <h4
34
- data-slot="card-title"
35
- className={cn("leading-none", className)}
36
- {...props}
37
- />
38
- );
39
- }
40
-
41
- function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
- return (
43
- <p
44
- data-slot="card-description"
45
- className={cn("text-muted-foreground", 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 [&:last-child]:pb-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 pb-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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/carousel.tsx DELETED
@@ -1,241 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import useEmblaCarousel, {
5
- type UseEmblaCarouselType,
6
- } from "embla-carousel-react@8.6.0";
7
- import { ArrowLeft, ArrowRight } from "lucide-react@0.487.0";
8
-
9
- import { cn } from "./utils";
10
- import { Button } from "./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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/chart.tsx DELETED
@@ -1,353 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as RechartsPrimitive from "recharts@2.15.2";
5
-
6
- import { cn } from "./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({
108
- active,
109
- payload,
110
- className,
111
- indicator = "dot",
112
- hideLabel = false,
113
- hideIndicator = false,
114
- label,
115
- labelFormatter,
116
- labelClassName,
117
- formatter,
118
- color,
119
- nameKey,
120
- labelKey,
121
- }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
122
- React.ComponentProps<"div"> & {
123
- hideLabel?: boolean;
124
- hideIndicator?: boolean;
125
- indicator?: "line" | "dot" | "dashed";
126
- nameKey?: string;
127
- labelKey?: string;
128
- }) {
129
- const { config } = useChart();
130
-
131
- const tooltipLabel = React.useMemo(() => {
132
- if (hideLabel || !payload?.length) {
133
- return null;
134
- }
135
-
136
- const [item] = payload;
137
- const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
138
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
139
- const value =
140
- !labelKey && typeof label === "string"
141
- ? config[label as keyof typeof config]?.label || label
142
- : itemConfig?.label;
143
-
144
- if (labelFormatter) {
145
- return (
146
- <div className={cn("font-medium", labelClassName)}>
147
- {labelFormatter(value, payload)}
148
- </div>
149
- );
150
- }
151
-
152
- if (!value) {
153
- return null;
154
- }
155
-
156
- return <div className={cn("font-medium", labelClassName)}>{value}</div>;
157
- }, [
158
- label,
159
- labelFormatter,
160
- payload,
161
- hideLabel,
162
- labelClassName,
163
- config,
164
- labelKey,
165
- ]);
166
-
167
- if (!active || !payload?.length) {
168
- return null;
169
- }
170
-
171
- const nestLabel = payload.length === 1 && indicator !== "dot";
172
-
173
- return (
174
- <div
175
- className={cn(
176
- "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",
177
- className,
178
- )}
179
- >
180
- {!nestLabel ? tooltipLabel : null}
181
- <div className="grid gap-1.5">
182
- {payload.map((item, index) => {
183
- const key = `${nameKey || item.name || item.dataKey || "value"}`;
184
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
185
- const indicatorColor = color || item.payload.fill || item.color;
186
-
187
- return (
188
- <div
189
- key={item.dataKey}
190
- className={cn(
191
- "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
192
- indicator === "dot" && "items-center",
193
- )}
194
- >
195
- {formatter && item?.value !== undefined && item.name ? (
196
- formatter(item.value, item.name, item, index, item.payload)
197
- ) : (
198
- <>
199
- {itemConfig?.icon ? (
200
- <itemConfig.icon />
201
- ) : (
202
- !hideIndicator && (
203
- <div
204
- className={cn(
205
- "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
206
- {
207
- "h-2.5 w-2.5": indicator === "dot",
208
- "w-1": indicator === "line",
209
- "w-0 border-[1.5px] border-dashed bg-transparent":
210
- indicator === "dashed",
211
- "my-0.5": nestLabel && indicator === "dashed",
212
- },
213
- )}
214
- style={
215
- {
216
- "--color-bg": indicatorColor,
217
- "--color-border": indicatorColor,
218
- } as React.CSSProperties
219
- }
220
- />
221
- )
222
- )}
223
- <div
224
- className={cn(
225
- "flex flex-1 justify-between leading-none",
226
- nestLabel ? "items-end" : "items-center",
227
- )}
228
- >
229
- <div className="grid gap-1.5">
230
- {nestLabel ? tooltipLabel : null}
231
- <span className="text-muted-foreground">
232
- {itemConfig?.label || item.name}
233
- </span>
234
- </div>
235
- {item.value && (
236
- <span className="text-foreground font-mono font-medium tabular-nums">
237
- {item.value.toLocaleString()}
238
- </span>
239
- )}
240
- </div>
241
- </>
242
- )}
243
- </div>
244
- );
245
- })}
246
- </div>
247
- </div>
248
- );
249
- }
250
-
251
- const ChartLegend = RechartsPrimitive.Legend;
252
-
253
- function ChartLegendContent({
254
- className,
255
- hideIcon = false,
256
- payload,
257
- verticalAlign = "bottom",
258
- nameKey,
259
- }: React.ComponentProps<"div"> &
260
- Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
261
- hideIcon?: boolean;
262
- nameKey?: string;
263
- }) {
264
- const { config } = useChart();
265
-
266
- if (!payload?.length) {
267
- return null;
268
- }
269
-
270
- return (
271
- <div
272
- className={cn(
273
- "flex items-center justify-center gap-4",
274
- verticalAlign === "top" ? "pb-3" : "pt-3",
275
- className,
276
- )}
277
- >
278
- {payload.map((item) => {
279
- const key = `${nameKey || item.dataKey || "value"}`;
280
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
281
-
282
- return (
283
- <div
284
- key={item.value}
285
- className={cn(
286
- "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
287
- )}
288
- >
289
- {itemConfig?.icon && !hideIcon ? (
290
- <itemConfig.icon />
291
- ) : (
292
- <div
293
- className="h-2 w-2 shrink-0 rounded-[2px]"
294
- style={{
295
- backgroundColor: item.color,
296
- }}
297
- />
298
- )}
299
- {itemConfig?.label}
300
- </div>
301
- );
302
- })}
303
- </div>
304
- );
305
- }
306
-
307
- // Helper to extract item config from a payload.
308
- function getPayloadConfigFromPayload(
309
- config: ChartConfig,
310
- payload: unknown,
311
- key: string,
312
- ) {
313
- if (typeof payload !== "object" || payload === null) {
314
- return undefined;
315
- }
316
-
317
- const payloadPayload =
318
- "payload" in payload &&
319
- typeof payload.payload === "object" &&
320
- payload.payload !== null
321
- ? payload.payload
322
- : undefined;
323
-
324
- let configLabelKey: string = key;
325
-
326
- if (
327
- key in payload &&
328
- typeof payload[key as keyof typeof payload] === "string"
329
- ) {
330
- configLabelKey = payload[key as keyof typeof payload] as string;
331
- } else if (
332
- payloadPayload &&
333
- key in payloadPayload &&
334
- typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
335
- ) {
336
- configLabelKey = payloadPayload[
337
- key as keyof typeof payloadPayload
338
- ] as string;
339
- }
340
-
341
- return configLabelKey in config
342
- ? config[configLabelKey]
343
- : config[key as keyof typeof config];
344
- }
345
-
346
- export {
347
- ChartContainer,
348
- ChartTooltip,
349
- ChartTooltipContent,
350
- ChartLegend,
351
- ChartLegendContent,
352
- ChartStyle,
353
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/checkbox.tsx DELETED
@@ -1,32 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as CheckboxPrimitive from "@radix-ui/react-checkbox@1.1.4";
5
- import { CheckIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./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 bg-input-background 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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/collapsible.tsx DELETED
@@ -1,33 +0,0 @@
1
- "use client";
2
-
3
- import * as CollapsiblePrimitive from "@radix-ui/react-collapsible@1.1.3";
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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/command.tsx DELETED
@@ -1,177 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { Command as CommandPrimitive } from "cmdk@1.1.1";
5
- import { SearchIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./utils";
8
- import {
9
- Dialog,
10
- DialogContent,
11
- DialogDescription,
12
- DialogHeader,
13
- DialogTitle,
14
- } from "./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
- ...props
37
- }: React.ComponentProps<typeof Dialog> & {
38
- title?: string;
39
- description?: string;
40
- }) {
41
- return (
42
- <Dialog {...props}>
43
- <DialogHeader className="sr-only">
44
- <DialogTitle>{title}</DialogTitle>
45
- <DialogDescription>{description}</DialogDescription>
46
- </DialogHeader>
47
- <DialogContent className="overflow-hidden p-0">
48
- <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">
49
- {children}
50
- </Command>
51
- </DialogContent>
52
- </Dialog>
53
- );
54
- }
55
-
56
- function CommandInput({
57
- className,
58
- ...props
59
- }: React.ComponentProps<typeof CommandPrimitive.Input>) {
60
- return (
61
- <div
62
- data-slot="command-input-wrapper"
63
- className="flex h-9 items-center gap-2 border-b px-3"
64
- >
65
- <SearchIcon className="size-4 shrink-0 opacity-50" />
66
- <CommandPrimitive.Input
67
- data-slot="command-input"
68
- className={cn(
69
- "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",
70
- className,
71
- )}
72
- {...props}
73
- />
74
- </div>
75
- );
76
- }
77
-
78
- function CommandList({
79
- className,
80
- ...props
81
- }: React.ComponentProps<typeof CommandPrimitive.List>) {
82
- return (
83
- <CommandPrimitive.List
84
- data-slot="command-list"
85
- className={cn(
86
- "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
87
- className,
88
- )}
89
- {...props}
90
- />
91
- );
92
- }
93
-
94
- function CommandEmpty({
95
- ...props
96
- }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
97
- return (
98
- <CommandPrimitive.Empty
99
- data-slot="command-empty"
100
- className="py-6 text-center text-sm"
101
- {...props}
102
- />
103
- );
104
- }
105
-
106
- function CommandGroup({
107
- className,
108
- ...props
109
- }: React.ComponentProps<typeof CommandPrimitive.Group>) {
110
- return (
111
- <CommandPrimitive.Group
112
- data-slot="command-group"
113
- className={cn(
114
- "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",
115
- className,
116
- )}
117
- {...props}
118
- />
119
- );
120
- }
121
-
122
- function CommandSeparator({
123
- className,
124
- ...props
125
- }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
126
- return (
127
- <CommandPrimitive.Separator
128
- data-slot="command-separator"
129
- className={cn("bg-border -mx-1 h-px", className)}
130
- {...props}
131
- />
132
- );
133
- }
134
-
135
- function CommandItem({
136
- className,
137
- ...props
138
- }: React.ComponentProps<typeof CommandPrimitive.Item>) {
139
- return (
140
- <CommandPrimitive.Item
141
- data-slot="command-item"
142
- className={cn(
143
- "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",
144
- className,
145
- )}
146
- {...props}
147
- />
148
- );
149
- }
150
-
151
- function CommandShortcut({
152
- className,
153
- ...props
154
- }: React.ComponentProps<"span">) {
155
- return (
156
- <span
157
- data-slot="command-shortcut"
158
- className={cn(
159
- "text-muted-foreground ml-auto text-xs tracking-widest",
160
- className,
161
- )}
162
- {...props}
163
- />
164
- );
165
- }
166
-
167
- export {
168
- Command,
169
- CommandDialog,
170
- CommandInput,
171
- CommandList,
172
- CommandEmpty,
173
- CommandGroup,
174
- CommandItem,
175
- CommandShortcut,
176
- CommandSeparator,
177
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/context-menu.tsx DELETED
@@ -1,252 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as ContextMenuPrimitive from "@radix-ui/react-context-menu@2.2.6";
5
- import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/dialog.tsx DELETED
@@ -1,137 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as DialogPrimitive from "@radix-ui/react-dialog@1.1.6";
5
- import { XIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./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
- const DialogOverlay = React.forwardRef<
34
- React.ElementRef<typeof DialogPrimitive.Overlay>,
35
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
36
- >(({ className, ...props }, ref) => {
37
- return (
38
- <DialogPrimitive.Overlay
39
- ref={ref}
40
- data-slot="dialog-overlay"
41
- className={cn(
42
- "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",
43
- className,
44
- )}
45
- {...props}
46
- />
47
- );
48
- });
49
- DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
50
-
51
- function DialogContent({
52
- className,
53
- children,
54
- ...props
55
- }: React.ComponentProps<typeof DialogPrimitive.Content>) {
56
- return (
57
- <DialogPortal data-slot="dialog-portal">
58
- <DialogOverlay />
59
- <DialogPrimitive.Content
60
- data-slot="dialog-content"
61
- className={cn(
62
- "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",
63
- className,
64
- )}
65
- {...props}
66
- >
67
- {children}
68
- <DialogPrimitive.Close 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">
69
- <XIcon />
70
- <span className="sr-only">Close</span>
71
- </DialogPrimitive.Close>
72
- </DialogPrimitive.Content>
73
- </DialogPortal>
74
- );
75
- }
76
-
77
- function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
78
- return (
79
- <div
80
- data-slot="dialog-header"
81
- className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
82
- {...props}
83
- />
84
- );
85
- }
86
-
87
- function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
88
- return (
89
- <div
90
- data-slot="dialog-footer"
91
- className={cn(
92
- "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
93
- className,
94
- )}
95
- {...props}
96
- />
97
- );
98
- }
99
-
100
- function DialogTitle({
101
- className,
102
- ...props
103
- }: React.ComponentProps<typeof DialogPrimitive.Title>) {
104
- return (
105
- <DialogPrimitive.Title
106
- data-slot="dialog-title"
107
- className={cn("text-lg leading-none font-semibold", className)}
108
- {...props}
109
- />
110
- );
111
- }
112
-
113
- function DialogDescription({
114
- className,
115
- ...props
116
- }: React.ComponentProps<typeof DialogPrimitive.Description>) {
117
- return (
118
- <DialogPrimitive.Description
119
- data-slot="dialog-description"
120
- className={cn("text-muted-foreground text-sm", className)}
121
- {...props}
122
- />
123
- );
124
- }
125
-
126
- export {
127
- Dialog,
128
- DialogClose,
129
- DialogContent,
130
- DialogDescription,
131
- DialogFooter,
132
- DialogHeader,
133
- DialogOverlay,
134
- DialogPortal,
135
- DialogTitle,
136
- DialogTrigger,
137
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/drawer.tsx DELETED
@@ -1,132 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { Drawer as DrawerPrimitive } from "vaul@1.1.2";
5
-
6
- import { cn } from "./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("flex flex-col gap-1.5 p-4", className)}
80
- {...props}
81
- />
82
- );
83
- }
84
-
85
- function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
86
- return (
87
- <div
88
- data-slot="drawer-footer"
89
- className={cn("mt-auto flex flex-col gap-2 p-4", className)}
90
- {...props}
91
- />
92
- );
93
- }
94
-
95
- function DrawerTitle({
96
- className,
97
- ...props
98
- }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
99
- return (
100
- <DrawerPrimitive.Title
101
- data-slot="drawer-title"
102
- className={cn("text-foreground font-semibold", className)}
103
- {...props}
104
- />
105
- );
106
- }
107
-
108
- function DrawerDescription({
109
- className,
110
- ...props
111
- }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
112
- return (
113
- <DrawerPrimitive.Description
114
- data-slot="drawer-description"
115
- className={cn("text-muted-foreground text-sm", className)}
116
- {...props}
117
- />
118
- );
119
- }
120
-
121
- export {
122
- Drawer,
123
- DrawerPortal,
124
- DrawerOverlay,
125
- DrawerTrigger,
126
- DrawerClose,
127
- DrawerContent,
128
- DrawerHeader,
129
- DrawerFooter,
130
- DrawerTitle,
131
- DrawerDescription,
132
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/dropdown-menu.tsx DELETED
@@ -1,257 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu@2.1.6";
5
- import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/form.tsx DELETED
@@ -1,168 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
5
- import { Slot } from "@radix-ui/react-slot@1.1.2";
6
- import {
7
- Controller,
8
- FormProvider,
9
- useFormContext,
10
- useFormState,
11
- type ControllerProps,
12
- type FieldPath,
13
- type FieldValues,
14
- } from "react-hook-form@7.55.0";
15
-
16
- import { cn } from "./utils";
17
- import { Label } from "./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 } =
109
- useFormField();
110
-
111
- return (
112
- <Slot
113
- data-slot="form-control"
114
- id={formItemId}
115
- aria-describedby={
116
- !error
117
- ? `${formDescriptionId}`
118
- : `${formDescriptionId} ${formMessageId}`
119
- }
120
- aria-invalid={!!error}
121
- {...props}
122
- />
123
- );
124
- }
125
-
126
- function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
127
- const { formDescriptionId } = useFormField();
128
-
129
- return (
130
- <p
131
- data-slot="form-description"
132
- id={formDescriptionId}
133
- className={cn("text-muted-foreground text-sm", className)}
134
- {...props}
135
- />
136
- );
137
- }
138
-
139
- function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
140
- const { error, formMessageId } = useFormField();
141
- const body = error ? String(error?.message ?? "") : props.children;
142
-
143
- if (!body) {
144
- return null;
145
- }
146
-
147
- return (
148
- <p
149
- data-slot="form-message"
150
- id={formMessageId}
151
- className={cn("text-destructive text-sm", className)}
152
- {...props}
153
- >
154
- {body}
155
- </p>
156
- );
157
- }
158
-
159
- export {
160
- useFormField,
161
- Form,
162
- FormItem,
163
- FormLabel,
164
- FormControl,
165
- FormDescription,
166
- FormMessage,
167
- FormField,
168
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/hover-card.tsx DELETED
@@ -1,44 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as HoverCardPrimitive from "@radix-ui/react-hover-card@1.1.6";
5
-
6
- import { cn } from "./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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/input-otp.tsx DELETED
@@ -1,77 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { OTPInput, OTPInputContext } from "input-otp@1.4.2";
5
- import { MinusIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./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 gap-1", 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 bg-input-background 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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/input.tsx DELETED
@@ -1,21 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "./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 px-3 py-1 text-base bg-input-background 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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/label.tsx DELETED
@@ -1,24 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
5
-
6
- import { cn } from "./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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/menubar.tsx DELETED
@@ -1,276 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as MenubarPrimitive from "@radix-ui/react-menubar@1.1.6";
5
- import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/navigation-menu.tsx DELETED
@@ -1,168 +0,0 @@
1
- import * as React from "react";
2
- import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu@1.2.5";
3
- import { cva } from "class-variance-authority@0.7.1";
4
- import { ChevronDownIcon } from "lucide-react@0.487.0";
5
-
6
- import { cn } from "./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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/pagination.tsx DELETED
@@ -1,127 +0,0 @@
1
- import * as React from "react";
2
- import {
3
- ChevronLeftIcon,
4
- ChevronRightIcon,
5
- MoreHorizontalIcon,
6
- } from "lucide-react@0.487.0";
7
-
8
- import { cn } from "./utils";
9
- import { Button, buttonVariants } from "./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
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/popover.tsx DELETED
@@ -1,48 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as PopoverPrimitive from "@radix-ui/react-popover@1.1.6";
5
-
6
- import { cn } from "./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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/progress.tsx DELETED
@@ -1,31 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as ProgressPrimitive from "@radix-ui/react-progress@1.1.2";
5
-
6
- import { cn } from "./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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/radio-group.tsx DELETED
@@ -1,45 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as RadioGroupPrimitive from "@radix-ui/react-radio-group@1.2.3";
5
- import { CircleIcon } from "lucide-react@0.487.0";
6
-
7
- import { cn } from "./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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/resizable.tsx DELETED
@@ -1,56 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { GripVerticalIcon } from "lucide-react@0.487.0";
5
- import * as ResizablePrimitive from "react-resizable-panels@2.1.7";
6
-
7
- import { cn } from "./utils";
8
-
9
- function ResizablePanelGroup({
10
- className,
11
- ...props
12
- }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
13
- return (
14
- <ResizablePrimitive.PanelGroup
15
- data-slot="resizable-panel-group"
16
- className={cn(
17
- "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
18
- className,
19
- )}
20
- {...props}
21
- />
22
- );
23
- }
24
-
25
- function ResizablePanel({
26
- ...props
27
- }: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
28
- return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
29
- }
30
-
31
- function ResizableHandle({
32
- withHandle,
33
- className,
34
- ...props
35
- }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
36
- withHandle?: boolean;
37
- }) {
38
- return (
39
- <ResizablePrimitive.PanelResizeHandle
40
- data-slot="resizable-handle"
41
- className={cn(
42
- "bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
43
- className,
44
- )}
45
- {...props}
46
- >
47
- {withHandle && (
48
- <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
49
- <GripVerticalIcon className="size-2.5" />
50
- </div>
51
- )}
52
- </ResizablePrimitive.PanelResizeHandle>
53
- );
54
- }
55
-
56
- export { ResizablePanelGroup, ResizablePanel, ResizableHandle };