Spaces:
Sleeping
Sleeping
Delete web/src
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- web/src/App.tsx +0 -531
- web/src/Attributions.md +0 -3
- web/src/assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png +0 -0
- web/src/components/ChatArea.tsx +0 -358
- web/src/components/FileUploadArea.tsx +0 -273
- web/src/components/FloatingActionButtons.tsx +0 -102
- web/src/components/GroupMembers.tsx +0 -60
- web/src/components/Header.tsx +0 -152
- web/src/components/LearningModeSelector.tsx +0 -93
- web/src/components/LeftSidebar.tsx +0 -243
- web/src/components/LoginScreen.tsx +0 -125
- web/src/components/Message.tsx +0 -368
- web/src/components/ProfileEditor.tsx +0 -207
- web/src/components/RightPanel.tsx +0 -281
- web/src/components/SmartReview.tsx +0 -263
- web/src/components/UserGuide.tsx +0 -151
- web/src/components/figma/ImageWithFallback.tsx +0 -27
- web/src/components/ui/accordion.tsx +0 -66
- web/src/components/ui/alert-dialog.tsx +0 -157
- web/src/components/ui/alert.tsx +0 -66
- web/src/components/ui/aspect-ratio.tsx +0 -11
- web/src/components/ui/avatar.tsx +0 -53
- web/src/components/ui/badge.tsx +0 -46
- web/src/components/ui/breadcrumb.tsx +0 -109
- web/src/components/ui/button.tsx +0 -58
- web/src/components/ui/calendar.tsx +0 -75
- web/src/components/ui/card.tsx +0 -92
- web/src/components/ui/carousel.tsx +0 -241
- web/src/components/ui/chart.tsx +0 -353
- web/src/components/ui/checkbox.tsx +0 -32
- web/src/components/ui/collapsible.tsx +0 -33
- web/src/components/ui/command.tsx +0 -177
- web/src/components/ui/context-menu.tsx +0 -252
- web/src/components/ui/dialog.tsx +0 -137
- web/src/components/ui/drawer.tsx +0 -132
- web/src/components/ui/dropdown-menu.tsx +0 -257
- web/src/components/ui/form.tsx +0 -168
- web/src/components/ui/hover-card.tsx +0 -44
- web/src/components/ui/input-otp.tsx +0 -77
- web/src/components/ui/input.tsx +0 -21
- web/src/components/ui/label.tsx +0 -24
- web/src/components/ui/menubar.tsx +0 -276
- web/src/components/ui/navigation-menu.tsx +0 -168
- web/src/components/ui/pagination.tsx +0 -127
- web/src/components/ui/popover.tsx +0 -48
- web/src/components/ui/progress.tsx +0 -31
- web/src/components/ui/radio-group.tsx +0 -45
- web/src/components/ui/resizable.tsx +0 -56
- web/src/components/ui/scroll-area.tsx +0 -58
- 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 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|