SarahXia0405 commited on
Commit
93e990c
·
verified ·
1 Parent(s): a801d04

Delete web/src

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/src/App.tsx +0 -531
  2. web/src/Attributions.md +0 -3
  3. web/src/assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png +0 -0
  4. web/src/components/ChatArea.tsx +0 -358
  5. web/src/components/FileUploadArea.tsx +0 -273
  6. web/src/components/FloatingActionButtons.tsx +0 -102
  7. web/src/components/GroupMembers.tsx +0 -60
  8. web/src/components/Header.tsx +0 -152
  9. web/src/components/LearningModeSelector.tsx +0 -93
  10. web/src/components/LeftSidebar.tsx +0 -243
  11. web/src/components/LoginScreen.tsx +0 -125
  12. web/src/components/Message.tsx +0 -368
  13. web/src/components/ProfileEditor.tsx +0 -207
  14. web/src/components/RightPanel.tsx +0 -281
  15. web/src/components/SmartReview.tsx +0 -263
  16. web/src/components/UserGuide.tsx +0 -151
  17. web/src/components/figma/ImageWithFallback.tsx +0 -27
  18. web/src/components/ui/accordion.tsx +0 -66
  19. web/src/components/ui/alert-dialog.tsx +0 -157
  20. web/src/components/ui/alert.tsx +0 -66
  21. web/src/components/ui/aspect-ratio.tsx +0 -11
  22. web/src/components/ui/avatar.tsx +0 -53
  23. web/src/components/ui/badge.tsx +0 -46
  24. web/src/components/ui/breadcrumb.tsx +0 -109
  25. web/src/components/ui/button.tsx +0 -58
  26. web/src/components/ui/calendar.tsx +0 -75
  27. web/src/components/ui/card.tsx +0 -92
  28. web/src/components/ui/carousel.tsx +0 -241
  29. web/src/components/ui/chart.tsx +0 -353
  30. web/src/components/ui/checkbox.tsx +0 -32
  31. web/src/components/ui/collapsible.tsx +0 -33
  32. web/src/components/ui/command.tsx +0 -177
  33. web/src/components/ui/context-menu.tsx +0 -252
  34. web/src/components/ui/dialog.tsx +0 -137
  35. web/src/components/ui/drawer.tsx +0 -132
  36. web/src/components/ui/dropdown-menu.tsx +0 -257
  37. web/src/components/ui/form.tsx +0 -168
  38. web/src/components/ui/hover-card.tsx +0 -44
  39. web/src/components/ui/input-otp.tsx +0 -77
  40. web/src/components/ui/input.tsx +0 -21
  41. web/src/components/ui/label.tsx +0 -24
  42. web/src/components/ui/menubar.tsx +0 -276
  43. web/src/components/ui/navigation-menu.tsx +0 -168
  44. web/src/components/ui/pagination.tsx +0 -127
  45. web/src/components/ui/popover.tsx +0 -48
  46. web/src/components/ui/progress.tsx +0 -31
  47. web/src/components/ui/radio-group.tsx +0 -45
  48. web/src/components/ui/resizable.tsx +0 -56
  49. web/src/components/ui/scroll-area.tsx +0 -58
  50. web/src/components/ui/select.tsx +0 -189
web/src/App.tsx DELETED
@@ -1,531 +0,0 @@
1
- // web/src/App.tsx
2
- import React, { useState, useEffect } 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 { LoginScreen } from './components/LoginScreen';
9
- import { ProfileEditor } from './components/ProfileEditor';
10
- import { X } from 'lucide-react';
11
- import { Button } from './components/ui/button';
12
- import { Toaster } from './components/ui/sonner';
13
- import { ChevronLeft, ChevronRight } from 'lucide-react';
14
- import { toast } from 'sonner';
15
-
16
- import {
17
- apiLogin,
18
- apiChat,
19
- apiUpload,
20
- apiExport,
21
- apiSummary,
22
- type LearningMode,
23
- type Language,
24
- type FileType,
25
- type User as ApiUser,
26
- } from './lib/api';
27
-
28
- export interface Message {
29
- id: string;
30
- role: 'user' | 'assistant';
31
- content: string;
32
- timestamp: Date;
33
- references?: string[];
34
- sender?: GroupMember; // For group chat
35
- }
36
-
37
- export interface User {
38
- name: string;
39
- email: string;
40
- }
41
-
42
- export interface GroupMember {
43
- id: string;
44
- name: string;
45
- email: string;
46
- avatar?: string;
47
- isAI?: boolean;
48
- }
49
-
50
- export type SpaceType = 'individual' | 'group';
51
-
52
- export interface Workspace {
53
- id: string;
54
- name: string;
55
- type: SpaceType;
56
- avatar: string;
57
- members?: GroupMember[];
58
- }
59
-
60
- export interface UploadedFile {
61
- file: File;
62
- type: FileType;
63
- }
64
-
65
- function App() {
66
- const [isDarkMode, setIsDarkMode] = useState(() => {
67
- const saved = localStorage.getItem('theme');
68
- return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
69
- });
70
-
71
- const [user, setUser] = useState<User | null>(null);
72
-
73
- const [messages, setMessages] = useState<Message[]>([
74
- {
75
- id: '1',
76
- role: 'assistant',
77
- content:
78
- "👋 Hi! I'm Clare, your AI teaching assistant. I'm here to help you learn through personalized tutoring. Feel free to ask me anything about the course materials, or upload your documents to get started!",
79
- timestamp: new Date(),
80
- },
81
- ]);
82
-
83
- const [learningMode, setLearningMode] = useState<LearningMode>('concept');
84
- const [language, setLanguage] = useState<Language>('auto');
85
- const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
86
-
87
- // You can later wire this to /api/memoryline
88
- const [memoryProgress] = useState(36);
89
-
90
- const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
91
- const [leftPanelVisible, setLeftPanelVisible] = useState(true);
92
-
93
- const [rightPanelOpen, setRightPanelOpen] = useState(false);
94
- const [rightPanelVisible, setRightPanelVisible] = useState(true);
95
-
96
- const [showProfileEditor, setShowProfileEditor] = useState(false);
97
-
98
- const [exportResult, setExportResult] = useState('');
99
- const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
100
-
101
- // Mock group members (still fine; AI responder uses backend now)
102
- const [groupMembers] = useState<GroupMember[]>([
103
- { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
104
- { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
105
- { id: '2', name: 'Michael Chen', email: 'michael.c@university.edu' },
106
- { id: '3', name: 'Emma Williams', email: 'emma.w@university.edu' },
107
- ]);
108
-
109
- const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
110
- const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>('individual');
111
-
112
- useEffect(() => {
113
- if (user) {
114
- const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
115
- setWorkspaces([
116
- {
117
- id: 'individual',
118
- name: 'My Space',
119
- type: 'individual',
120
- avatar: userAvatar,
121
- },
122
- {
123
- id: 'group-1',
124
- name: 'CS 101 Study Group',
125
- type: 'group',
126
- avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=cs101group',
127
- members: groupMembers,
128
- },
129
- {
130
- id: 'group-2',
131
- name: 'AI Ethics Team',
132
- type: 'group',
133
- avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=aiethicsteam',
134
- members: groupMembers,
135
- },
136
- ]);
137
- }
138
- }, [user, groupMembers]);
139
-
140
- const currentWorkspace = workspaces.find((w) => w.id === currentWorkspaceId) || workspaces[0];
141
- const spaceType: SpaceType = currentWorkspace?.type || 'individual';
142
-
143
- useEffect(() => {
144
- document.documentElement.classList.toggle('dark', isDarkMode);
145
- localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
146
- }, [isDarkMode]);
147
-
148
- const asApiUser = (u: User): ApiUser => ({ name: u.name, email: u.email });
149
-
150
- const handleSendMessage = async (content: string) => {
151
- if (!content.trim() || !user) return;
152
-
153
- const sender: GroupMember | undefined =
154
- spaceType === 'group'
155
- ? { id: user.email, name: user.name, email: user.email }
156
- : undefined;
157
-
158
- const userMessage: Message = {
159
- id: crypto.randomUUID(),
160
- role: 'user',
161
- content,
162
- timestamp: new Date(),
163
- sender,
164
- };
165
-
166
- setMessages((prev) => [...prev, userMessage]);
167
-
168
- const shouldAIRespond = spaceType === 'individual' || content.toLowerCase().includes('@clare');
169
- if (!shouldAIRespond) return;
170
-
171
- const assistantId = crypto.randomUUID();
172
- const assistantPlaceholder: Message = {
173
- id: assistantId,
174
- role: 'assistant',
175
- content: 'Thinking...',
176
- timestamp: new Date(),
177
- sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
178
- };
179
- setMessages((prev) => [...prev, assistantPlaceholder]);
180
-
181
- try {
182
- const data = await apiChat({
183
- user: asApiUser(user),
184
- message: content,
185
- learningMode,
186
- language,
187
- docType: 'Syllabus',
188
- });
189
-
190
- const references =
191
- (data.refs || [])
192
- .map((r) => [r.source_file, r.section].filter(Boolean).join(' — '))
193
- .filter(Boolean);
194
-
195
- setMessages((prev) =>
196
- prev.map((m) =>
197
- m.id === assistantId
198
- ? {
199
- ...m,
200
- content: data.reply || '',
201
- references: references.length ? references : undefined,
202
- }
203
- : m
204
- )
205
- );
206
- } catch (err: any) {
207
- setMessages((prev) =>
208
- prev.map((m) =>
209
- m.id === assistantId
210
- ? { ...m, content: `Sorry — request failed.\n${err?.message ?? String(err)}` }
211
- : m
212
- )
213
- );
214
- }
215
- };
216
-
217
- const handleFileUpload = async (files: File[]) => {
218
- if (!user) return;
219
-
220
- const newFiles: UploadedFile[] = files.map((file) => ({
221
- file,
222
- type: 'other',
223
- }));
224
-
225
- setUploadedFiles((prev) => [...prev, ...newFiles]);
226
-
227
- for (const f of files) {
228
- try {
229
- const r = await apiUpload({ user: asApiUser(user), file: f, fileType: 'other' });
230
- toast.success(r.status_md || `Uploaded: ${f.name}`);
231
- } catch (e: any) {
232
- toast.error(e?.message ?? `Upload failed: ${f.name}`);
233
- }
234
- }
235
- };
236
-
237
- const handleRemoveFile = (index: number) => {
238
- setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
239
- };
240
-
241
- const handleFileTypeChange = async (index: number, type: FileType) => {
242
- setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
243
-
244
- if (!user) return;
245
- const target = uploadedFiles[index];
246
- if (!target) return;
247
-
248
- try {
249
- const r = await apiUpload({
250
- user: asApiUser(user),
251
- file: target.file,
252
- fileType: type,
253
- });
254
- toast.success(r.status_md || `Updated type: ${target.file.name}`);
255
- } catch (e: any) {
256
- toast.error(e?.message ?? `Failed to update type: ${target.file.name}`);
257
- }
258
- };
259
-
260
- const handleClearConversation = () => {
261
- setMessages([
262
- {
263
- id: '1',
264
- role: 'assistant',
265
- content:
266
- "👋 Hi! I'm Clare, your AI teaching assistant. I'm here to help you learn through personalized tutoring. Feel free to ask me anything about the course materials, or upload your documents to get started!",
267
- timestamp: new Date(),
268
- },
269
- ]);
270
- toast.success('Conversation cleared');
271
- };
272
-
273
- const handleExport = async () => {
274
- if (!user) return;
275
- try {
276
- const r = await apiExport({ user: asApiUser(user), learningMode });
277
- setExportResult(r.markdown || '');
278
- setResultType('export');
279
- toast.success('Conversation exported!');
280
- } catch (e: any) {
281
- toast.error(e?.message ?? 'Export failed');
282
- }
283
- };
284
-
285
- const handleSummary = async () => {
286
- if (!user) return;
287
- try {
288
- const r = await apiSummary({ user: asApiUser(user), learningMode, language });
289
- setExportResult(r.markdown || '');
290
- setResultType('summary');
291
- toast.success('Summary generated!');
292
- } catch (e: any) {
293
- toast.error(e?.message ?? 'Summary failed');
294
- }
295
- };
296
-
297
- if (!user) {
298
- return (
299
- <LoginScreen
300
- onLogin={async (u) => {
301
- setUser(u);
302
- try {
303
- await apiLogin(asApiUser(u));
304
- } catch (e: any) {
305
- toast.error(e?.message ?? 'Login sync failed');
306
- }
307
- }}
308
- />
309
- );
310
- }
311
-
312
- return (
313
- <div className="min-h-screen bg-background flex flex-col">
314
- <Toaster />
315
- <Header
316
- user={user}
317
- onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
318
- onUserClick={() => setRightPanelOpen(!rightPanelOpen)}
319
- isDarkMode={isDarkMode}
320
- onToggleDarkMode={() => setIsDarkMode(!isDarkMode)}
321
- language={language}
322
- onLanguageChange={setLanguage}
323
- workspaces={workspaces}
324
- currentWorkspace={currentWorkspace}
325
- onWorkspaceChange={setCurrentWorkspaceId}
326
- />
327
-
328
- {showProfileEditor && user && (
329
- <ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
330
- )}
331
-
332
- <div
333
- className="flex-1 flex overflow-hidden"
334
- onWheel={(e) => e.stopPropagation()}
335
- style={{ overscrollBehavior: 'none' }}
336
- >
337
- {/* Mobile Sidebar Toggle - Left */}
338
- {leftSidebarOpen && (
339
- <div
340
- className="fixed inset-0 bg-black/50 z-40 lg:hidden"
341
- onClick={() => setLeftSidebarOpen(false)}
342
- />
343
- )}
344
-
345
- {/* Left Sidebar */}
346
- {leftPanelVisible ? (
347
- <aside className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative">
348
- <Button
349
- variant="secondary"
350
- size="icon"
351
- onClick={() => setLeftPanelVisible(false)}
352
- className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
353
- style={{ right: '-10px' }}
354
- title="Close panel"
355
- >
356
- <ChevronLeft className="h-3 w-3" />
357
- </Button>
358
- <LeftSidebar
359
- learningMode={learningMode}
360
- language={language}
361
- onLearningModeChange={setLearningMode}
362
- onLanguageChange={setLanguage}
363
- spaceType={spaceType}
364
- groupMembers={groupMembers}
365
- user={user}
366
- onLogin={setUser}
367
- onLogout={() => setUser(null)}
368
- isLoggedIn={!!user}
369
- onEditProfile={() => setShowProfileEditor(true)}
370
- />
371
- </aside>
372
- ) : (
373
- <Button
374
- variant="secondary"
375
- size="icon"
376
- onClick={() => setLeftPanelVisible(true)}
377
- className="hidden lg:flex fixed top-20 left-0 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
378
- title="Open panel"
379
- >
380
- <ChevronRight className="h-3 w-3" />
381
- </Button>
382
- )}
383
-
384
- {/* Left Sidebar - Mobile */}
385
- <aside
386
- className={`
387
- fixed lg:hidden inset-y-0 left-0 z-50
388
- w-80 bg-card border-r border-border
389
- transform transition-transform duration-300 ease-in-out
390
- ${leftSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
391
- flex flex-col
392
- mt-16
393
- h-[calc(100vh-4rem)]
394
- min-h-0
395
- `}
396
- >
397
- <div className="p-4 border-b border-border flex justify-between items-center">
398
- <h3>Settings & Guide</h3>
399
- <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
400
- <X className="h-5 w-5" />
401
- </Button>
402
- </div>
403
- <LeftSidebar
404
- learningMode={learningMode}
405
- language={language}
406
- onLearningModeChange={setLearningMode}
407
- onLanguageChange={setLanguage}
408
- spaceType={spaceType}
409
- groupMembers={groupMembers}
410
- user={user}
411
- onLogin={setUser}
412
- onLogout={() => setUser(null)}
413
- isLoggedIn={!!user}
414
- onEditProfile={() => setShowProfileEditor(true)}
415
- />
416
- </aside>
417
-
418
- {/* Main Chat Area */}
419
- <main className="flex-1 flex flex-col min-w-0 min-h-0 h-full">
420
- <ChatArea
421
- // ✅ NEW: pass ApiUser down so Message can submit feedback
422
- user={asApiUser(user)}
423
- messages={messages}
424
- onSendMessage={handleSendMessage}
425
- uploadedFiles={uploadedFiles}
426
- onFileUpload={handleFileUpload}
427
- onRemoveFile={handleRemoveFile}
428
- onFileTypeChange={handleFileTypeChange}
429
- memoryProgress={memoryProgress}
430
- isLoggedIn={!!user}
431
- learningMode={learningMode}
432
- onClearConversation={handleClearConversation}
433
- onLearningModeChange={setLearningMode}
434
- spaceType={spaceType}
435
- />
436
- </main>
437
-
438
- {/* Mobile Sidebar Toggle - Right */}
439
- {rightPanelOpen && (
440
- <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
441
- )}
442
-
443
- {/* Right Panel */}
444
- {rightPanelVisible ? (
445
- <aside className="hidden lg:flex w-80 bg-card border-l border-border flex-col min-h-0 relative" style={{ height: 'calc(100vh - 4rem)' }}>
446
- <Button
447
- variant="secondary"
448
- size="icon"
449
- onClick={() => setRightPanelVisible(false)}
450
- className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
451
- style={{ left: '-10px' }}
452
- title="Close panel"
453
- >
454
- <ChevronRight className="h-3 w-3" />
455
- </Button>
456
- <RightPanel
457
- user={user}
458
- onLogin={setUser}
459
- onLogout={() => setUser(null)}
460
- isLoggedIn={!!user}
461
- onClose={() => setRightPanelVisible(false)}
462
- exportResult={exportResult}
463
- setExportResult={setExportResult}
464
- resultType={resultType}
465
- setResultType={setResultType}
466
- onExport={handleExport}
467
- onSummary={handleSummary}
468
- />
469
- </aside>
470
- ) : (
471
- <Button
472
- variant="secondary"
473
- size="icon"
474
- onClick={() => setRightPanelVisible(true)}
475
- className="hidden lg:flex fixed top-20 right-0 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
476
- title="Open panel"
477
- >
478
- <ChevronLeft className="h-3 w-3" />
479
- </Button>
480
- )}
481
-
482
- {/* Right Panel - Mobile */}
483
- <aside
484
- className={`
485
- fixed lg:hidden inset-y-0 right-0 z-50
486
- w-80 bg-card border-l border-border
487
- transform transition-transform duration-300 ease-in-out
488
- ${rightPanelOpen ? 'translate-x-0' : 'translate-x-full'}
489
- flex flex-col
490
- mt-16
491
- h-[calc(100vh-4rem)]
492
- min-h-0
493
- `}
494
- >
495
- <div className="p-4 border-b border-border flex justify-between items-center">
496
- <h3>Account & Actions</h3>
497
- <Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
498
- <X className="h-5 w-5" />
499
- </Button>
500
- </div>
501
- <RightPanel
502
- user={user}
503
- onLogin={setUser}
504
- onLogout={() => setUser(null)}
505
- isLoggedIn={!!user}
506
- onClose={() => setRightPanelVisible(false)}
507
- exportResult={exportResult}
508
- setExportResult={setExportResult}
509
- resultType={resultType}
510
- setResultType={setResultType}
511
- onExport={handleExport}
512
- onSummary={handleSummary}
513
- />
514
- </aside>
515
-
516
- {/* Floating Action Buttons - Desktop only, when panel is closed */}
517
- {!rightPanelVisible && (
518
- <FloatingActionButtons
519
- user={user}
520
- isLoggedIn={!!user}
521
- onOpenPanel={() => setRightPanelVisible(true)}
522
- onExport={handleExport}
523
- onSummary={handleSummary}
524
- />
525
- )}
526
- </div>
527
- </div>
528
- );
529
- }
530
-
531
- 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/assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png DELETED
Binary file (40.2 kB)
 
web/src/components/ChatArea.tsx DELETED
@@ -1,358 +0,0 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { Button } from './ui/button';
3
- import { Textarea } from './ui/textarea';
4
- import { Send, ArrowDown, AlertCircle, Trash2, Share2 } from 'lucide-react';
5
- import { Message } from './Message';
6
- import { FileUploadArea } from './FileUploadArea';
7
- import { Alert, AlertDescription } from './ui/alert';
8
- import { Badge } from './ui/badge';
9
- import type { Message as MessageType, LearningMode, UploadedFile, FileType, SpaceType } from '../App';
10
- import { toast } from 'sonner';
11
- import {
12
- DropdownMenu,
13
- DropdownMenuContent,
14
- DropdownMenuItem,
15
- DropdownMenuTrigger,
16
- } from './ui/dropdown-menu';
17
- import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
18
-
19
- interface ChatAreaProps {
20
- messages: MessageType[];
21
- onSendMessage: (content: string) => void;
22
- uploadedFiles: UploadedFile[];
23
- onFileUpload: (files: File[]) => void;
24
- onRemoveFile: (index: number) => void;
25
- onFileTypeChange: (index: number, type: FileType) => void;
26
- memoryProgress: number;
27
- isLoggedIn: boolean;
28
- learningMode: LearningMode;
29
- onClearConversation: () => void;
30
- onLearningModeChange: (mode: LearningMode) => void;
31
- spaceType: SpaceType;
32
- }
33
-
34
- export function ChatArea({
35
- messages,
36
- onSendMessage,
37
- uploadedFiles,
38
- onFileUpload,
39
- onRemoveFile,
40
- onFileTypeChange,
41
- memoryProgress,
42
- isLoggedIn,
43
- learningMode,
44
- onClearConversation,
45
- onLearningModeChange,
46
- spaceType,
47
- }: ChatAreaProps) {
48
- const [input, setInput] = useState('');
49
- const [isTyping, setIsTyping] = useState(false);
50
- const [showScrollButton, setShowScrollButton] = useState(false);
51
- const messagesEndRef = useRef<HTMLDivElement>(null);
52
- const scrollContainerRef = useRef<HTMLDivElement>(null);
53
-
54
- const scrollToBottom = () => {
55
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
56
- };
57
-
58
- useEffect(() => {
59
- scrollToBottom();
60
- }, [messages]);
61
-
62
- useEffect(() => {
63
- const handleScroll = () => {
64
- if (scrollContainerRef.current) {
65
- const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
66
- setShowScrollButton(scrollHeight - scrollTop - clientHeight > 100);
67
- }
68
- };
69
-
70
- const container = scrollContainerRef.current;
71
- container?.addEventListener('scroll', handleScroll);
72
- return () => container?.removeEventListener('scroll', handleScroll);
73
- }, []);
74
-
75
- const handleSubmit = (e: React.FormEvent) => {
76
- e.preventDefault();
77
- if (!input.trim() || !isLoggedIn) return;
78
-
79
- onSendMessage(input);
80
- setInput('');
81
- setIsTyping(true);
82
- setTimeout(() => setIsTyping(false), 1500);
83
- };
84
-
85
- const handleKeyDown = (e: React.KeyboardEvent) => {
86
- if (e.key === 'Enter' && !e.shiftKey) {
87
- e.preventDefault();
88
- handleSubmit(e);
89
- }
90
- };
91
-
92
- const modeLabels: Record<LearningMode, string> = {
93
- concept: 'Concept Explainer',
94
- socratic: 'Socratic Tutor',
95
- exam: 'Exam Prep',
96
- assignment: 'Assignment Helper',
97
- summary: 'Quick Summary',
98
- };
99
-
100
- const handleClearClick = () => {
101
- if (messages.length <= 1) {
102
- toast.info('No conversation to clear');
103
- return;
104
- }
105
-
106
- if (window.confirm('Are you sure you want to clear the conversation? This cannot be undone.')) {
107
- onClearConversation();
108
- toast.success('Conversation cleared');
109
- }
110
- };
111
-
112
- const handleShareClick = () => {
113
- if (messages.length <= 1) {
114
- toast.info('No conversation to share');
115
- return;
116
- }
117
-
118
- // Create a shareable text version of the conversation
119
- const conversationText = messages
120
- .map(msg => `${msg.role === 'user' ? (msg.sender?.name ?? 'You') : 'Clare'}: ${msg.content}`)
121
- .join('\n\n');
122
-
123
- // Copy to clipboard
124
- navigator.clipboard.writeText(conversationText).then(() => {
125
- toast.success('Conversation copied to clipboard!');
126
- }).catch(() => {
127
- toast.error('Failed to copy conversation');
128
- });
129
- };
130
-
131
- return (
132
- <div className="flex flex-col h-full overflow-hidden">
133
- {/* Chat Area with Floating Input */}
134
- <div className="flex-1 relative border-b-2 border-border min-h-0 flex flex-col">
135
- {/* Action Buttons - Fixed at top right */}
136
- {messages.length > 1 && (
137
- <div className="absolute top-4 right-12 z-10 flex gap-2">
138
- <Button
139
- variant="ghost"
140
- size="sm"
141
- onClick={handleShareClick}
142
- disabled={!isLoggedIn}
143
- className="gap-2 bg-background/95 backdrop-blur-sm shadow-sm hover:shadow-md transition-all group"
144
- >
145
- <Share2 className="h-4 w-4" />
146
- <span className="hidden group-hover:inline">Share</span>
147
- </Button>
148
- <Button
149
- variant="ghost"
150
- size="sm"
151
- onClick={handleClearClick}
152
- disabled={!isLoggedIn}
153
- className="gap-2 bg-background/95 backdrop-blur-sm shadow-sm hover:shadow-md transition-all group"
154
- >
155
- <Trash2 className="h-4 w-4" />
156
- <span className="hidden group-hover:inline">Clear</span>
157
- </Button>
158
- </div>
159
- )}
160
-
161
- {/* Messages Area */}
162
- <div
163
- ref={scrollContainerRef}
164
- className="flex-1 overflow-y-auto overscroll-contain px-4 py-6 pb-36"
165
- style={{ overscrollBehavior: 'contain' }}
166
- onWheel={(e) => {
167
- const container = scrollContainerRef.current;
168
- if (!container) return;
169
-
170
- const { scrollTop, scrollHeight, clientHeight } = container;
171
- const isScrollable = scrollHeight > clientHeight;
172
- const isAtTop = scrollTop === 0;
173
- const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
174
-
175
- // If scrolling up at top or down at bottom, prevent default to stop propagation
176
- if (isScrollable && ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0))) {
177
- e.preventDefault();
178
- }
179
-
180
- e.stopPropagation();
181
- e.nativeEvent.stopImmediatePropagation();
182
- }}
183
- >
184
- <div className="max-w-4xl mx-auto space-y-6">
185
- {messages.map((message) => (
186
- <Message
187
- key={message.id}
188
- message={message}
189
- showSenderInfo={spaceType === 'group'}
190
- />
191
- ))}
192
-
193
- {isTyping && (
194
- <div className="flex gap-3">
195
- <div className="w-8 h-8 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
196
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
197
- </div>
198
- <div className="bg-muted rounded-2xl px-4 py-3">
199
- <div className="flex gap-1">
200
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '0ms' }} />
201
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '150ms' }} />
202
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '300ms' }} />
203
- </div>
204
- </div>
205
- </div>
206
- )}
207
-
208
- <div ref={messagesEndRef} />
209
- </div>
210
- </div>
211
-
212
- {/* Scroll to Bottom Button - Floating above input */}
213
- {showScrollButton && (
214
- <div className="absolute bottom-24 left-1/2 -translate-x-1/2 z-20">
215
- <Button
216
- variant="secondary"
217
- size="icon"
218
- className="rounded-full shadow-lg hover:shadow-xl transition-shadow bg-background"
219
- onClick={scrollToBottom}
220
- >
221
- <ArrowDown className="h-4 w-4" />
222
- </Button>
223
- </div>
224
- )}
225
-
226
- {/* Floating Input Area */}
227
- <div className="absolute bottom-0 left-0 right-0 bg-background/95 backdrop-blur-sm z-10">
228
- <div className="max-w-4xl mx-auto px-4 py-4">
229
- <form onSubmit={handleSubmit}>
230
- <div className="relative">
231
- {/* Mode Selector - ChatGPT style at bottom left */}
232
- <DropdownMenu>
233
- <DropdownMenuTrigger asChild>
234
- <Button
235
- variant="ghost"
236
- size="sm"
237
- className="absolute bottom-3 left-2 gap-1.5 h-8 px-2 text-xs z-10 hover:bg-muted/50"
238
- disabled={!isLoggedIn}
239
- type="button"
240
- >
241
- <span>{modeLabels[learningMode]}</span>
242
- <svg
243
- className="h-3 w-3 opacity-50"
244
- fill="none"
245
- stroke="currentColor"
246
- viewBox="0 0 24 24"
247
- >
248
- <path
249
- strokeLinecap="round"
250
- strokeLinejoin="round"
251
- strokeWidth={2}
252
- d="M19 9l-7 7-7-7"
253
- />
254
- </svg>
255
- </Button>
256
- </DropdownMenuTrigger>
257
- <DropdownMenuContent align="start" className="w-56">
258
- <DropdownMenuItem
259
- onClick={() => onLearningModeChange('concept')}
260
- className={learningMode === 'concept' ? 'bg-accent' : ''}
261
- >
262
- <div className="flex flex-col">
263
- <span className="font-medium">Concept Explainer</span>
264
- <span className="text-xs text-muted-foreground">
265
- Get detailed explanations of concepts
266
- </span>
267
- </div>
268
- </DropdownMenuItem>
269
- <DropdownMenuItem
270
- onClick={() => onLearningModeChange('socratic')}
271
- className={learningMode === 'socratic' ? 'bg-accent' : ''}
272
- >
273
- <div className="flex flex-col">
274
- <span className="font-medium">Socratic Tutor</span>
275
- <span className="text-xs text-muted-foreground">
276
- Learn through guided questions
277
- </span>
278
- </div>
279
- </DropdownMenuItem>
280
- <DropdownMenuItem
281
- onClick={() => onLearningModeChange('exam')}
282
- className={learningMode === 'exam' ? 'bg-accent' : ''}
283
- >
284
- <div className="flex flex-col">
285
- <span className="font-medium">Exam Prep</span>
286
- <span className="text-xs text-muted-foreground">
287
- Practice with quiz questions
288
- </span>
289
- </div>
290
- </DropdownMenuItem>
291
- <DropdownMenuItem
292
- onClick={() => onLearningModeChange('assignment')}
293
- className={learningMode === 'assignment' ? 'bg-accent' : ''}
294
- >
295
- <div className="flex flex-col">
296
- <span className="font-medium">Assignment Helper</span>
297
- <span className="text-xs text-muted-foreground">
298
- Get help with assignments
299
- </span>
300
- </div>
301
- </DropdownMenuItem>
302
- <DropdownMenuItem
303
- onClick={() => onLearningModeChange('summary')}
304
- className={learningMode === 'summary' ? 'bg-accent' : ''}
305
- >
306
- <div className="flex flex-col">
307
- <span className="font-medium">Quick Summary</span>
308
- <span className="text-xs text-muted-foreground">
309
- Get concise summaries
310
- </span>
311
- </div>
312
- </DropdownMenuItem>
313
- </DropdownMenuContent>
314
- </DropdownMenu>
315
-
316
- <Textarea
317
- value={input}
318
- onChange={(e) => setInput(e.target.value)}
319
- onKeyDown={handleKeyDown}
320
- placeholder={
321
- isLoggedIn
322
- ? spaceType === 'group'
323
- ? "Type a message... (mention @Clare to get AI assistance)"
324
- : "Ask Clare anything about the course..."
325
- : "Please log in on the right to start chatting..."
326
- }
327
- disabled={!isLoggedIn}
328
- className="min-h-[80px] pl-4 pr-12 resize-none bg-background border-2 border-border"
329
- />
330
- <Button
331
- type="submit"
332
- size="icon"
333
- disabled={!input.trim() || !isLoggedIn}
334
- className="absolute bottom-2 right-2 rounded-full"
335
- >
336
- <Send className="h-4 w-4" />
337
- </Button>
338
- </div>
339
- </form>
340
- </div>
341
- </div>
342
- </div>
343
-
344
- {/* Course Materials Section */}
345
- <div className="bg-card">
346
- <div className="max-w-4xl mx-auto px-4 py-4">
347
- <FileUploadArea
348
- uploadedFiles={uploadedFiles}
349
- onFileUpload={onFileUpload}
350
- onRemoveFile={onRemoveFile}
351
- onFileTypeChange={onFileTypeChange}
352
- disabled={!isLoggedIn}
353
- />
354
- </div>
355
- </div>
356
- </div>
357
- );
358
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/FileUploadArea.tsx DELETED
@@ -1,273 +0,0 @@
1
- import React, { useRef, useState } from 'react';
2
- import { Button } from './ui/button';
3
- import { Upload, File, X, FileText, FileSpreadsheet, Presentation } 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
- disabled?: boolean;
16
- }
17
-
18
- interface PendingFile {
19
- file: File;
20
- type: FileType;
21
- }
22
-
23
- export function FileUploadArea({
24
- uploadedFiles,
25
- onFileUpload,
26
- onRemoveFile,
27
- onFileTypeChange,
28
- disabled = false,
29
- }: FileUploadAreaProps) {
30
- const [isDragging, setIsDragging] = useState(false);
31
- const fileInputRef = useRef<HTMLInputElement>(null);
32
- const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
33
- const [showTypeDialog, setShowTypeDialog] = useState(false);
34
-
35
- const handleDragOver = (e: React.DragEvent) => {
36
- e.preventDefault();
37
- if (!disabled) setIsDragging(true);
38
- };
39
-
40
- const handleDragLeave = () => {
41
- setIsDragging(false);
42
- };
43
-
44
- const handleDrop = (e: React.DragEvent) => {
45
- e.preventDefault();
46
- setIsDragging(false);
47
- if (disabled) return;
48
-
49
- const files = Array.from(e.dataTransfer.files).filter((file) =>
50
- ['.pdf', '.docx', '.pptx'].some((ext) => file.name.toLowerCase().endsWith(ext))
51
- );
52
-
53
- if (files.length > 0) {
54
- setPendingFiles(files.map(file => ({ file, type: 'other' as FileType })));
55
- setShowTypeDialog(true);
56
- }
57
- };
58
-
59
- const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
60
- const files = Array.from(e.target.files || []);
61
- if (files.length > 0) {
62
- setPendingFiles(files.map(file => ({ file, type: 'other' as FileType })));
63
- setShowTypeDialog(true);
64
- }
65
- e.target.value = '';
66
- };
67
-
68
- const handleConfirmUpload = () => {
69
- onFileUpload(pendingFiles.map(pf => pf.file));
70
- // Update the parent's file types
71
- const startIndex = uploadedFiles.length;
72
- pendingFiles.forEach((pf, idx) => {
73
- setTimeout(() => {
74
- onFileTypeChange(startIndex + idx, pf.type);
75
- }, 0);
76
- });
77
- setPendingFiles([]);
78
- setShowTypeDialog(false);
79
- };
80
-
81
- const handleCancelUpload = () => {
82
- setPendingFiles([]);
83
- setShowTypeDialog(false);
84
- };
85
-
86
- const handlePendingFileTypeChange = (index: number, type: FileType) => {
87
- setPendingFiles(prev => prev.map((pf, i) =>
88
- i === index ? { ...pf, type } : pf
89
- ));
90
- };
91
-
92
- const getFileIcon = (filename: string) => {
93
- if (filename.endsWith('.pdf')) return FileText;
94
- if (filename.endsWith('.docx')) return File;
95
- if (filename.endsWith('.pptx')) return Presentation;
96
- return File;
97
- };
98
-
99
- const formatFileSize = (bytes: number) => {
100
- if (bytes < 1024) return bytes + ' B';
101
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
102
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
103
- };
104
-
105
- const getFileTypeLabel = (type: FileType) => {
106
- const labels: Record<FileType, string> = {
107
- 'syllabus': 'Syllabus',
108
- 'lecture-slides': 'Lecture Slides / PPT',
109
- 'literature-review': 'Literature Review / Paper',
110
- 'other': 'Other Course Document',
111
- };
112
- return labels[type];
113
- };
114
-
115
- return (
116
- <Card className="p-4 space-y-3">
117
- <div className="flex items-center justify-between">
118
- <h4 className="text-sm">Course Materials</h4>
119
- {uploadedFiles.length > 0 && (
120
- <Badge variant="secondary">{uploadedFiles.length} file(s)</Badge>
121
- )}
122
- </div>
123
-
124
- {/* Upload Area */}
125
- <div
126
- onDragOver={handleDragOver}
127
- onDragLeave={handleDragLeave}
128
- onDrop={handleDrop}
129
- className={`
130
- border-2 border-dashed rounded-lg p-4 text-center transition-colors
131
- ${isDragging ? 'border-primary bg-accent' : 'border-border'}
132
- ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
133
- `}
134
- onClick={() => !disabled && fileInputRef.current?.click()}
135
- >
136
- <Upload className="h-6 w-6 mx-auto mb-2 text-muted-foreground" />
137
- <p className="text-sm text-muted-foreground mb-1">
138
- {disabled ? 'Please log in to upload' : 'Drop files or click to upload'}
139
- </p>
140
- <p className="text-xs text-muted-foreground">
141
- .pdf, .docx, .pptx
142
- </p>
143
- <input
144
- ref={fileInputRef}
145
- type="file"
146
- multiple
147
- accept=".pdf,.docx,.pptx"
148
- onChange={handleFileSelect}
149
- className="hidden"
150
- disabled={disabled}
151
- />
152
- </div>
153
-
154
- {/* Uploaded Files List */}
155
- {uploadedFiles.length > 0 && (
156
- <div className="space-y-3 max-h-64 overflow-y-auto">
157
- {uploadedFiles.map((uploadedFile, index) => {
158
- const Icon = getFileIcon(uploadedFile.file.name);
159
- return (
160
- <div
161
- key={index}
162
- className="p-3 bg-muted rounded-md space-y-2"
163
- >
164
- <div className="flex items-center gap-2 group">
165
- <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
166
- <div className="flex-1 min-w-0">
167
- <p className="text-sm truncate">{uploadedFile.file.name}</p>
168
- <p className="text-xs text-muted-foreground">
169
- {formatFileSize(uploadedFile.file.size)}
170
- </p>
171
- </div>
172
- <Button
173
- variant="ghost"
174
- size="icon"
175
- className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
176
- onClick={(e) => {
177
- e.stopPropagation();
178
- onRemoveFile(index);
179
- }}
180
- >
181
- <X className="h-3 w-3" />
182
- </Button>
183
- </div>
184
- <div className="space-y-1">
185
- <label className="text-xs text-muted-foreground">File Type</label>
186
- <Select
187
- value={uploadedFile.type}
188
- onValueChange={(value) => onFileTypeChange(index, value as FileType)}
189
- >
190
- <SelectTrigger className="h-8 text-xs">
191
- <SelectValue />
192
- </SelectTrigger>
193
- <SelectContent>
194
- <SelectItem value="syllabus">Syllabus</SelectItem>
195
- <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
196
- <SelectItem value="literature-review">Literature Review / Paper</SelectItem>
197
- <SelectItem value="other">Other Course Document</SelectItem>
198
- </SelectContent>
199
- </Select>
200
- </div>
201
- </div>
202
- );
203
- })}
204
- </div>
205
- )}
206
-
207
- {/* Type Selection Dialog */}
208
- {showTypeDialog && (
209
- <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
210
- <DialogContent className="sm:max-w-[425px]">
211
- <DialogHeader>
212
- <DialogTitle>Select File Types</DialogTitle>
213
- <DialogDescription>
214
- Please select the type for each file you are uploading.
215
- </DialogDescription>
216
- </DialogHeader>
217
- <div className="space-y-3 max-h-64 overflow-y-auto">
218
- {pendingFiles.map((pendingFile, index) => {
219
- const Icon = getFileIcon(pendingFile.file.name);
220
- return (
221
- <div
222
- key={index}
223
- className="p-3 bg-muted rounded-md space-y-2"
224
- >
225
- <div className="flex items-center gap-2 group">
226
- <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
227
- <div className="flex-1 min-w-0">
228
- <p className="text-sm truncate">{pendingFile.file.name}</p>
229
- <p className="text-xs text-muted-foreground">
230
- {formatFileSize(pendingFile.file.size)}
231
- </p>
232
- </div>
233
- </div>
234
- <div className="space-y-1">
235
- <label className="text-xs text-muted-foreground">File Type</label>
236
- <Select
237
- value={pendingFile.type}
238
- onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
239
- >
240
- <SelectTrigger className="h-8 text-xs">
241
- <SelectValue />
242
- </SelectTrigger>
243
- <SelectContent>
244
- <SelectItem value="syllabus">Syllabus</SelectItem>
245
- <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
246
- <SelectItem value="literature-review">Literature Review / Paper</SelectItem>
247
- <SelectItem value="other">Other Course Document</SelectItem>
248
- </SelectContent>
249
- </Select>
250
- </div>
251
- </div>
252
- );
253
- })}
254
- </div>
255
- <DialogFooter>
256
- <Button
257
- variant="outline"
258
- onClick={handleCancelUpload}
259
- >
260
- Cancel
261
- </Button>
262
- <Button
263
- onClick={handleConfirmUpload}
264
- >
265
- Upload
266
- </Button>
267
- </DialogFooter>
268
- </DialogContent>
269
- </Dialog>
270
- )}
271
- </Card>
272
- );
273
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/FloatingActionButtons.tsx DELETED
@@ -1,102 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Button } from './ui/button';
3
- import { Download, Sparkles } from 'lucide-react';
4
- import { toast } from 'sonner';
5
- import type { User } from '../App';
6
-
7
- interface FloatingActionButtonsProps {
8
- user: User | null;
9
- isLoggedIn: boolean;
10
- onOpenPanel: () => void;
11
- onExport: () => void;
12
- onSummary: () => void;
13
- }
14
-
15
- export function FloatingActionButtons({
16
- user,
17
- isLoggedIn,
18
- onOpenPanel,
19
- onExport,
20
- onSummary,
21
- }: FloatingActionButtonsProps) {
22
- const [hoveredButton, setHoveredButton] = useState<string | null>(null);
23
-
24
- const handleAction = (action: () => void, actionName: string, shouldOpenPanel: boolean = false) => {
25
- if (!isLoggedIn) {
26
- toast.error('Please log in to use this feature');
27
- return;
28
- }
29
- action();
30
- if (shouldOpenPanel) {
31
- onOpenPanel();
32
- }
33
- };
34
-
35
- const buttons = [
36
- {
37
- id: 'export',
38
- icon: Download,
39
- label: 'Export Conversation',
40
- action: onExport,
41
- openPanel: true, // Open panel for export
42
- },
43
- {
44
- id: 'summary',
45
- icon: Sparkles,
46
- label: 'Summarization',
47
- action: onSummary,
48
- openPanel: true, // Open panel for summary
49
- },
50
- ];
51
-
52
- return (
53
- <div className="fixed right-4 bottom-[28rem] z-40 flex flex-col gap-2">
54
- {buttons.map((button, index) => {
55
- const Icon = button.icon;
56
- const isHovered = hoveredButton === button.id;
57
-
58
- return (
59
- <div
60
- key={button.id}
61
- className="relative group"
62
- onMouseEnter={() => setHoveredButton(button.id)}
63
- onMouseLeave={() => setHoveredButton(null)}
64
- >
65
- {/* Tooltip */}
66
- <div
67
- className={`
68
- absolute right-full mr-3 top-1/2 -translate-y-1/2
69
- px-3 py-2 rounded-lg bg-popover border border-border
70
- whitespace-nowrap text-sm shadow-lg
71
- transition-all duration-200
72
- ${isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-2 pointer-events-none'}
73
- `}
74
- >
75
- {button.label}
76
- </div>
77
-
78
- {/* Floating Button */}
79
- <Button
80
- size="icon"
81
- className={`
82
- h-6 w-6 rounded-full shadow-md opacity-60 hover:opacity-100
83
- transition-all duration-200
84
- ${isLoggedIn
85
- ? 'bg-primary hover:bg-primary/90 text-primary-foreground'
86
- : 'bg-muted hover:bg-muted/90 text-muted-foreground'
87
- }
88
- ${isHovered ? 'scale-110' : 'scale-100'}
89
- `}
90
- onClick={() => handleAction(button.action, button.label, button.openPanel)}
91
- style={{
92
- animationDelay: `${index * 100}ms`,
93
- }}
94
- >
95
- <Icon className="h-3 w-3" />
96
- </Button>
97
- </div>
98
- );
99
- })}
100
- </div>
101
- );
102
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/GroupMembers.tsx DELETED
@@ -1,60 +0,0 @@
1
- import React from 'react';
2
- import { Users } from 'lucide-react';
3
- import { Badge } from './ui/badge';
4
- import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
5
- import type { GroupMember } from '../App';
6
-
7
- interface GroupMembersProps {
8
- members: GroupMember[];
9
- }
10
-
11
- export function GroupMembers({ members }: GroupMembersProps) {
12
- return (
13
- <div className="space-y-3">
14
- <div className="flex items-center gap-2">
15
- <Users className="h-4 w-4 text-muted-foreground" />
16
- <h3 className="text-sm">Group Members ({members.length})</h3>
17
- </div>
18
-
19
- <div className="space-y-2">
20
- {members.map((member) => {
21
- const isAI = !!member.isAI;
22
- return (
23
- <div
24
- key={member.id}
25
- className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors"
26
- >
27
- {/* Avatar */}
28
- <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
29
- isAI
30
- ? 'overflow-hidden bg-white'
31
- : 'bg-muted'
32
- }`}>
33
- {isAI ? (
34
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
35
- ) : (
36
- <span className="text-sm">
37
- {member.name.split(' ').map(n => n[0]).join('').toUpperCase()}
38
- </span>
39
- )}
40
- </div>
41
-
42
- {/* Member Info */}
43
- <div className="flex-1 min-w-0">
44
- <div className="flex items-center gap-2">
45
- <p className="text-sm truncate">{member.name}</p>
46
- {isAI && (
47
- <Badge variant="secondary" className="text-xs">AI</Badge>
48
- )}
49
- </div>
50
- <p className="text-xs text-muted-foreground truncate">{member.email}</p>
51
- </div>
52
-
53
- {/* Online Status */}
54
- <div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" title="Online" />
55
- </div>
56
- )})}
57
- </div>
58
- </div>
59
- );
60
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/Header.tsx DELETED
@@ -1,152 +0,0 @@
1
- import React from 'react';
2
- import { Button } from './ui/button';
3
- import { Menu, Sun, Moon, Languages, ChevronDown } from 'lucide-react';
4
- import {
5
- DropdownMenu,
6
- DropdownMenuContent,
7
- DropdownMenuItem,
8
- DropdownMenuTrigger,
9
- } from './ui/dropdown-menu';
10
- import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
11
- import type { Workspace } from '../App';
12
-
13
- interface HeaderProps {
14
- user: UserType | null;
15
- onMenuClick: () => void;
16
- onUserClick: () => void;
17
- isDarkMode: boolean;
18
- onToggleDarkMode: () => void;
19
- language: Language;
20
- onLanguageChange: (lang: Language) => void;
21
- workspaces: Workspace[];
22
- currentWorkspace: Workspace | undefined;
23
- onWorkspaceChange: (workspaceId: string) => void;
24
- }
25
-
26
- type UserType = {
27
- name: string;
28
- email: string;
29
- };
30
-
31
- type Language = 'auto' | 'en' | 'zh';
32
-
33
- export function Header({
34
- user,
35
- onMenuClick,
36
- onUserClick,
37
- isDarkMode,
38
- onToggleDarkMode,
39
- language,
40
- onLanguageChange,
41
- workspaces,
42
- currentWorkspace,
43
- onWorkspaceChange,
44
- }: HeaderProps) {
45
- const languageLabels = {
46
- auto: 'Auto',
47
- en: 'English',
48
- zh: '简体中文',
49
- };
50
-
51
- return (
52
- <header className="h-16 border-b border-border bg-card px-4 lg:px-6 flex items-center justify-between sticky top-0 z-[100]">
53
- <div className="flex items-center gap-4">
54
- <Button
55
- variant="ghost"
56
- size="icon"
57
- className="lg:hidden"
58
- onClick={onMenuClick}
59
- >
60
- <Menu className="h-5 w-5" />
61
- </Button>
62
-
63
- <div className="flex items-center gap-3">
64
- <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center">
65
- <img src={clareAvatar} alt="Clare AI" className="w-full h-full object-cover" />
66
- </div>
67
- <div>
68
- <h1 className="text-lg sm:text-xl tracking-tight">
69
- Clare <span className="text-sm font-bold text-muted-foreground hidden sm:inline ml-2">Your Personalized AI Tutor</span>
70
- </h1>
71
- <p className="text-xs text-muted-foreground hidden sm:block">
72
- Personalized guidance, review, and intelligent reinforcement
73
- </p>
74
- </div>
75
- </div>
76
- </div>
77
-
78
- <div className="flex items-center gap-2">
79
- <DropdownMenu>
80
- <DropdownMenuTrigger asChild>
81
- <Button
82
- variant="ghost"
83
- size="icon"
84
- aria-label="Change language"
85
- >
86
- <Languages className="h-5 w-5" />
87
- </Button>
88
- </DropdownMenuTrigger>
89
- <DropdownMenuContent align="end">
90
- <DropdownMenuItem onClick={() => onLanguageChange('auto')}>
91
- {language === 'auto' && '✓ '}Auto
92
- </DropdownMenuItem>
93
- <DropdownMenuItem onClick={() => onLanguageChange('en')}>
94
- {language === 'en' && '✓ '}English
95
- </DropdownMenuItem>
96
- <DropdownMenuItem onClick={() => onLanguageChange('zh')}>
97
- {language === 'zh' && '✓ '}简体中文
98
- </DropdownMenuItem>
99
- </DropdownMenuContent>
100
- </DropdownMenu>
101
-
102
- <Button
103
- variant="ghost"
104
- size="icon"
105
- onClick={onToggleDarkMode}
106
- aria-label="Toggle dark mode"
107
- >
108
- {isDarkMode ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
109
- </Button>
110
-
111
- {user && currentWorkspace ? (
112
- <DropdownMenu>
113
- <DropdownMenuTrigger asChild>
114
- <Button
115
- variant="outline"
116
- className="gap-2 pl-2 pr-3"
117
- aria-label="Switch workspace"
118
- >
119
- <img
120
- src={currentWorkspace.avatar}
121
- alt={currentWorkspace.name}
122
- className="w-6 h-6 rounded-full object-cover"
123
- />
124
- <span className="hidden sm:inline max-w-[120px] truncate">{currentWorkspace.name}</span>
125
- <ChevronDown className="h-4 w-4 opacity-50" />
126
- </Button>
127
- </DropdownMenuTrigger>
128
- <DropdownMenuContent align="end" className="min-w-[14rem]">
129
- {workspaces.map((workspace) => (
130
- <DropdownMenuItem
131
- key={workspace.id}
132
- onClick={() => onWorkspaceChange(workspace.id)}
133
- className={`gap-3 ${currentWorkspace.id === workspace.id ? 'bg-accent' : ''}`}
134
- >
135
- <img
136
- src={workspace.avatar}
137
- alt={workspace.name}
138
- className="w-6 h-6 rounded-full object-cover flex-shrink-0"
139
- />
140
- <span className="truncate">{workspace.name}</span>
141
- {currentWorkspace.id === workspace.id && (
142
- <span className="ml-auto text-primary">✓</span>
143
- )}
144
- </DropdownMenuItem>
145
- ))}
146
- </DropdownMenuContent>
147
- </DropdownMenu>
148
- ) : null}
149
- </div>
150
- </header>
151
- );
152
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/LearningModeSelector.tsx DELETED
@@ -1,93 +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
- title: 'Socratic Tutor',
28
- description: 'Learn through questions',
29
- color: 'from-red-500 to-rose-600',
30
- },
31
- {
32
- id: 'exam' as LearningMode,
33
- icon: GraduationCap,
34
- title: 'Exam Prep/Quiz',
35
- description: 'Test your knowledge',
36
- color: 'from-green-500 to-green-600',
37
- },
38
- {
39
- id: 'assignment' as LearningMode,
40
- icon: FileEdit,
41
- title: 'Assignment Helper',
42
- description: 'Get homework guidance',
43
- color: 'from-orange-500 to-orange-600',
44
- },
45
- {
46
- id: 'summary' as LearningMode,
47
- icon: Zap,
48
- title: 'Quick Summary',
49
- description: 'Fast key points review',
50
- color: 'from-pink-500 to-pink-600',
51
- },
52
- ];
53
-
54
- export function LearningModeSelector({ selectedMode, onModeChange }: ModeSelectorProps) {
55
- return (
56
- <div className="space-y-2">
57
- {modes.map((mode) => {
58
- const Icon = mode.icon;
59
- const isSelected = selectedMode === mode.id;
60
-
61
- return (
62
- <Card
63
- key={mode.id}
64
- className={`
65
- p-3 cursor-pointer transition-all duration-200
66
- ${isSelected
67
- ? 'border-primary bg-accent shadow-sm'
68
- : 'hover:border-primary/50 hover:shadow-sm'
69
- }
70
- `}
71
- onClick={() => onModeChange(mode.id)}
72
- >
73
- <div className="flex items-start gap-3">
74
- <div className={`
75
- w-10 h-10 rounded-lg bg-gradient-to-br ${mode.color}
76
- flex items-center justify-center flex-shrink-0
77
- `}>
78
- <Icon className="h-5 w-5 text-white" />
79
- </div>
80
- <div className="flex-1 min-w-0">
81
- <h4 className="text-sm mb-1">{mode.title}</h4>
82
- <p className="text-xs text-muted-foreground">{mode.description}</p>
83
- </div>
84
- {isSelected && (
85
- <div className="w-2 h-2 rounded-full bg-primary flex-shrink-0 mt-2" />
86
- )}
87
- </div>
88
- </Card>
89
- );
90
- })}
91
- </div>
92
- );
93
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/LeftSidebar.tsx DELETED
@@ -1,243 +0,0 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
3
- import { UserGuide } from './UserGuide';
4
- import { SmartReview } from './SmartReview';
5
- import { Label } from './ui/label';
6
- import { Button } from './ui/button';
7
- import { LogIn, Edit, BookOpen } from 'lucide-react';
8
- import { GroupMembers } from './GroupMembers';
9
- import { Card } from './ui/card';
10
- import { Input } from './ui/input';
11
- import type { LearningMode, Language, SpaceType, GroupMember, User as UserType } from '../App';
12
- import { toast } from 'sonner';
13
-
14
- interface LeftSidebarProps {
15
- learningMode: LearningMode;
16
- language: Language;
17
- onLearningModeChange: (mode: LearningMode) => void;
18
- onLanguageChange: (lang: Language) => void;
19
- spaceType: SpaceType;
20
- groupMembers: GroupMember[];
21
- user: UserType | null;
22
- onLogin: (user: UserType) => void;
23
- onLogout: () => void;
24
- isLoggedIn: boolean;
25
- onEditProfile: () => void;
26
- }
27
-
28
- export function LeftSidebar({
29
- learningMode,
30
- language,
31
- onLearningModeChange,
32
- onLanguageChange,
33
- spaceType,
34
- groupMembers,
35
- user,
36
- onLogin,
37
- onLogout,
38
- isLoggedIn,
39
- onEditProfile,
40
- }: LeftSidebarProps) {
41
- const [showLoginForm, setShowLoginForm] = useState(false);
42
- const [name, setName] = useState('');
43
- const [email, setEmail] = useState('');
44
-
45
- const handleLogin = () => {
46
- if (!name.trim() || !email.trim()) {
47
- toast.error('Please fill in all fields');
48
- return;
49
- }
50
-
51
- onLogin({ name: name.trim(), email: email.trim() });
52
- setShowLoginForm(false);
53
- setName('');
54
- setEmail('');
55
- toast.success(`Welcome, ${name}!`);
56
- };
57
-
58
- const handleLogout = () => {
59
- onLogout();
60
- setShowLoginForm(false);
61
- toast.success('Logged out successfully');
62
- };
63
-
64
- const scrollContainerRef = useRef<HTMLDivElement>(null);
65
-
66
- useEffect(() => {
67
- const container = scrollContainerRef.current;
68
- if (!container) return;
69
-
70
- const handleWheel = (e: WheelEvent) => {
71
- e.stopPropagation();
72
- e.stopImmediatePropagation();
73
-
74
- const { scrollTop, scrollHeight, clientHeight } = container;
75
- const isScrollable = scrollHeight > clientHeight;
76
- const isAtTop = scrollTop === 0;
77
- const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
78
-
79
- if (isScrollable && ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0))) {
80
- e.preventDefault();
81
- }
82
- };
83
-
84
- container.addEventListener('wheel', handleWheel, { passive: false, capture: true });
85
- return () => {
86
- container.removeEventListener('wheel', handleWheel, { capture: true } as any);
87
- };
88
- }, []);
89
-
90
- return (
91
- <div
92
- ref={scrollContainerRef}
93
- className="flex-1 overflow-auto overscroll-contain flex flex-col"
94
- style={{ overscrollBehavior: 'contain' }}
95
- >
96
- {/* Profile/Login Section */}
97
- <div className="p-4 border-b border-border flex-shrink-0">
98
- <h3 className="text-base font-medium mb-4">Profile</h3>
99
- <Card className="p-4">
100
- {!isLoggedIn ? (
101
- <div className="space-y-4">
102
- <div className="flex flex-col items-center py-4">
103
- <img
104
- src="https://images.unsplash.com/photo-1588912914049-d2664f76a947?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzdHVkZW50JTIwc3R1ZHlpbmclMjBpbGx1c3RyYXRpb258ZW58MXx8fHwxNzY2MDY2NjcyfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
105
- alt="Student studying"
106
- className="w-20 h-20 rounded-full object-cover mb-4"
107
- />
108
- <h3 className="mb-2">Welcome to Clare!</h3>
109
- <p className="text-sm text-muted-foreground text-center mb-4">
110
- Log in to start your personalized learning journey
111
- </p>
112
- </div>
113
-
114
- {!showLoginForm ? (
115
- <Button onClick={() => setShowLoginForm(true)} className="w-full gap-2">
116
- <LogIn className="h-4 w-4" />
117
- Student Login
118
- </Button>
119
- ) : (
120
- <div className="space-y-3">
121
- <div className="space-y-2">
122
- <Label htmlFor="name">Name</Label>
123
- <Input
124
- id="name"
125
- value={name}
126
- onChange={(e) => setName(e.target.value)}
127
- placeholder="Enter your name"
128
- />
129
- </div>
130
- <div className="space-y-2">
131
- <Label htmlFor="email">Email / Student ID</Label>
132
- <Input
133
- id="email"
134
- type="email"
135
- value={email}
136
- onChange={(e) => setEmail(e.target.value)}
137
- placeholder="Enter your email or ID"
138
- />
139
- </div>
140
- <div className="flex gap-2">
141
- <Button onClick={handleLogin} className="flex-1">
142
- Enter
143
- </Button>
144
- <Button variant="outline" onClick={() => setShowLoginForm(false)}>
145
- Cancel
146
- </Button>
147
- </div>
148
- </div>
149
- )}
150
- </div>
151
- ) : (
152
- <div className="space-y-3">
153
- <div className="flex items-start justify-between">
154
- <div className="flex items-center gap-2">
155
- <img
156
- src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(
157
- user?.email || ''
158
- )}`}
159
- alt={user?.name || 'User'}
160
- className="w-8 h-8 rounded-full object-cover flex-shrink-0 bg-muted"
161
- />
162
- <div className="space-y-1">
163
- <p className="text-sm text-muted-foreground">Hello,</p>
164
- <h4>{user?.name ?? ''}!</h4>
165
- </div>
166
- </div>
167
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onEditProfile}>
168
- <Edit className="h-4 w-4" />
169
- </Button>
170
- </div>
171
- <div className="text-xs text-muted-foreground">
172
- ID: {user?.email ? user.email.split('@')[0] : ''}
173
- </div>
174
-
175
- <Button variant="outline" className="w-full" onClick={handleLogout}>
176
- Log out
177
- </Button>
178
- </div>
179
- )}
180
- </Card>
181
- </div>
182
-
183
- {/* Group Members - Only show in group mode */}
184
- {spaceType === 'group' && (
185
- <div className="p-4 border-b border-border flex-shrink-0">
186
- <GroupMembers members={groupMembers} />
187
- </div>
188
- )}
189
-
190
- {/* Tabs */}
191
- <Tabs defaultValue="review" className="flex flex-col flex-1 min-h-0 overflow-hidden">
192
- <div className="px-4 pt-4">
193
- {/* 关键:TabsList 已在 ui/tabs.tsx 改成 w-full flex,这里只需要三等分 */}
194
- <TabsList className="w-full">
195
- <TabsTrigger value="review" className="flex-1 px-2 text-xs whitespace-nowrap">
196
- Smart Review
197
- </TabsTrigger>
198
- <TabsTrigger value="quiz" className="flex-1 px-2 text-xs whitespace-nowrap">
199
- Personal Quiz
200
- </TabsTrigger>
201
- <TabsTrigger value="guide" className="flex-1 px-2 text-xs whitespace-nowrap">
202
- User Guide
203
- </TabsTrigger>
204
- </TabsList>
205
- </div>
206
-
207
- <TabsContent value="review" className="flex-1 mt-0 p-4 space-y-6 overflow-auto">
208
- <SmartReview />
209
- </TabsContent>
210
-
211
- <TabsContent value="quiz" className="flex-1 mt-0 p-4 overflow-auto">
212
- <div className="space-y-4">
213
- <div className="flex items-center gap-2">
214
- <BookOpen className="h-5 w-5 text-red-500" />
215
- <h3 className="text-base font-medium">Personal Quiz</h3>
216
- </div>
217
-
218
- <Card className="p-3 bg-muted/50 border-border">
219
- <p className="text-xs text-muted-foreground leading-relaxed">
220
- Clare analyzes your chat history and learning patterns to randomly select a personalized question that
221
- challenges your understanding of previously discussed topics.
222
- </p>
223
- </Card>
224
-
225
- <Button
226
- className="w-full bg-red-500 hover:bg-red-600 text-white"
227
- size="sm"
228
- onClick={() => {
229
- toast.success('Generating personalized quiz...');
230
- }}
231
- >
232
- Test your memory
233
- </Button>
234
- </div>
235
- </TabsContent>
236
-
237
- <TabsContent value="guide" className="flex-1 mt-0 p-4 overflow-auto">
238
- <UserGuide />
239
- </TabsContent>
240
- </Tabs>
241
- </div>
242
- );
243
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/LoginScreen.tsx DELETED
@@ -1,125 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Button } from './ui/button';
3
- import { Input } from './ui/input';
4
- import { Label } from './ui/label';
5
- import { Card } from './ui/card';
6
- import { toast } from 'sonner';
7
- import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
8
- import type { User } from '../App';
9
-
10
- interface LoginScreenProps {
11
- onLogin: (user: User) => void;
12
- }
13
-
14
- export function LoginScreen({ onLogin }: LoginScreenProps) {
15
- const [showForm, setShowForm] = useState(false);
16
- const [name, setName] = useState('');
17
- const [emailOrId, setEmailOrId] = useState('');
18
- const [submitting, setSubmitting] = useState(false);
19
-
20
- const handleSubmit = async (e: React.FormEvent) => {
21
- e.preventDefault();
22
- const n = name.trim();
23
- const uid = emailOrId.trim();
24
-
25
- if (!n || !uid) return;
26
-
27
- setSubmitting(true);
28
- try {
29
- // HF Space: same-origin call is correct (your FastAPI serves the SPA)
30
- const resp = await fetch('/api/login', {
31
- method: 'POST',
32
- headers: { 'Content-Type': 'application/json' },
33
- body: JSON.stringify({ name: n, user_id: uid }),
34
- });
35
-
36
- const data = await resp.json().catch(() => ({}));
37
-
38
- if (!resp.ok || !data?.ok) {
39
- const msg = data?.error || `Login failed (HTTP ${resp.status})`;
40
- throw new Error(msg);
41
- }
42
-
43
- // Keep your existing App flow: user = { name, email }
44
- onLogin({ name: data.user?.name ?? n, email: data.user?.user_id ?? uid });
45
- toast.success('Signed in');
46
- } catch (err: any) {
47
- toast.error(err?.message || 'Login failed');
48
- } finally {
49
- setSubmitting(false);
50
- }
51
- };
52
-
53
- return (
54
- <div className="min-h-screen bg-background flex items-center justify-center p-4">
55
- <Card className="w-full max-w-md p-8">
56
- <div className="flex flex-col items-center space-y-6">
57
- {/* Clare Avatar */}
58
- <div className="w-24 h-24 rounded-full overflow-hidden bg-white flex items-center justify-center">
59
- <img src={clareAvatar} alt="Clare AI" className="w-full h-full object-cover" />
60
- </div>
61
-
62
- {/* Welcome Text */}
63
- <div className="text-center space-y-2">
64
- <h1 className="text-2xl">Welcome to Clare</h1>
65
- <p className="text-sm text-muted-foreground">
66
- Your AI teaching assistant for personalized learning
67
- </p>
68
- </div>
69
-
70
- {!showForm ? (
71
- <Button onClick={() => setShowForm(true)} className="w-full" size="lg">
72
- Sign In
73
- </Button>
74
- ) : (
75
- <form onSubmit={handleSubmit} className="w-full space-y-4">
76
- <div className="space-y-2">
77
- <Label htmlFor="login-name">Name</Label>
78
- <Input
79
- id="login-name"
80
- value={name}
81
- onChange={(e) => setName(e.target.value)}
82
- placeholder="Enter your name"
83
- required
84
- disabled={submitting}
85
- />
86
- </div>
87
-
88
- <div className="space-y-2">
89
- <Label htmlFor="login-userid">Email / Student ID</Label>
90
- <Input
91
- id="login-userid"
92
- // IMPORTANT: allow non-email IDs
93
- type="text"
94
- value={emailOrId}
95
- onChange={(e) => setEmailOrId(e.target.value)}
96
- placeholder="Enter your email or ID"
97
- required
98
- disabled={submitting}
99
- />
100
- </div>
101
-
102
- <div className="flex gap-2">
103
- <Button type="submit" className="flex-1" disabled={submitting}>
104
- {submitting ? 'Signing in…' : 'Enter'}
105
- </Button>
106
- <Button
107
- type="button"
108
- variant="outline"
109
- onClick={() => setShowForm(false)}
110
- disabled={submitting}
111
- >
112
- Cancel
113
- </Button>
114
- </div>
115
-
116
- <div className="text-xs text-muted-foreground">
117
- This sign-in is for session identification only (name + ID), used to personalize tutoring and log events.
118
- </div>
119
- </form>
120
- )}
121
- </div>
122
- </Card>
123
- </div>
124
- );
125
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/Message.tsx DELETED
@@ -1,368 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Button } from './ui/button';
3
- import {
4
- Copy,
5
- ThumbsUp,
6
- ThumbsDown,
7
- ChevronDown,
8
- ChevronUp,
9
- Check,
10
- X,
11
- } from 'lucide-react';
12
- import { Badge } from './ui/badge';
13
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
14
- import { Textarea } from './ui/textarea';
15
- import type { Message as MessageType } from '../App';
16
- import { toast } from 'sonner';
17
- import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
18
-
19
- // ✅ Markdown rendering (NEW)
20
- import ReactMarkdown from 'react-markdown';
21
- import remarkGfm from 'remark-gfm';
22
-
23
- // ✅ NEW: call backend feedback API
24
- import {
25
- apiFeedback,
26
- type User as ApiUser,
27
- type LearningMode as ApiLearningMode,
28
- type FileType as ApiFileType,
29
- type FeedbackRating,
30
- } from '../lib/api';
31
-
32
- interface MessageProps {
33
- message: MessageType;
34
- showSenderInfo?: boolean; // For group chat mode
35
-
36
- // ✅ NEW (recommended) — for logging feedback correctly
37
- user?: ApiUser | null;
38
-
39
- // context (optional but useful for metadata)
40
- learningMode?: ApiLearningMode;
41
- docType?: ApiFileType | string;
42
-
43
- // optional: supply refs (if your message.references is not the same as backend refs)
44
- refs?: string[];
45
-
46
- /**
47
- * Optional: provide the user question that led to this assistant message.
48
- * Best practice: parent passes a function that finds previous user message.
49
- */
50
- getContextUserText?: () => string;
51
- }
52
-
53
- // 反馈标签选项
54
- const FEEDBACK_TAGS: Record<FeedbackRating, string[]> = {
55
- not_helpful: [
56
- 'Code was incorrect',
57
- "Shouldn't have used Memory",
58
- "Don't like the personality",
59
- "Don't like the style",
60
- 'Not factually correct',
61
- ],
62
- helpful: [
63
- 'Accurate and helpful',
64
- 'Clear explanation',
65
- 'Good examples',
66
- 'Solved my problem',
67
- 'Well structured',
68
- ],
69
- };
70
-
71
- export function Message({
72
- message,
73
- showSenderInfo = false,
74
-
75
- // NEW
76
- user = null,
77
- learningMode,
78
- docType,
79
- refs,
80
- getContextUserText,
81
- }: MessageProps) {
82
- const [feedback, setFeedback] = useState<FeedbackRating | null>(null);
83
- const [copied, setCopied] = useState(false);
84
- const [referencesOpen, setReferencesOpen] = useState(false);
85
- const [showFeedbackArea, setShowFeedbackArea] = useState(false);
86
-
87
- // ✅ unify to backend enum
88
- const [feedbackType, setFeedbackType] = useState<FeedbackRating | null>(null);
89
-
90
- const [feedbackText, setFeedbackText] = useState('');
91
- const [selectedTags, setSelectedTags] = useState<string[]>([]);
92
- const [submitting, setSubmitting] = useState(false);
93
-
94
- const isUser = message.role === 'user';
95
-
96
- const handleCopy = async () => {
97
- await navigator.clipboard.writeText(message.content);
98
- setCopied(true);
99
- toast.success('Message copied to clipboard');
100
- setTimeout(() => setCopied(false), 2000);
101
- };
102
-
103
- const handleFeedbackClick = (type: FeedbackRating) => {
104
- if (feedback === type) {
105
- // clicked same state -> collapse + reset detail
106
- setFeedback(null);
107
- setShowFeedbackArea(false);
108
- setFeedbackType(null);
109
- setFeedbackText('');
110
- setSelectedTags([]);
111
- return;
112
- }
113
-
114
- // open feedback area
115
- setFeedback(type);
116
- setFeedbackType(type);
117
- setShowFeedbackArea(true);
118
- };
119
-
120
- const handleFeedbackClose = () => {
121
- setShowFeedbackArea(false);
122
- setFeedbackType(null);
123
- setFeedbackText('');
124
- setSelectedTags([]);
125
- // do NOT reset feedback (keeps thumbs state)
126
- };
127
-
128
- const handleTagToggle = (tag: string) => {
129
- setSelectedTags((prev) =>
130
- prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],
131
- );
132
- };
133
-
134
- const handleFeedbackSubmit = async () => {
135
- if (!feedbackType) return;
136
-
137
- // If not logged in / no user, we cannot attribute feedback to a student_id
138
- if (!user?.email) {
139
- toast.error('Please login to submit feedback.');
140
- return;
141
- }
142
-
143
- const assistantText = (message.content || '').trim();
144
- const userText = (getContextUserText ? getContextUserText() : '').trim();
145
-
146
- // refs: prefer explicit refs prop; fallback to message.references
147
- const refsToSend =
148
- refs && refs.length > 0 ? refs : (message.references ?? []);
149
-
150
- setSubmitting(true);
151
- try {
152
- await apiFeedback({
153
- user,
154
- rating: feedbackType,
155
- assistantMessageId: message.id, // recommended
156
- assistantText,
157
- userText,
158
- tags: selectedTags,
159
- comment: feedbackText,
160
- refs: refsToSend,
161
- learningMode,
162
- docType,
163
- timestampMs: Date.now(),
164
- });
165
-
166
- toast.success('Thanks — feedback recorded.');
167
- handleFeedbackClose();
168
- // keep thumbs state
169
- } catch (e: any) {
170
- console.error('[feedback] submit failed:', e);
171
- toast.error(e?.message ? `Feedback failed: ${e.message}` : 'Feedback failed.');
172
- } finally {
173
- setSubmitting(false);
174
- }
175
- };
176
-
177
- return (
178
- <div className={`flex gap-3 ${isUser && !showSenderInfo ? 'justify-end' : 'justify-start'}`}>
179
- {/* Avatar */}
180
- {showSenderInfo && message.sender ? (
181
- <div
182
- className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
183
- message.sender.isAI ? 'overflow-hidden bg-white' : 'bg-muted'
184
- }`}
185
- >
186
- {message.sender.isAI ? (
187
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
188
- ) : (
189
- <span className="text-sm">
190
- {message.sender.name
191
- .split(' ')
192
- .map((n) => n[0])
193
- .join('')
194
- .toUpperCase()}
195
- </span>
196
- )}
197
- </div>
198
- ) : !isUser ? (
199
- <div className="w-8 h-8 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
200
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
201
- </div>
202
- ) : null}
203
-
204
- <div className={`flex flex-col gap-2 max-w-[80%] ${isUser && !showSenderInfo ? 'items-end' : 'items-start'}`}>
205
- {/* Sender name in group chat */}
206
- {showSenderInfo && message.sender && (
207
- <div className="flex items-center gap-2 px-1">
208
- <span className="text-xs">{message.sender.name}</span>
209
- {message.sender.isAI && (
210
- <Badge variant="secondary" className="text-xs h-4 px-1">
211
- AI
212
- </Badge>
213
- )}
214
- </div>
215
- )}
216
-
217
- <div
218
- className={`
219
- rounded-2xl px-4 py-3
220
- ${isUser && !showSenderInfo ? 'bg-primary text-primary-foreground' : 'bg-muted'}
221
- `}
222
- >
223
- {/* ✅ KEY FIX: assistant uses Markdown renderer */}
224
- {isUser ? (
225
- <p className="whitespace-pre-wrap">{message.content}</p>
226
- ) : (
227
- <div className="prose prose-sm max-w-none dark:prose-invert">
228
- <ReactMarkdown
229
- remarkPlugins={[remarkGfm]}
230
- components={{
231
- // keep line breaks looking natural in chat bubbles
232
- p: ({ children }) => <p className="whitespace-pre-wrap">{children}</p>,
233
- // code blocks + inline code styling
234
- code: ({ className, children }) => (
235
- <code className={className ? className : 'px-1 py-0.5 rounded bg-black/5 dark:bg-white/10'}>
236
- {children}
237
- </code>
238
- ),
239
- }}
240
- >
241
- {message.content}
242
- </ReactMarkdown>
243
- </div>
244
- )}
245
- </div>
246
-
247
- {/* References */}
248
- {message.references && message.references.length > 0 && (
249
- <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
250
- <CollapsibleTrigger asChild>
251
- <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
252
- {referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
253
- {message.references.length} {message.references.length === 1 ? 'reference' : 'references'}
254
- </Button>
255
- </CollapsibleTrigger>
256
- <CollapsibleContent className="space-y-1 mt-1">
257
- {message.references.map((ref, index) => (
258
- <Badge key={index} variant="outline" className="text-xs">
259
- {ref}
260
- </Badge>
261
- ))}
262
- </CollapsibleContent>
263
- </Collapsible>
264
- )}
265
-
266
- {/* Message Actions */}
267
- <div className="flex items-center gap-1">
268
- <Button variant="ghost" size="sm" className="h-7 gap-1" onClick={handleCopy}>
269
- {copied ? (
270
- <>
271
- <Check className="h-3 w-3" />
272
- <span className="text-xs">Copied</span>
273
- </>
274
- ) : (
275
- <>
276
- <Copy className="h-3 w-3" />
277
- <span className="text-xs">Copy</span>
278
- </>
279
- )}
280
- </Button>
281
-
282
- {!isUser && (
283
- <>
284
- <Button
285
- variant="ghost"
286
- size="sm"
287
- className={`h-7 gap-1 ${
288
- feedback === 'helpful' ? 'bg-green-100 text-green-600 dark:bg-green-900/20' : ''
289
- }`}
290
- onClick={() => handleFeedbackClick('helpful')}
291
- >
292
- <ThumbsUp className="h-3 w-3" />
293
- <span className="text-xs">Helpful</span>
294
- </Button>
295
-
296
- <Button
297
- variant="ghost"
298
- size="sm"
299
- className={`h-7 gap-1 ${
300
- feedback === 'not_helpful' ? 'bg-red-100 text-red-600 dark:bg-red-900/20' : ''
301
- }`}
302
- onClick={() => handleFeedbackClick('not_helpful')}
303
- >
304
- <ThumbsDown className="h-3 w-3" />
305
- <span className="text-xs">Not helpful</span>
306
- </Button>
307
- </>
308
- )}
309
- </div>
310
-
311
- {/* Feedback Area */}
312
- {!isUser && showFeedbackArea && feedbackType && (
313
- <div className="w-full mt-2 bg-gray-50 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
314
- <div className="flex items-start justify-between mb-4">
315
- <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">Tell us more:</h4>
316
- <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleFeedbackClose}>
317
- <X className="h-4 w-4" />
318
- </Button>
319
- </div>
320
-
321
- {/* Tags */}
322
- <div className="flex flex-wrap gap-2 mb-4">
323
- {FEEDBACK_TAGS[feedbackType].map((tag) => (
324
- <Button
325
- key={tag}
326
- variant={selectedTags.includes(tag) ? 'default' : 'outline'}
327
- size="sm"
328
- className="h-7 text-xs"
329
- onClick={() => handleTagToggle(tag)}
330
- >
331
- {tag}
332
- </Button>
333
- ))}
334
- </div>
335
-
336
- {/* Comment box */}
337
- <Textarea
338
- className="min-h-[60px] mb-4 bg-white dark:bg-gray-900"
339
- value={feedbackText}
340
- onChange={(e) => setFeedbackText(e.target.value)}
341
- placeholder="Additional feedback (optional)..."
342
- />
343
-
344
- {/* Submit */}
345
- <div className="flex justify-end gap-2">
346
- <Button variant="outline" size="sm" onClick={handleFeedbackClose} disabled={submitting}>
347
- Cancel
348
- </Button>
349
- <Button
350
- size="sm"
351
- onClick={handleFeedbackSubmit}
352
- disabled={submitting || (selectedTags.length === 0 && !feedbackText.trim())}
353
- >
354
- {submitting ? 'Submitting…' : 'Submit'}
355
- </Button>
356
- </div>
357
- </div>
358
- )}
359
- </div>
360
-
361
- {isUser && !showSenderInfo && (
362
- <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
363
- <span className="text-sm">👤</span>
364
- </div>
365
- )}
366
- </div>
367
- );
368
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ProfileEditor.tsx DELETED
@@ -1,207 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Button } from './ui/button';
3
- import { Input } from './ui/input';
4
- import { Label } from './ui/label';
5
- import { Textarea } from './ui/textarea';
6
- import { Dialog, DialogContent, DialogTitle } from './ui/dialog';
7
- import type { User as UserType } from '../App';
8
- import { toast } from 'sonner';
9
- import {
10
- Select,
11
- SelectContent,
12
- SelectItem,
13
- SelectTrigger,
14
- SelectValue,
15
- } from './ui/select';
16
-
17
- interface ProfileEditorProps {
18
- user: UserType;
19
- onSave: (user: UserType) => void;
20
- onClose: () => void;
21
- }
22
-
23
- export function ProfileEditor({ user, onSave, onClose }: ProfileEditorProps) {
24
- const [name, setName] = useState(user.name);
25
- const [email, setEmail] = useState(user.email);
26
- const [studentId, setStudentId] = useState('S12345678');
27
- const [department, setDepartment] = useState('Computer Science');
28
- const [year, setYear] = useState('3rd Year');
29
- const [major, setMajor] = useState('Artificial Intelligence');
30
- const [bio, setBio] = useState('Passionate about AI and machine learning');
31
-
32
- const handleSave = () => {
33
- if (name.trim() && email.trim()) {
34
- onSave({ name: name.trim(), email: email.trim() });
35
- toast.success('Profile updated successfully!');
36
- onClose();
37
- } else {
38
- toast.error('Please fill in all required fields');
39
- }
40
- };
41
-
42
- return (
43
- <Dialog open onOpenChange={(open) => { if (!open) onClose(); }}>
44
- <DialogContent className="sm:max-w-2xl p-0 gap-0 max-h-[90vh] overflow-hidden">
45
- <div className="flex flex-col max-h-[90vh]">
46
- {/* Header */}
47
- <div className="border-b border-border p-4 flex items-center justify-between flex-shrink-0">
48
- <DialogTitle className="text-xl font-medium">Edit Profile</DialogTitle>
49
- </div>
50
-
51
- {/* Content */}
52
- <div className="p-6 space-y-6 overflow-y-auto flex-1">
53
- {/* Profile Picture */}
54
- <div className="flex items-center gap-4">
55
- <div className="w-20 h-20 rounded-full bg-gradient-to-br from-red-500 to-orange-500 flex items-center justify-center text-white text-2xl">
56
- {name.charAt(0).toUpperCase()}
57
- </div>
58
- <div>
59
- <Button variant="outline" size="sm">
60
- Change Photo
61
- </Button>
62
- <p className="text-xs text-muted-foreground mt-1">
63
- JPG, PNG or GIF. Max size 2MB
64
- </p>
65
- </div>
66
- </div>
67
-
68
- {/* Basic Information */}
69
- <div className="space-y-4">
70
- <h3 className="text-sm font-medium">Basic Information</h3>
71
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
72
- <div className="space-y-2">
73
- <Label htmlFor="edit-name">Full Name *</Label>
74
- <Input
75
- id="edit-name"
76
- value={name}
77
- onChange={(e) => setName(e.target.value)}
78
- placeholder="Enter your full name"
79
- />
80
- </div>
81
- <div className="space-y-2">
82
- <Label htmlFor="edit-email">Email *</Label>
83
- <Input
84
- id="edit-email"
85
- type="email"
86
- value={email}
87
- onChange={(e) => setEmail(e.target.value)}
88
- placeholder="Enter your email"
89
- />
90
- </div>
91
- <div className="space-y-2">
92
- <Label htmlFor="edit-student-id">Student ID</Label>
93
- <Input
94
- id="edit-student-id"
95
- value={studentId}
96
- onChange={(e) => setStudentId(e.target.value)}
97
- placeholder="Enter your student ID"
98
- />
99
- </div>
100
- <div className="space-y-2">
101
- <Label htmlFor="edit-department">Department</Label>
102
- <Input
103
- id="edit-department"
104
- value={department}
105
- onChange={(e) => setDepartment(e.target.value)}
106
- placeholder="Enter your department"
107
- />
108
- </div>
109
- </div>
110
- </div>
111
-
112
- {/* Academic Background */}
113
- <div className="space-y-4">
114
- <h3 className="text-sm font-medium">Academic Background</h3>
115
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
116
- <div className="space-y-2">
117
- <Label htmlFor="edit-year">Year Level</Label>
118
- <Select value={year} onValueChange={setYear}>
119
- <SelectTrigger id="edit-year">
120
- <SelectValue />
121
- </SelectTrigger>
122
- <SelectContent>
123
- <SelectItem value="1st Year">1st Year</SelectItem>
124
- <SelectItem value="2nd Year">2nd Year</SelectItem>
125
- <SelectItem value="3rd Year">3rd Year</SelectItem>
126
- <SelectItem value="4th Year">4th Year</SelectItem>
127
- <SelectItem value="Graduate">Graduate</SelectItem>
128
- </SelectContent>
129
- </Select>
130
- </div>
131
- <div className="space-y-2">
132
- <Label htmlFor="edit-major">Major</Label>
133
- <Input
134
- id="edit-major"
135
- value={major}
136
- onChange={(e) => setMajor(e.target.value)}
137
- placeholder="Enter your major"
138
- />
139
- </div>
140
- </div>
141
- </div>
142
-
143
- {/* Bio */}
144
- <div className="space-y-2">
145
- <Label htmlFor="edit-bio">Bio</Label>
146
- <Textarea
147
- id="edit-bio"
148
- value={bio}
149
- onChange={(e) => setBio(e.target.value)}
150
- placeholder="Tell us about yourself..."
151
- className="min-h-[100px] resize-none"
152
- />
153
- <p className="text-xs text-muted-foreground">
154
- Brief description for your profile. Max 200 characters.
155
- </p>
156
- </div>
157
-
158
- {/* Learning Preferences */}
159
- <div className="space-y-4">
160
- <h3 className="text-sm font-medium">Learning Preferences</h3>
161
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
162
- <div className="space-y-2">
163
- <Label htmlFor="edit-learning-style">Preferred Learning Style</Label>
164
- <Select defaultValue="visual">
165
- <SelectTrigger id="edit-learning-style">
166
- <SelectValue />
167
- </SelectTrigger>
168
- <SelectContent>
169
- <SelectItem value="visual">Visual</SelectItem>
170
- <SelectItem value="auditory">Auditory</SelectItem>
171
- <SelectItem value="reading">Reading/Writing</SelectItem>
172
- <SelectItem value="kinesthetic">Kinesthetic</SelectItem>
173
- </SelectContent>
174
- </Select>
175
- </div>
176
- <div className="space-y-2">
177
- <Label htmlFor="edit-pace">Learning Pace</Label>
178
- <Select defaultValue="moderate">
179
- <SelectTrigger id="edit-pace">
180
- <SelectValue />
181
- </SelectTrigger>
182
- <SelectContent>
183
- <SelectItem value="slow">Slow & Steady</SelectItem>
184
- <SelectItem value="moderate">Moderate</SelectItem>
185
- <SelectItem value="fast">Fast-paced</SelectItem>
186
- </SelectContent>
187
- </Select>
188
- </div>
189
- </div>
190
- </div>
191
-
192
- </div>
193
-
194
- {/* Footer */}
195
- <div className="border-t border-border p-4 flex justify-end gap-2 flex-shrink-0">
196
- <Button variant="outline" onClick={onClose}>
197
- Cancel
198
- </Button>
199
- <Button onClick={handleSave}>
200
- Save Changes
201
- </Button>
202
- </div>
203
- </div>
204
- </DialogContent>
205
- </Dialog>
206
- );
207
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/RightPanel.tsx DELETED
@@ -1,281 +0,0 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { Button } from './ui/button';
3
- import { Input } from './ui/input';
4
- import { Label } from './ui/label';
5
- import { Card } from './ui/card';
6
- import { Separator } from './ui/separator';
7
- import { Textarea } from './ui/textarea';
8
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
9
- import {
10
- LogIn,
11
- LogOut,
12
- Download,
13
- ClipboardList,
14
- FileText,
15
- Sparkles,
16
- ChevronUp,
17
- ChevronDown,
18
- PanelRightClose
19
- } from 'lucide-react';
20
- import { Document, HeadingLevel, Packer, Paragraph, TextRun } from 'docx';
21
- import type { User } from '../App';
22
- import { toast } from 'sonner';
23
- import {
24
- Dialog,
25
- DialogContent,
26
- DialogDescription,
27
- DialogHeader,
28
- DialogTitle,
29
- DialogTrigger,
30
- DialogFooter,
31
- } from './ui/dialog';
32
-
33
- interface RightPanelProps {
34
- user: User | null;
35
- onLogin: (user: User) => void;
36
- onLogout: () => void;
37
- isLoggedIn: boolean;
38
- onClose?: () => void;
39
- exportResult: string;
40
- setExportResult: (result: string) => void;
41
- resultType: 'export' | 'quiz' | 'summary' | null;
42
- setResultType: (type: 'export' | 'quiz' | 'summary' | null) => void;
43
- onExport: () => void;
44
- onSummary: () => void;
45
- }
46
-
47
- export function RightPanel({ user, onLogin, onLogout, isLoggedIn, onClose, exportResult, setExportResult, resultType, setResultType, onExport, onSummary }: RightPanelProps) {
48
- const [showLoginForm, setShowLoginForm] = useState(false);
49
- const [name, setName] = useState('');
50
- const [email, setEmail] = useState('');
51
- const [isExpanded, setIsExpanded] = useState(true);
52
- const [isDownloading, setIsDownloading] = useState(false);
53
-
54
- const handleLogin = () => {
55
- if (!name.trim() || !email.trim()) {
56
- toast.error('Please fill in all fields');
57
- return;
58
- }
59
-
60
- onLogin({ name: name.trim(), email: email.trim() });
61
- setShowLoginForm(false);
62
- setName('');
63
- setEmail('');
64
- toast.success(`Welcome, ${name}!`);
65
- };
66
-
67
- const handleLogout = () => {
68
- onLogout();
69
- setShowLoginForm(false);
70
- toast.success('Logged out successfully');
71
- };
72
-
73
- const scrollContainerRef = useRef<HTMLDivElement>(null);
74
-
75
- // Use native event listeners to prevent scroll propagation
76
- useEffect(() => {
77
- const container = scrollContainerRef.current;
78
- if (!container) return;
79
-
80
- const handleWheel = (e: WheelEvent) => {
81
- // Always stop propagation to prevent scrolling other panels
82
- e.stopPropagation();
83
- e.stopImmediatePropagation();
84
-
85
- // Only prevent default if we're at the boundaries
86
- const { scrollTop, scrollHeight, clientHeight } = container;
87
- const isScrollable = scrollHeight > clientHeight;
88
- const isAtTop = scrollTop === 0;
89
- const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
90
-
91
- // If scrolling up at top or down at bottom, prevent default to stop propagation
92
- if (isScrollable && ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0))) {
93
- e.preventDefault();
94
- }
95
- };
96
-
97
- container.addEventListener('wheel', handleWheel, { passive: false, capture: true });
98
-
99
- return () => {
100
- container.removeEventListener('wheel', handleWheel, { capture: true });
101
- };
102
- }, []);
103
-
104
- const downloadBlob = (blob: Blob, filename: string) => {
105
- const url = URL.createObjectURL(blob);
106
- const a = document.createElement('a');
107
- a.href = url;
108
- a.download = filename;
109
- document.body.appendChild(a);
110
- a.click();
111
- a.remove();
112
- URL.revokeObjectURL(url);
113
- };
114
-
115
- const formatDateStamp = () => {
116
- const d = new Date();
117
- const yyyy = d.getFullYear();
118
- const mm = String(d.getMonth() + 1).padStart(2, '0');
119
- const dd = String(d.getDate()).padStart(2, '0');
120
- return `${yyyy}-${mm}-${dd}`;
121
- };
122
-
123
- const getDefaultFilenameBase = () => {
124
- const kind =
125
- resultType === 'export' ? 'export' :
126
- resultType === 'summary' ? 'summary' :
127
- 'result';
128
- return `clare-${kind}-${formatDateStamp()}`;
129
- };
130
-
131
- const handleDownloadMd = async () => {
132
- if (!exportResult) return;
133
- try {
134
- setIsDownloading(true);
135
- toast.message('Preparing .md…');
136
- const blob = new Blob([exportResult], { type: 'text/markdown;charset=utf-8' });
137
- downloadBlob(blob, `${getDefaultFilenameBase()}.md`);
138
- toast.success('Downloaded .md');
139
- } catch (e) {
140
- console.error(e);
141
- toast.error('Failed to download .md');
142
- } finally {
143
- setIsDownloading(false);
144
- }
145
- };
146
-
147
- const handleDownloadDocx = async () => {
148
- if (!exportResult) return;
149
- try {
150
- setIsDownloading(true);
151
- toast.message('Preparing .docx…');
152
-
153
- const lines = exportResult.split('\n');
154
- const paragraphs: Paragraph[] = lines.map((line) => {
155
- const trimmed = line.trim();
156
- if (!trimmed) return new Paragraph({ text: '' });
157
-
158
- // Basic markdown-ish heading support
159
- if (trimmed.startsWith('### ')) {
160
- return new Paragraph({ text: trimmed.replace(/^###\s+/, ''), heading: HeadingLevel.HEADING_3 });
161
- }
162
- if (trimmed.startsWith('## ')) {
163
- return new Paragraph({ text: trimmed.replace(/^##\s+/, ''), heading: HeadingLevel.HEADING_2 });
164
- }
165
- if (trimmed.startsWith('# ')) {
166
- return new Paragraph({ text: trimmed.replace(/^#\s+/, ''), heading: HeadingLevel.HEADING_1 });
167
- }
168
-
169
- return new Paragraph({ children: [new TextRun({ text: line })] });
170
- });
171
-
172
- const doc = new Document({
173
- sections: [{ properties: {}, children: paragraphs }],
174
- });
175
-
176
- const blob = await Packer.toBlob(doc);
177
- downloadBlob(blob, `${getDefaultFilenameBase()}.docx`);
178
- toast.success('Downloaded .docx');
179
- } catch (e) {
180
- console.error(e);
181
- toast.error('Failed to download .docx');
182
- } finally {
183
- setIsDownloading(false);
184
- }
185
- };
186
-
187
- return (
188
- <div
189
- ref={scrollContainerRef}
190
- className="flex-1 overflow-auto overscroll-contain flex flex-col"
191
- style={{ overscrollBehavior: 'contain' }}
192
- >
193
- <div className="p-4 space-y-4">
194
- {isExpanded && (
195
- <>
196
- {/* Actions Section with Results */}
197
- <div className="space-y-3">
198
- <h3 className="text-base font-medium">Export / Summarize Conversation</h3>
199
- <Card className="p-4 bg-muted/30">
200
- <div className="flex flex-col gap-3">
201
- <Button
202
- variant="outline"
203
- className="w-full h-12 rounded-lg justify-start gap-3"
204
- onClick={onExport}
205
- disabled={!isLoggedIn}
206
- >
207
- <Download className="h-5 w-5" />
208
- <span>Export</span>
209
- </Button>
210
- <Button
211
- variant="outline"
212
- className="w-full h-12 rounded-lg justify-start gap-3"
213
- onClick={onSummary}
214
- disabled={!isLoggedIn}
215
- >
216
- <Sparkles className="h-5 w-5" />
217
- <span>Summarize</span>
218
- </Button>
219
-
220
- {/* Results - Expanded from buttons */}
221
- {exportResult && (
222
- <>
223
- <Separator className="my-2" />
224
- <div className="space-y-3">
225
- <div className="flex items-center justify-between">
226
- <h4 className="text-base font-bold">
227
- {resultType === 'export' && 'Exported Conversation'}
228
- {resultType === 'quiz' && 'Micro-Quiz'}
229
- {resultType === 'summary' && 'Summarization'}
230
- </h4>
231
- </div>
232
- <div className="flex items-center justify-end gap-2">
233
- <Button
234
- variant="outline"
235
- size="sm"
236
- disabled={isDownloading}
237
- onClick={handleDownloadMd}
238
- title="Download as .md"
239
- className="gap-2"
240
- >
241
- <Download className="h-4 w-4" />
242
- .md
243
- </Button>
244
- <Button
245
- variant="outline"
246
- size="sm"
247
- disabled={isDownloading}
248
- onClick={handleDownloadDocx}
249
- title="Download as .docx"
250
- className="gap-2"
251
- >
252
- <Download className="h-4 w-4" />
253
- .docx
254
- </Button>
255
- <Button
256
- variant="outline"
257
- size="sm"
258
- onClick={() => {
259
- navigator.clipboard.writeText(exportResult);
260
- toast.success('Copied to clipboard!');
261
- }}
262
- disabled={isDownloading}
263
- >
264
- Copy
265
- </Button>
266
- </div>
267
- <div className="text-sm whitespace-pre-wrap text-foreground">
268
- {exportResult}
269
- </div>
270
- </div>
271
- </>
272
- )}
273
- </div>
274
- </Card>
275
- </div>
276
- </>
277
- )}
278
- </div>
279
- </div>
280
- );
281
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/SmartReview.tsx DELETED
@@ -1,263 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Brain, AlertTriangle, AlertCircle, CheckCircle, ChevronDown, ChevronRight, Info } from 'lucide-react';
3
- import { Badge } from './ui/badge';
4
- import { Button } from './ui/button';
5
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
6
- import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
7
-
8
- interface ReviewItem {
9
- id: string;
10
- title: string;
11
- schedule: string;
12
- status: 'urgent' | 'review' | 'stable';
13
- weight: number;
14
- lastReviewed: string;
15
- memoryRetention: number;
16
- previousQuestion: string;
17
- }
18
-
19
- export function SmartReview() {
20
- // Initialize with the first item of W-4 (red zone) expanded by default
21
- const [expandedItems, setExpandedItems] = useState<string[]>(['w4-1']);
22
- const [selectedCategory, setSelectedCategory] = useState<string>('W-4'); // Default to red zone
23
-
24
- const reviewData = [
25
- {
26
- label: 'W-4',
27
- percentage: 35,
28
- color: 'bg-red-500',
29
- textColor: 'text-red-500',
30
- icon: AlertTriangle,
31
- description: 'Higher forgetting risk',
32
- items: [
33
- {
34
- id: 'w4-1',
35
- title: 'Main Concept of Lab 3',
36
- schedule: 'T+7',
37
- status: 'urgent' as const,
38
- weight: 35,
39
- lastReviewed: '7 days ago',
40
- memoryRetention: 25,
41
- previousQuestion: 'I\'ve read the instructions for Lab 3, but what is the main concept we\'re supposed to be learning here?'
42
- }
43
- ]
44
- },
45
- {
46
- label: 'W-2',
47
- percentage: 20,
48
- color: 'bg-orange-500',
49
- textColor: 'text-orange-500',
50
- icon: AlertCircle,
51
- description: 'Medium forgetting risk',
52
- items: [
53
- {
54
- id: 'w2-1',
55
- title: 'Effective Prompt Engineering',
56
- schedule: 'T+14',
57
- status: 'review' as const,
58
- weight: 20,
59
- lastReviewed: '3 days ago',
60
- memoryRetention: 60,
61
- previousQuestion: 'I understand what prompt engineering is, but what specifically makes a prompt effective versus ineffective?'
62
- }
63
- ]
64
- },
65
- {
66
- label: 'W-1',
67
- percentage: 12,
68
- color: 'bg-green-500',
69
- textColor: 'text-green-500',
70
- icon: CheckCircle,
71
- description: 'Recently learned',
72
- items: [
73
- {
74
- id: 'w1-1',
75
- title: 'Objective LLM Evaluation',
76
- schedule: 'T+7',
77
- status: 'stable' as const,
78
- weight: 12,
79
- lastReviewed: '1 day ago',
80
- memoryRetention: 90,
81
- previousQuestion: 'How can we objectively evaluate an LLM\'s performance when the output quality seems so subjective?'
82
- }
83
- ]
84
- },
85
- ];
86
-
87
- const totalPercentage = reviewData.reduce((sum, item) => sum + item.percentage, 0);
88
- const selectedData = reviewData.find(item => item.label === selectedCategory);
89
-
90
- const toggleItem = (itemId: string) => {
91
- setExpandedItems(prev =>
92
- prev.includes(itemId)
93
- ? prev.filter(id => id !== itemId)
94
- : [...prev, itemId]
95
- );
96
- };
97
-
98
- // When category changes, automatically expand the first item of that category
99
- const handleCategoryChange = (categoryLabel: string) => {
100
- setSelectedCategory(categoryLabel);
101
- const categoryData = reviewData.find(item => item.label === categoryLabel);
102
- if (categoryData && categoryData.items.length > 0) {
103
- // Expand only the first item of the selected category
104
- setExpandedItems([categoryData.items[0].id]);
105
- }
106
- };
107
-
108
- const getStatusBadge = (status: 'urgent' | 'review' | 'stable') => {
109
- const configs = {
110
- urgent: { label: '🔥 URGENT', className: 'bg-red-500 text-white hover:bg-red-600' },
111
- review: { label: '⚠️ REVIEW', className: 'bg-orange-500 text-white hover:bg-orange-600' },
112
- stable: { label: '✓ STABLE', className: 'bg-green-500 text-white hover:bg-green-600' },
113
- };
114
- return configs[status];
115
- };
116
-
117
- const getButtonColorClass = (colorClass: string) => {
118
- // Extract color from bg-red-500, bg-orange-500, bg-green-500
119
- if (colorClass.includes('red')) {
120
- return 'bg-red-500 hover:bg-red-600';
121
- } else if (colorClass.includes('orange')) {
122
- return 'bg-orange-500 hover:bg-orange-600';
123
- } else if (colorClass.includes('green')) {
124
- return 'bg-green-500 hover:bg-green-600';
125
- }
126
- return 'bg-red-500 hover:bg-red-600'; // default
127
- };
128
-
129
- return (
130
- <div className="space-y-4">
131
- <div className="flex flex-col gap-1">
132
- <div className="flex items-center gap-2">
133
- <Brain className="h-5 w-5 text-red-500" />
134
- <h3>Current Review Distribution</h3>
135
- <Tooltip>
136
- <TooltipTrigger asChild>
137
- <button className="text-muted-foreground hover:text-foreground transition-colors">
138
- <Info className="h-3 w-3" />
139
- </button>
140
- </TooltipTrigger>
141
- <TooltipContent side="right" className="p-2" style={{ maxWidth: '170px', whiteSpace: 'normal', wordBreak: 'break-word' }}>
142
- <p className="text-[10px] leading-relaxed text-left" style={{ whiteSpace: 'normal' }}>
143
- Based on the forgetting curve, Clare has selected topics you might be forgetting from your learning history and interaction patterns. Higher weights indicate higher forgetting risk.
144
- </p>
145
- </TooltipContent>
146
- </Tooltip>
147
- </div>
148
- <div className="text-xs text-muted-foreground ml-7">(T+7 Schedule)</div>
149
- </div>
150
-
151
- {/* Combined Progress Bar - Clickable */}
152
- <div className="space-y-2">
153
- <div className="text-xs text-muted-foreground">Overall Distribution</div>
154
- <div className="flex items-center gap-3">
155
- {reviewData.map((item) => (
156
- <button
157
- key={item.label}
158
- className={`flex flex-col gap-1.5 pt-2 px-1 pb-0 rounded-lg transition-all duration-200 hover:brightness-110 focus:outline-none cursor-pointer ${
159
- selectedCategory === item.label ? 'bg-muted/80' : 'bg-transparent hover:bg-muted/40'
160
- }`}
161
- onClick={() => handleCategoryChange(item.label)}
162
- title={`Click to view ${item.label} items`}
163
- style={{ flex: item.percentage }}
164
- >
165
- <div className="flex items-center gap-1 justify-center whitespace-nowrap">
166
- <span className="text-[10px]">{item.label}:</span>
167
- <span className={`text-[10px] font-medium ${item.textColor}`}>{item.percentage}%</span>
168
- </div>
169
- <div className={`h-2 ${item.color} rounded-full mb-2`} />
170
- </button>
171
- ))}
172
- </div>
173
- </div>
174
-
175
- {/* Selected Category Progress Bar with Expandable Items */}
176
- {selectedData && (
177
- <div className="space-y-2">
178
- <div className="flex items-center justify-between">
179
- <div className="flex items-center gap-2">
180
- <selectedData.icon className={`h-4 w-4 ${selectedData.textColor}`} />
181
- <span className="text-sm">{selectedData.label}:</span>
182
- <span className={`text-sm font-medium ${selectedData.textColor}`}>{selectedData.percentage}%</span>
183
- </div>
184
- </div>
185
- <div className="h-2 bg-muted rounded-full overflow-hidden">
186
- <div
187
- className={`h-full ${selectedData.color} transition-all duration-500`}
188
- style={{ width: `${(selectedData.percentage / totalPercentage) * 100}%` }}
189
- />
190
- </div>
191
-
192
- {/* Expandable Review Items */}
193
- <div className="space-y-2">
194
- {selectedData.items.map((reviewItem) => (
195
- <Collapsible
196
- key={reviewItem.id}
197
- open={expandedItems.includes(reviewItem.id)}
198
- onOpenChange={() => toggleItem(reviewItem.id)}
199
- >
200
- <CollapsibleTrigger asChild>
201
- <Button
202
- variant="ghost"
203
- className="w-full justify-between text-left h-auto p-2 hover:bg-muted/50"
204
- >
205
- <span className="text-sm">Review: {reviewItem.title}</span>
206
- {expandedItems.includes(reviewItem.id) ? (
207
- <ChevronDown className="h-4 w-4 flex-shrink-0" />
208
- ) : (
209
- <ChevronRight className="h-4 w-4 flex-shrink-0" />
210
- )}
211
- </Button>
212
- </CollapsibleTrigger>
213
- <CollapsibleContent>
214
- <div className="p-3 space-y-3 pl-4" style={{ borderLeft: '0.5px solid', borderColor: `var(--${selectedData.color.replace('bg-', '')})` }}>
215
- <div className="flex items-center gap-2 flex-wrap">
216
- <Badge variant="outline" className="text-xs">
217
- {reviewItem.schedule}
218
- </Badge>
219
- <Badge className={`text-xs ${getStatusBadge(reviewItem.status).className}`}>
220
- {getStatusBadge(reviewItem.status).label}
221
- </Badge>
222
- <span className="text-xs text-muted-foreground">
223
- Weight: {reviewItem.weight}% | Last: {reviewItem.lastReviewed}
224
- </span>
225
- </div>
226
-
227
- <div className="space-y-1">
228
- <div className="flex justify-between items-center">
229
- <span className="text-xs text-muted-foreground">Memory Retention</span>
230
- <span className="text-xs font-medium">{reviewItem.memoryRetention}%</span>
231
- </div>
232
- <div className="h-2 bg-muted rounded-full overflow-hidden">
233
- <div
234
- className={`h-full ${selectedData.color} transition-all duration-500`}
235
- style={{ width: `${reviewItem.memoryRetention}%` }}
236
- />
237
- </div>
238
- </div>
239
-
240
- <div className="space-y-1">
241
- <p className="text-xs text-muted-foreground">You previously asked:</p>
242
- <p className="text-xs bg-muted/50 p-2 rounded italic">
243
- "{reviewItem.previousQuestion}"
244
- </p>
245
- </div>
246
-
247
- <Button
248
- className={`w-full ${getButtonColorClass(selectedData.color)} text-white`}
249
- size="sm"
250
- >
251
- Review this topic
252
- </Button>
253
- </div>
254
- </CollapsibleContent>
255
- </Collapsible>
256
- ))}
257
- </div>
258
- </div>
259
- )}
260
-
261
- </div>
262
- );
263
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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";
5
- import { XIcon } from "lucide-react";
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-[100] 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-[100] 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";
5
- import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
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-[100] 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 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/scroll-area.tsx DELETED
@@ -1,58 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area@1.2.3";
5
-
6
- import { cn } from "./utils";
7
-
8
- function ScrollArea({
9
- className,
10
- children,
11
- ...props
12
- }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
13
- return (
14
- <ScrollAreaPrimitive.Root
15
- data-slot="scroll-area"
16
- className={cn("relative", className)}
17
- {...props}
18
- >
19
- <ScrollAreaPrimitive.Viewport
20
- data-slot="scroll-area-viewport"
21
- className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
22
- >
23
- {children}
24
- </ScrollAreaPrimitive.Viewport>
25
- <ScrollBar />
26
- <ScrollAreaPrimitive.Corner />
27
- </ScrollAreaPrimitive.Root>
28
- );
29
- }
30
-
31
- function ScrollBar({
32
- className,
33
- orientation = "vertical",
34
- ...props
35
- }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
36
- return (
37
- <ScrollAreaPrimitive.ScrollAreaScrollbar
38
- data-slot="scroll-area-scrollbar"
39
- orientation={orientation}
40
- className={cn(
41
- "flex touch-none p-px transition-colors select-none",
42
- orientation === "vertical" &&
43
- "h-full w-2.5 border-l border-l-transparent",
44
- orientation === "horizontal" &&
45
- "h-2.5 flex-col border-t border-t-transparent",
46
- className,
47
- )}
48
- {...props}
49
- >
50
- <ScrollAreaPrimitive.ScrollAreaThumb
51
- data-slot="scroll-area-thumb"
52
- className="bg-border relative flex-1 rounded-full"
53
- />
54
- </ScrollAreaPrimitive.ScrollAreaScrollbar>
55
- );
56
- }
57
-
58
- export { ScrollArea, ScrollBar };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
web/src/components/ui/select.tsx DELETED
@@ -1,189 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as SelectPrimitive from "@radix-ui/react-select@2.1.6";
5
- import {
6
- CheckIcon,
7
- ChevronDownIcon,
8
- ChevronUpIcon,
9
- } from "lucide-react@0.487.0";
10
-
11
- import { cn } from "./utils";
12
-
13
- function Select({
14
- ...props
15
- }: React.ComponentProps<typeof SelectPrimitive.Root>) {
16
- return <SelectPrimitive.Root data-slot="select" {...props} />;
17
- }
18
-
19
- function SelectGroup({
20
- ...props
21
- }: React.ComponentProps<typeof SelectPrimitive.Group>) {
22
- return <SelectPrimitive.Group data-slot="select-group" {...props} />;
23
- }
24
-
25
- function SelectValue({
26
- ...props
27
- }: React.ComponentProps<typeof SelectPrimitive.Value>) {
28
- return <SelectPrimitive.Value data-slot="select-value" {...props} />;
29
- }
30
-
31
- function SelectTrigger({
32
- className,
33
- size = "default",
34
- children,
35
- ...props
36
- }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
37
- size?: "sm" | "default";
38
- }) {
39
- return (
40
- <SelectPrimitive.Trigger
41
- data-slot="select-trigger"
42
- data-size={size}
43
- className={cn(
44
- "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
45
- className,
46
- )}
47
- {...props}
48
- >
49
- {children}
50
- <SelectPrimitive.Icon asChild>
51
- <ChevronDownIcon className="size-4 opacity-50" />
52
- </SelectPrimitive.Icon>
53
- </SelectPrimitive.Trigger>
54
- );
55
- }
56
-
57
- function SelectContent({
58
- className,
59
- children,
60
- position = "popper",
61
- ...props
62
- }: React.ComponentProps<typeof SelectPrimitive.Content>) {
63
- return (
64
- <SelectPrimitive.Portal>
65
- <SelectPrimitive.Content
66
- data-slot="select-content"
67
- className={cn(
68
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
69
- position === "popper" &&
70
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
71
- className,
72
- )}
73
- position={position}
74
- {...props}
75
- >
76
- <SelectScrollUpButton />
77
- <SelectPrimitive.Viewport
78
- className={cn(
79
- "p-1",
80
- position === "popper" &&
81
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
82
- )}
83
- >
84
- {children}
85
- </SelectPrimitive.Viewport>
86
- <SelectScrollDownButton />
87
- </SelectPrimitive.Content>
88
- </SelectPrimitive.Portal>
89
- );
90
- }
91
-
92
- function SelectLabel({
93
- className,
94
- ...props
95
- }: React.ComponentProps<typeof SelectPrimitive.Label>) {
96
- return (
97
- <SelectPrimitive.Label
98
- data-slot="select-label"
99
- className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
100
- {...props}
101
- />
102
- );
103
- }
104
-
105
- function SelectItem({
106
- className,
107
- children,
108
- ...props
109
- }: React.ComponentProps<typeof SelectPrimitive.Item>) {
110
- return (
111
- <SelectPrimitive.Item
112
- data-slot="select-item"
113
- className={cn(
114
- "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
115
- className,
116
- )}
117
- {...props}
118
- >
119
- <span className="absolute right-2 flex size-3.5 items-center justify-center">
120
- <SelectPrimitive.ItemIndicator>
121
- <CheckIcon className="size-4" />
122
- </SelectPrimitive.ItemIndicator>
123
- </span>
124
- <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
125
- </SelectPrimitive.Item>
126
- );
127
- }
128
-
129
- function SelectSeparator({
130
- className,
131
- ...props
132
- }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
133
- return (
134
- <SelectPrimitive.Separator
135
- data-slot="select-separator"
136
- className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
137
- {...props}
138
- />
139
- );
140
- }
141
-
142
- function SelectScrollUpButton({
143
- className,
144
- ...props
145
- }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
146
- return (
147
- <SelectPrimitive.ScrollUpButton
148
- data-slot="select-scroll-up-button"
149
- className={cn(
150
- "flex cursor-default items-center justify-center py-1",
151
- className,
152
- )}
153
- {...props}
154
- >
155
- <ChevronUpIcon className="size-4" />
156
- </SelectPrimitive.ScrollUpButton>
157
- );
158
- }
159
-
160
- function SelectScrollDownButton({
161
- className,
162
- ...props
163
- }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
164
- return (
165
- <SelectPrimitive.ScrollDownButton
166
- data-slot="select-scroll-down-button"
167
- className={cn(
168
- "flex cursor-default items-center justify-center py-1",
169
- className,
170
- )}
171
- {...props}
172
- >
173
- <ChevronDownIcon className="size-4" />
174
- </SelectPrimitive.ScrollDownButton>
175
- );
176
- }
177
-
178
- export {
179
- Select,
180
- SelectContent,
181
- SelectGroup,
182
- SelectItem,
183
- SelectLabel,
184
- SelectScrollDownButton,
185
- SelectScrollUpButton,
186
- SelectSeparator,
187
- SelectTrigger,
188
- SelectValue,
189
- };