SarahXia0405 commited on
Commit
b180cdf
·
verified ·
1 Parent(s): c6a412f

Update web/src/components/Header.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Header.tsx +276 -290
web/src/components/Header.tsx CHANGED
@@ -1,23 +1,31 @@
1
- import React, { useState } from 'react';
2
- import { Button } from './ui/button';
3
- import { Menu, Sun, Moon, Languages, ChevronDown, LogOut, Plus, X, User, Edit, Star } from 'lucide-react';
4
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
 
5
  import {
6
  DropdownMenu,
7
  DropdownMenuContent,
8
  DropdownMenuItem,
9
  DropdownMenuTrigger,
10
  DropdownMenuSeparator,
11
- } from './ui/dropdown-menu';
12
- import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
13
- import type { Workspace, CourseInfo } from '../App';
14
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog';
15
- import { Input } from './ui/input';
16
- import { Label } from './ui/label';
17
- import { RadioGroup, RadioGroupItem } from './ui/radio-group';
18
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
19
- import { toast } from 'sonner';
20
- import { ProfileEditor } from './ProfileEditor';
 
 
 
 
 
 
 
21
 
22
  interface HeaderProps {
23
  user: UserType | null;
@@ -32,21 +40,19 @@ interface HeaderProps {
32
  onWorkspaceChange: (workspaceId: string) => void;
33
  onCreateWorkspace?: (payload: {
34
  name: string;
35
- category: 'course' | 'personal';
36
  courseId?: string;
37
  invites: string[];
38
  }) => void;
39
  onLogout: () => void;
40
  availableCourses?: CourseInfo[];
41
  onUserUpdate?: (user: UserType) => void;
42
- }
43
-
44
- type UserType = {
45
- name: string;
46
- email: string;
47
- };
48
 
49
- type Language = 'auto' | 'en' | 'zh';
 
 
 
 
50
 
51
  export function Header({
52
  user,
@@ -63,125 +69,96 @@ export function Header({
63
  onCreateWorkspace,
64
  availableCourses = [],
65
  onUserUpdate,
 
 
 
66
  }: HeaderProps) {
67
  const [showProfileEditor, setShowProfileEditor] = useState(false);
68
- // Star brightness levels: 0-10 (11 levels), 0 = 100%, 10 = 0%
69
- const [starBrightness, setStarBrightness] = useState(0);
70
- const languageLabels = {
71
- auto: 'Auto',
72
- en: 'English',
73
- zh: '简体中文',
74
- };
75
 
76
  const [createOpen, setCreateOpen] = useState(false);
77
- const [workspaceName, setWorkspaceName] = useState('');
78
- const [category, setCategory] = useState<'course' | 'personal'>('course');
79
- const [courseId, setCourseId] = useState('');
80
- const [inviteEmail, setInviteEmail] = useState('');
81
  const [invites, setInvites] = useState<string[]>([]);
82
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  const addInvite = () => {
84
  const email = inviteEmail.trim();
85
  if (!email) return;
86
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
87
- toast.error('Please enter a valid email');
88
  return;
89
  }
90
  if (invites.includes(email)) return;
91
- setInvites(prev => [...prev, email]);
92
- setInviteEmail('');
93
  };
94
 
95
  const removeInvite = (email: string) => {
96
- setInvites(prev => prev.filter(e => e !== email));
97
- };
98
-
99
- const handleStarClick = (e: React.MouseEvent) => {
100
- e.preventDefault();
101
- e.stopPropagation();
102
- // Cycle through brightness levels: 0 -> 1 -> ... -> 10 -> 0
103
- setStarBrightness((prev) => (prev + 1) % 11);
104
- };
105
-
106
- const getStarOpacity = () => {
107
- // 11 levels: 0 = 100% (1.0), 10 = 0% (0.0)
108
- return 1 - (starBrightness / 10);
109
- };
110
-
111
- const getEnergyValue = () => {
112
- // Energy value: 0 = 100%, 1 = 90%, ..., 10 = 0%
113
- return 100 - (starBrightness * 10);
114
- };
115
-
116
- const getStarColor = () => {
117
- // Color transitions from gold (100%) to gray/white (0%)
118
- if (starBrightness === 10) {
119
- return { fill: 'transparent', stroke: 'white', strokeWidth: 1.5, strokeDasharray: '2 2' };
120
- }
121
- // Interpolate from gold (#fbbf24) to lighter gold/gray
122
- const energy = getEnergyValue();
123
- if (energy >= 50) {
124
- // High energy: bright gold
125
- return { fill: '#fbbf24', stroke: '#fbbf24', strokeWidth: 0 };
126
- } else if (energy >= 20) {
127
- // Medium energy: lighter gold
128
- return { fill: '#fcd34d', stroke: '#fcd34d', strokeWidth: 0 };
129
- } else {
130
- // Low energy: very light gold/gray
131
- return { fill: '#fde68a', stroke: '#fde68a', strokeWidth: 0 };
132
- }
133
  };
134
 
135
  const handleCreate = () => {
136
  if (!workspaceName.trim()) {
137
- toast.error('Please enter a workspace name');
138
  return;
139
  }
140
- if (category === 'course' && !courseId) {
141
- toast.error('Please select a course');
142
  return;
143
  }
144
  if (invites.length === 0) {
145
- toast.error('Please add at least one member');
146
  return;
147
  }
148
-
149
- // Call the create function
150
  onCreateWorkspace?.({
151
  name: workspaceName.trim(),
152
  category,
153
  courseId: courseId || undefined,
154
  invites,
155
  });
156
-
157
- // Reset form and close dialog
158
- setWorkspaceName('');
159
- setCourseId('');
160
- setCategory('course');
161
  setInvites([]);
162
- setInviteEmail('');
163
  setCreateOpen(false);
164
  };
165
 
166
  return (
167
  <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]">
168
  <div className="flex items-center gap-4">
169
- <Button
170
- variant="ghost"
171
- size="icon"
172
- className="lg:hidden"
173
- onClick={onMenuClick}
174
- >
175
  <Menu className="h-5 w-5" />
176
  </Button>
177
-
178
  <div className="flex items-center gap-3">
179
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center">
180
  <img src={clareAvatar} alt="Clare AI" className="w-full h-full object-cover" />
181
  </div>
182
  <div>
183
- <h1 className="text-lg sm:text-xl tracking-tight" style={{ fontFamily: 'Inter, sans-serif', fontWeight: 600, letterSpacing: '-0.02em' }}>
184
- Clare <span className="text-sm font-bold text-muted-foreground hidden sm:inline ml-2">Your Personalized AI Tutor</span>
 
 
 
 
 
 
185
  </h1>
186
  <p className="text-xs text-muted-foreground hidden sm:block">
187
  Personalized guidance, review, and intelligent reinforcement
@@ -193,226 +170,202 @@ export function Header({
193
  <div className="flex items-center gap-2">
194
  <DropdownMenu>
195
  <DropdownMenuTrigger asChild>
196
- <Button
197
- variant="ghost"
198
- size="icon"
199
- aria-label="Change language"
200
- >
201
  <Languages className="h-5 w-5" />
202
  </Button>
203
  </DropdownMenuTrigger>
204
  <DropdownMenuContent align="end">
205
- <DropdownMenuItem onClick={() => onLanguageChange('auto')}>
206
- {language === 'auto' && ''}Auto
207
  </DropdownMenuItem>
208
- <DropdownMenuItem onClick={() => onLanguageChange('en')}>
209
- {language === 'en' && ''}English
210
  </DropdownMenuItem>
211
- <DropdownMenuItem onClick={() => onLanguageChange('zh')}>
212
- {language === 'zh' && ''}简体中文
213
  </DropdownMenuItem>
214
  </DropdownMenuContent>
215
  </DropdownMenu>
216
-
217
- <Button
218
- variant="ghost"
219
- size="icon"
220
- onClick={onToggleDarkMode}
221
- aria-label="Toggle dark mode"
222
- >
223
  {isDarkMode ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
224
  </Button>
225
-
226
  {user && currentWorkspace ? (
227
  <>
228
  <DropdownMenu>
229
  <DropdownMenuTrigger asChild>
230
- <Button
231
- variant="outline"
232
- className="gap-2 pl-2 pr-3"
233
- aria-label="Switch workspace"
234
- >
235
- <img
236
- src={currentWorkspace.avatar}
237
- alt={currentWorkspace.name}
238
- className="w-6 h-6 rounded-full object-cover"
239
- />
240
- <span className="hidden sm:inline max-w-[120px] truncate">{currentWorkspace.name}</span>
241
- <ChevronDown className="h-4 w-4 opacity-50" />
242
- </Button>
243
- </DropdownMenuTrigger>
244
- <DropdownMenuContent align="end" className="min-w-[14rem]">
245
- {workspaces.map((workspace) => (
246
- <DropdownMenuItem
247
- key={workspace.id}
248
- onClick={() => onWorkspaceChange(workspace.id)}
249
- className={`gap-3 ${currentWorkspace.id === workspace.id ? 'bg-accent' : ''}`}
250
- >
251
- <img
252
- src={workspace.avatar}
253
- alt={workspace.name}
254
- className="w-6 h-6 rounded-full object-cover flex-shrink-0"
255
- />
256
- <span className="truncate">{workspace.name}</span>
257
- {currentWorkspace.id === workspace.id && (
258
- <span className="ml-auto text-primary">✓</span>
259
- )}
260
- </DropdownMenuItem>
261
- ))}
262
- <DropdownMenuSeparator />
263
- <DropdownMenuItem className="gap-2" onClick={() => setCreateOpen(true)}>
264
- <Plus className="h-4 w-4" />
265
- <span>New Group Workspace</span>
266
- </DropdownMenuItem>
267
- </DropdownMenuContent>
268
- </DropdownMenu>
269
-
270
- {/* Profile Avatar Button */}
271
- <div className="relative inline-block">
272
- <DropdownMenu>
273
- <DropdownMenuTrigger asChild>
274
- <Button
275
- variant="ghost"
276
- size="icon"
277
- className="rounded-full"
278
- aria-label="User profile"
279
- >
280
- <img
281
- src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`}
282
- alt={user.name}
283
- className="w-8 h-8 rounded-full object-cover"
284
  />
 
 
285
  </Button>
286
  </DropdownMenuTrigger>
287
- <DropdownMenuContent align="end" className="w-56">
288
- <div className="px-2 py-1.5">
289
- <div className="flex items-center justify-between gap-2">
290
- <div className="flex-1 min-w-0">
291
- <p className="text-sm font-medium truncate">{user.name}</p>
292
- <p className="text-xs text-muted-foreground truncate">ID: {user.email.split('@')[0] || user.email}</p>
293
- </div>
294
- <div className="flex items-center gap-2 flex-shrink-0">
295
- {starBrightness === 10 ? (
296
- <Star
297
- className="w-4 h-4"
298
- style={{
299
- fill: 'transparent',
300
- stroke: 'white',
301
- strokeWidth: 1.5,
302
- strokeDasharray: '2 2',
303
- opacity: 0.6
304
- }}
305
  />
306
- ) : (
307
- <Star
308
- className="w-4 h-4"
309
- style={{
310
- ...getStarColor(),
311
- opacity: getStarOpacity(),
312
- filter: starBrightness === 0 ? 'drop-shadow(0 0 2px rgba(251, 191, 36, 0.8))' : 'none'
313
- }}
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  />
315
- )}
316
- <div className="flex flex-col items-end">
317
- <span className="text-xs font-medium">{getEnergyValue()}%</span>
318
- <div className="w-12 h-1.5 bg-muted rounded-full overflow-hidden">
319
- <div
320
- className="h-full bg-gradient-to-r from-amber-400 to-yellow-500 transition-all duration-300"
321
- style={{ width: `${getEnergyValue()}%` }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  />
 
 
 
 
 
 
 
 
 
323
  </div>
324
  </div>
325
  </div>
326
- </div>
327
- </div>
328
- <DropdownMenuSeparator />
329
- <DropdownMenuItem onClick={() => setShowProfileEditor(true)}>
330
- <Edit className="h-4 w-4 mr-2" />
331
- Edit Profile
332
- </DropdownMenuItem>
333
- <DropdownMenuSeparator />
334
- <DropdownMenuItem onClick={onLogout} className="text-destructive focus:text-destructive">
335
- <LogOut className="h-4 w-4 mr-2" />
336
- Log out
337
- </DropdownMenuItem>
338
- </DropdownMenuContent>
339
- </DropdownMenu>
340
- {/* Star badge in top-right corner of avatar - positioned outside, not overlapping */}
341
- <TooltipProvider>
342
- <Tooltip>
343
- <TooltipTrigger asChild>
344
- <button
345
- type="button"
346
- className="absolute cursor-pointer z-20 pointer-events-auto bg-transparent border-0 p-0"
347
- style={{
348
- top: '-8px',
349
- right: '-16px',
350
- opacity: starBrightness === 10 ? 0.6 : 1,
351
- transition: 'opacity 0.3s ease-in-out',
352
- filter: starBrightness === 0 ? 'drop-shadow(0 0 4px rgba(251, 191, 36, 0.8)) drop-shadow(0 0 8px rgba(251, 191, 36, 0.4))' : 'none'
353
- }}
354
- onClick={handleStarClick}
355
- >
356
- {starBrightness === 10 ? (
357
- <Star
358
- className="w-5 h-5"
359
  style={{
360
- fill: 'transparent',
361
- stroke: 'white',
362
- strokeWidth: 1.5,
363
- strokeDasharray: '2 2'
 
 
 
364
  }}
365
- />
366
- ) : (
367
- <Star
368
- className="w-5 h-5"
369
- style={{
370
- ...getStarColor(),
371
- opacity: getStarOpacity(),
372
- filter: starBrightness === 0 ? 'drop-shadow(0 0 2px rgba(251, 191, 36, 1))' : 'none'
373
  }}
374
- />
375
- )}
376
- </button>
377
- </TooltipTrigger>
378
- <TooltipContent
379
- className="z-[200] border border-amber-300/30 shadow-md"
380
- style={{
381
- zIndex: 200,
382
- backgroundColor: 'rgba(251, 191, 36, 0.95)',
383
- color: 'rgb(28, 25, 23)'
384
- }}
385
- sideOffset={5}
386
- >
387
- <div className="space-y-1">
388
- <p className="text-sm font-medium">Energy: {getEnergyValue()}%</p>
389
- <div className="w-32 h-2 bg-muted rounded-full overflow-hidden">
390
- <div
391
- className="h-full bg-gradient-to-r from-amber-400 to-yellow-500 transition-all duration-300"
392
- style={{ width: `${getEnergyValue()}%` }}
393
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  </div>
395
- </div>
396
- </TooltipContent>
397
- </Tooltip>
398
- </TooltipProvider>
399
- </div>
400
  </>
401
  ) : null}
402
  </div>
 
403
  {/* Create Group Workspace Dialog */}
404
  <Dialog open={createOpen} onOpenChange={setCreateOpen}>
405
- <DialogContent
406
- className="w-[600px] max-w-[600px] sm:max-w-[600px] z-[1001] pointer-events-auto"
407
- style={{ width: 600, maxWidth: 600, zIndex: 1001 }}
408
  overlayClassName="!z-[99]"
409
- overlayStyle={{
410
- top: '64px',
411
- left: 0,
412
- right: 0,
413
- bottom: 0,
414
  zIndex: 99,
415
- position: 'fixed'
416
  }}
417
  onPointerDownOutside={(e) => e.preventDefault()}
418
  onInteractOutside={(e) => e.preventDefault()}
@@ -420,14 +373,25 @@ export function Header({
420
  <DialogHeader>
421
  <DialogTitle>Create Group Workspace</DialogTitle>
422
  </DialogHeader>
 
423
  <div className="space-y-6">
424
  <div className="space-y-2">
425
  <Label htmlFor="ws-name">Workspace Name</Label>
426
- <Input id="ws-name" value={workspaceName} onChange={e => setWorkspaceName(e.target.value)} placeholder="e.g., CS 101 Study Group" />
 
 
 
 
 
427
  </div>
 
428
  <div className="space-y-2">
429
  <Label>Category</Label>
430
- <RadioGroup value={category} onValueChange={(val) => setCategory(val as 'course' | 'personal')} className="flex gap-4">
 
 
 
 
431
  <div className="flex items-center space-x-2">
432
  <RadioGroupItem id="cat-course" value="course" />
433
  <Label htmlFor="cat-course">Course</Label>
@@ -438,7 +402,8 @@ export function Header({
438
  </div>
439
  </RadioGroup>
440
  </div>
441
- {category === 'course' && (
 
442
  <div className="space-y-2">
443
  <Label htmlFor="course-select">Course Name</Label>
444
  <Select value={courseId} onValueChange={setCourseId}>
@@ -455,18 +420,38 @@ export function Header({
455
  </Select>
456
  </div>
457
  )}
 
458
  <div className="space-y-2">
459
  <Label>Invite Members (emails)</Label>
460
  <div className="flex gap-2">
461
- <Input value={inviteEmail} onChange={e => setInviteEmail(e.target.value)} placeholder="Enter email and click Add" onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addInvite(); } }} />
462
- <Button variant="secondary" onClick={addInvite}>Add</Button>
 
 
 
 
 
 
 
 
 
 
 
 
463
  </div>
 
464
  {invites.length > 0 && (
465
  <div className="flex flex-wrap gap-2">
466
- {invites.map(email => (
467
  <span key={email} className="inline-flex items-center px-2 py-1 rounded-full bg-muted text-sm">
468
  {email}
469
- <Button variant="ghost" size="icon" className="h-4 w-4 ml-1" onClick={() => removeInvite(email)} aria-label={`Remove ${email}`}>
 
 
 
 
 
 
470
  <X className="h-3 w-3" />
471
  </Button>
472
  </span>
@@ -475,8 +460,11 @@ export function Header({
475
  )}
476
  </div>
477
  </div>
 
478
  <DialogFooter>
479
- <Button variant="outline" onClick={() => setCreateOpen(false)}>Cancel</Button>
 
 
480
  <Button onClick={handleCreate}>Create</Button>
481
  </DialogFooter>
482
  </DialogContent>
@@ -487,9 +475,7 @@ export function Header({
487
  <ProfileEditor
488
  user={user}
489
  onSave={(updatedUser) => {
490
- if (onUserUpdate) {
491
- onUserUpdate(updatedUser);
492
- }
493
  setShowProfileEditor(false);
494
  }}
495
  onClose={() => setShowProfileEditor(false)}
@@ -497,4 +483,4 @@ export function Header({
497
  )}
498
  </header>
499
  );
500
- }
 
1
+ // web/src/components/Header.tsx
2
+ import React, { useState } from "react";
3
+ import { Button } from "./ui/button";
4
+ import { Menu, Sun, Moon, Languages, ChevronDown, LogOut, Plus, X, Edit, Star } from "lucide-react";
5
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
6
  import {
7
  DropdownMenu,
8
  DropdownMenuContent,
9
  DropdownMenuItem,
10
  DropdownMenuTrigger,
11
  DropdownMenuSeparator,
12
+ } from "./ui/dropdown-menu";
13
+ import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
14
+ import type { Workspace, CourseInfo } from "../App";
15
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "./ui/dialog";
16
+ import { Input } from "./ui/input";
17
+ import { Label } from "./ui/label";
18
+ import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
19
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
20
+ import { toast } from "sonner";
21
+ import { ProfileEditor } from "./ProfileEditor";
22
+
23
+ type UserType = {
24
+ name: string;
25
+ email: string;
26
+ };
27
+
28
+ type Language = "auto" | "en" | "zh";
29
 
30
  interface HeaderProps {
31
  user: UserType | null;
 
40
  onWorkspaceChange: (workspaceId: string) => void;
41
  onCreateWorkspace?: (payload: {
42
  name: string;
43
+ category: "course" | "personal";
44
  courseId?: string;
45
  invites: string[];
46
  }) => void;
47
  onLogout: () => void;
48
  availableCourses?: CourseInfo[];
49
  onUserUpdate?: (user: UserType) => void;
 
 
 
 
 
 
50
 
51
+ // NEW: controlled review-star display + click behavior
52
+ reviewStarOpacity?: number; // 0..1
53
+ reviewEnergyPct?: number; // 0..100
54
+ onStarClick?: () => void; // recommended: switch to Review
55
+ }
56
 
57
  export function Header({
58
  user,
 
69
  onCreateWorkspace,
70
  availableCourses = [],
71
  onUserUpdate,
72
+ reviewStarOpacity,
73
+ reviewEnergyPct,
74
+ onStarClick,
75
  }: HeaderProps) {
76
  const [showProfileEditor, setShowProfileEditor] = useState(false);
 
 
 
 
 
 
 
77
 
78
  const [createOpen, setCreateOpen] = useState(false);
79
+ const [workspaceName, setWorkspaceName] = useState("");
80
+ const [category, setCategory] = useState<"course" | "personal">("course");
81
+ const [courseId, setCourseId] = useState("");
82
+ const [inviteEmail, setInviteEmail] = useState("");
83
  const [invites, setInvites] = useState<string[]>([]);
84
 
85
+ const opacity = typeof reviewStarOpacity === "number" ? reviewStarOpacity : 0.15;
86
+ const energy = typeof reviewEnergyPct === "number" ? reviewEnergyPct : Math.round(opacity * 100);
87
+
88
+ const getStarStyle = () => {
89
+ if (energy <= 0) {
90
+ return { fill: "transparent", stroke: "white", strokeWidth: 1.5, strokeDasharray: "2 2" };
91
+ }
92
+ if (energy >= 60) return { fill: "#fbbf24", stroke: "#fbbf24", strokeWidth: 0 };
93
+ if (energy >= 25) return { fill: "#fcd34d", stroke: "#fcd34d", strokeWidth: 0 };
94
+ return { fill: "#fde68a", stroke: "#fde68a", strokeWidth: 0 };
95
+ };
96
+
97
  const addInvite = () => {
98
  const email = inviteEmail.trim();
99
  if (!email) return;
100
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
101
+ toast.error("Please enter a valid email");
102
  return;
103
  }
104
  if (invites.includes(email)) return;
105
+ setInvites((prev) => [...prev, email]);
106
+ setInviteEmail("");
107
  };
108
 
109
  const removeInvite = (email: string) => {
110
+ setInvites((prev) => prev.filter((e) => e !== email));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  };
112
 
113
  const handleCreate = () => {
114
  if (!workspaceName.trim()) {
115
+ toast.error("Please enter a workspace name");
116
  return;
117
  }
118
+ if (category === "course" && !courseId) {
119
+ toast.error("Please select a course");
120
  return;
121
  }
122
  if (invites.length === 0) {
123
+ toast.error("Please add at least one member");
124
  return;
125
  }
126
+
 
127
  onCreateWorkspace?.({
128
  name: workspaceName.trim(),
129
  category,
130
  courseId: courseId || undefined,
131
  invites,
132
  });
133
+
134
+ setWorkspaceName("");
135
+ setCourseId("");
136
+ setCategory("course");
 
137
  setInvites([]);
138
+ setInviteEmail("");
139
  setCreateOpen(false);
140
  };
141
 
142
  return (
143
  <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]">
144
  <div className="flex items-center gap-4">
145
+ <Button variant="ghost" size="icon" className="lg:hidden" onClick={onMenuClick}>
 
 
 
 
 
146
  <Menu className="h-5 w-5" />
147
  </Button>
148
+
149
  <div className="flex items-center gap-3">
150
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center">
151
  <img src={clareAvatar} alt="Clare AI" className="w-full h-full object-cover" />
152
  </div>
153
  <div>
154
+ <h1
155
+ className="text-lg sm:text-xl tracking-tight"
156
+ style={{ fontFamily: "Inter, sans-serif", fontWeight: 600, letterSpacing: "-0.02em" }}
157
+ >
158
+ Clare{" "}
159
+ <span className="text-sm font-bold text-muted-foreground hidden sm:inline ml-2">
160
+ Your Personalized AI Tutor
161
+ </span>
162
  </h1>
163
  <p className="text-xs text-muted-foreground hidden sm:block">
164
  Personalized guidance, review, and intelligent reinforcement
 
170
  <div className="flex items-center gap-2">
171
  <DropdownMenu>
172
  <DropdownMenuTrigger asChild>
173
+ <Button variant="ghost" size="icon" aria-label="Change language">
 
 
 
 
174
  <Languages className="h-5 w-5" />
175
  </Button>
176
  </DropdownMenuTrigger>
177
  <DropdownMenuContent align="end">
178
+ <DropdownMenuItem onClick={() => onLanguageChange("auto")}>
179
+ {language === "auto" && ""}Auto
180
  </DropdownMenuItem>
181
+ <DropdownMenuItem onClick={() => onLanguageChange("en")}>
182
+ {language === "en" && ""}English
183
  </DropdownMenuItem>
184
+ <DropdownMenuItem onClick={() => onLanguageChange("zh")}>
185
+ {language === "zh" && ""}简体中文
186
  </DropdownMenuItem>
187
  </DropdownMenuContent>
188
  </DropdownMenu>
189
+
190
+ <Button variant="ghost" size="icon" onClick={onToggleDarkMode} aria-label="Toggle dark mode">
 
 
 
 
 
191
  {isDarkMode ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
192
  </Button>
193
+
194
  {user && currentWorkspace ? (
195
  <>
196
  <DropdownMenu>
197
  <DropdownMenuTrigger asChild>
198
+ <Button variant="outline" className="gap-2 pl-2 pr-3" aria-label="Switch workspace">
199
+ <img
200
+ src={currentWorkspace.avatar}
201
+ alt={currentWorkspace.name}
202
+ className="w-6 h-6 rounded-full object-cover"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  />
204
+ <span className="hidden sm:inline max-w-[120px] truncate">{currentWorkspace.name}</span>
205
+ <ChevronDown className="h-4 w-4 opacity-50" />
206
  </Button>
207
  </DropdownMenuTrigger>
208
+
209
+ <DropdownMenuContent align="end" className="min-w-[14rem]">
210
+ {workspaces.map((workspace) => (
211
+ <DropdownMenuItem
212
+ key={workspace.id}
213
+ onClick={() => onWorkspaceChange(workspace.id)}
214
+ className={`gap-3 ${currentWorkspace.id === workspace.id ? "bg-accent" : ""}`}
215
+ >
216
+ <img
217
+ src={workspace.avatar}
218
+ alt={workspace.name}
219
+ className="w-6 h-6 rounded-full object-cover flex-shrink-0"
 
 
 
 
 
 
220
  />
221
+ <span className="truncate">{workspace.name}</span>
222
+ {currentWorkspace.id === workspace.id && <span className="ml-auto text-primary">✓</span>}
223
+ </DropdownMenuItem>
224
+ ))}
225
+ <DropdownMenuSeparator />
226
+ <DropdownMenuItem className="gap-2" onClick={() => setCreateOpen(true)}>
227
+ <Plus className="h-4 w-4" />
228
+ <span>New Group Workspace</span>
229
+ </DropdownMenuItem>
230
+ </DropdownMenuContent>
231
+ </DropdownMenu>
232
+
233
+ {/* Profile Avatar Button */}
234
+ <div className="relative inline-block">
235
+ <DropdownMenu>
236
+ <DropdownMenuTrigger asChild>
237
+ <Button variant="ghost" size="icon" className="rounded-full" aria-label="User profile">
238
+ <img
239
+ src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`}
240
+ alt={user.name}
241
+ className="w-8 h-8 rounded-full object-cover"
242
  />
243
+ </Button>
244
+ </DropdownMenuTrigger>
245
+
246
+ <DropdownMenuContent align="end" className="w-56">
247
+ <div className="px-2 py-1.5">
248
+ <div className="flex items-center justify-between gap-2">
249
+ <div className="flex-1 min-w-0">
250
+ <p className="text-sm font-medium truncate">{user.name}</p>
251
+ <p className="text-xs text-muted-foreground truncate">
252
+ ID: {user.email.split("@")[0] || user.email}
253
+ </p>
254
+ </div>
255
+
256
+ <div className="flex items-center gap-2 flex-shrink-0">
257
+ <Star
258
+ className="w-4 h-4"
259
+ style={{
260
+ ...getStarStyle(),
261
+ opacity,
262
+ filter: energy >= 85 ? "drop-shadow(0 0 2px rgba(251, 191, 36, 0.8))" : "none",
263
+ }}
264
  />
265
+ <div className="flex flex-col items-end">
266
+ <span className="text-xs font-medium">{energy}%</span>
267
+ <div className="w-12 h-1.5 bg-muted rounded-full overflow-hidden">
268
+ <div
269
+ className="h-full bg-gradient-to-r from-amber-400 to-yellow-500 transition-all duration-300"
270
+ style={{ width: `${energy}%` }}
271
+ />
272
+ </div>
273
+ </div>
274
  </div>
275
  </div>
276
  </div>
277
+
278
+ <DropdownMenuSeparator />
279
+
280
+ <DropdownMenuItem onClick={() => setShowProfileEditor(true)}>
281
+ <Edit className="h-4 w-4 mr-2" />
282
+ Edit Profile
283
+ </DropdownMenuItem>
284
+
285
+ <DropdownMenuSeparator />
286
+
287
+ <DropdownMenuItem onClick={onLogout} className="text-destructive focus:text-destructive">
288
+ <LogOut className="h-4 w-4 mr-2" />
289
+ Log out
290
+ </DropdownMenuItem>
291
+ </DropdownMenuContent>
292
+ </DropdownMenu>
293
+
294
+ {/* Star badge in top-right corner of avatar */}
295
+ <TooltipProvider>
296
+ <Tooltip>
297
+ <TooltipTrigger asChild>
298
+ <button
299
+ type="button"
300
+ className="absolute cursor-pointer z-20 pointer-events-auto bg-transparent border-0 p-0"
 
 
 
 
 
 
 
 
 
301
  style={{
302
+ top: "-8px",
303
+ right: "-16px",
304
+ opacity,
305
+ transition: "opacity 0.3s ease-in-out",
306
+ filter: energy >= 85
307
+ ? "drop-shadow(0 0 4px rgba(251, 191, 36, 0.8)) drop-shadow(0 0 8px rgba(251, 191, 36, 0.4))"
308
+ : "none",
309
  }}
310
+ onClick={(e) => {
311
+ e.preventDefault();
312
+ e.stopPropagation();
313
+ onStarClick?.();
 
 
 
 
314
  }}
315
+ aria-label="Review energy"
316
+ title="Review energy"
317
+ >
318
+ <Star
319
+ className="w-5 h-5"
320
+ style={{
321
+ ...getStarStyle(),
322
+ opacity,
323
+ filter: energy >= 85 ? "drop-shadow(0 0 2px rgba(251, 191, 36, 1))" : "none",
324
+ }}
 
 
 
 
 
 
 
 
 
325
  />
326
+ </button>
327
+ </TooltipTrigger>
328
+
329
+ <TooltipContent
330
+ className="z-[200] border border-amber-300/30 shadow-md"
331
+ style={{
332
+ zIndex: 200,
333
+ backgroundColor: "rgba(251, 191, 36, 0.95)",
334
+ color: "rgb(28, 25, 23)",
335
+ }}
336
+ sideOffset={5}
337
+ >
338
+ <div className="space-y-1">
339
+ <p className="text-sm font-medium">Energy: {energy}%</p>
340
+ <div className="w-32 h-2 bg-muted rounded-full overflow-hidden">
341
+ <div
342
+ className="h-full bg-gradient-to-r from-amber-400 to-yellow-500 transition-all duration-300"
343
+ style={{ width: `${energy}%` }}
344
+ />
345
+ </div>
346
+ <p className="text-xs opacity-80">Enter Review and complete at least 1 action today to recharge.</p>
347
  </div>
348
+ </TooltipContent>
349
+ </Tooltip>
350
+ </TooltipProvider>
351
+ </div>
 
352
  </>
353
  ) : null}
354
  </div>
355
+
356
  {/* Create Group Workspace Dialog */}
357
  <Dialog open={createOpen} onOpenChange={setCreateOpen}>
358
+ <DialogContent
359
+ className="w-[600px] max-w-[600px] sm:max-w-[600px] z-[1001] pointer-events-auto"
360
+ style={{ width: 600, maxWidth: 600, zIndex: 1001 }}
361
  overlayClassName="!z-[99]"
362
+ overlayStyle={{
363
+ top: "64px",
364
+ left: 0,
365
+ right: 0,
366
+ bottom: 0,
367
  zIndex: 99,
368
+ position: "fixed",
369
  }}
370
  onPointerDownOutside={(e) => e.preventDefault()}
371
  onInteractOutside={(e) => e.preventDefault()}
 
373
  <DialogHeader>
374
  <DialogTitle>Create Group Workspace</DialogTitle>
375
  </DialogHeader>
376
+
377
  <div className="space-y-6">
378
  <div className="space-y-2">
379
  <Label htmlFor="ws-name">Workspace Name</Label>
380
+ <Input
381
+ id="ws-name"
382
+ value={workspaceName}
383
+ onChange={(e) => setWorkspaceName(e.target.value)}
384
+ placeholder="e.g., CS 101 Study Group"
385
+ />
386
  </div>
387
+
388
  <div className="space-y-2">
389
  <Label>Category</Label>
390
+ <RadioGroup
391
+ value={category}
392
+ onValueChange={(val) => setCategory(val as "course" | "personal")}
393
+ className="flex gap-4"
394
+ >
395
  <div className="flex items-center space-x-2">
396
  <RadioGroupItem id="cat-course" value="course" />
397
  <Label htmlFor="cat-course">Course</Label>
 
402
  </div>
403
  </RadioGroup>
404
  </div>
405
+
406
+ {category === "course" && (
407
  <div className="space-y-2">
408
  <Label htmlFor="course-select">Course Name</Label>
409
  <Select value={courseId} onValueChange={setCourseId}>
 
420
  </Select>
421
  </div>
422
  )}
423
+
424
  <div className="space-y-2">
425
  <Label>Invite Members (emails)</Label>
426
  <div className="flex gap-2">
427
+ <Input
428
+ value={inviteEmail}
429
+ onChange={(e) => setInviteEmail(e.target.value)}
430
+ placeholder="Enter email and click Add"
431
+ onKeyDown={(e) => {
432
+ if (e.key === "Enter") {
433
+ e.preventDefault();
434
+ addInvite();
435
+ }
436
+ }}
437
+ />
438
+ <Button variant="secondary" onClick={addInvite}>
439
+ Add
440
+ </Button>
441
  </div>
442
+
443
  {invites.length > 0 && (
444
  <div className="flex flex-wrap gap-2">
445
+ {invites.map((email) => (
446
  <span key={email} className="inline-flex items-center px-2 py-1 rounded-full bg-muted text-sm">
447
  {email}
448
+ <Button
449
+ variant="ghost"
450
+ size="icon"
451
+ className="h-4 w-4 ml-1"
452
+ onClick={() => removeInvite(email)}
453
+ aria-label={`Remove ${email}`}
454
+ >
455
  <X className="h-3 w-3" />
456
  </Button>
457
  </span>
 
460
  )}
461
  </div>
462
  </div>
463
+
464
  <DialogFooter>
465
+ <Button variant="outline" onClick={() => setCreateOpen(false)}>
466
+ Cancel
467
+ </Button>
468
  <Button onClick={handleCreate}>Create</Button>
469
  </DialogFooter>
470
  </DialogContent>
 
475
  <ProfileEditor
476
  user={user}
477
  onSave={(updatedUser) => {
478
+ if (onUserUpdate) onUserUpdate(updatedUser);
 
 
479
  setShowProfileEditor(false);
480
  }}
481
  onClose={() => setShowProfileEditor(false)}
 
483
  )}
484
  </header>
485
  );
486
+ }