Spaces:
Sleeping
Sleeping
| import React, { useState, useRef, useEffect } from 'react'; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'; | |
| import { UserGuide } from './UserGuide'; | |
| import { SmartReview } from './SmartReview'; | |
| import { Label } from './ui/label'; | |
| import { Button } from './ui/button'; | |
| import { LogIn, Edit, BookOpen } from 'lucide-react'; | |
| import { GroupMembers } from './GroupMembers'; | |
| import { Card } from './ui/card'; | |
| import { Input } from './ui/input'; | |
| import type { LearningMode, Language, SpaceType, GroupMember, User as UserType } from '../App'; | |
| import { toast } from 'sonner'; | |
| interface LeftSidebarProps { | |
| learningMode: LearningMode; | |
| language: Language; | |
| onLearningModeChange: (mode: LearningMode) => void; | |
| onLanguageChange: (lang: Language) => void; | |
| spaceType: SpaceType; | |
| groupMembers: GroupMember[]; | |
| user: UserType | null; | |
| onLogin: (user: UserType) => void; | |
| onLogout: () => void; | |
| isLoggedIn: boolean; | |
| onEditProfile: () => void; | |
| } | |
| export function LeftSidebar({ | |
| learningMode, | |
| language, | |
| onLearningModeChange, | |
| onLanguageChange, | |
| spaceType, | |
| groupMembers, | |
| user, | |
| onLogin, | |
| onLogout, | |
| isLoggedIn, | |
| onEditProfile, | |
| }: LeftSidebarProps) { | |
| const [showLoginForm, setShowLoginForm] = useState(false); | |
| const [name, setName] = useState(''); | |
| const [email, setEmail] = useState(''); | |
| const handleLogin = () => { | |
| if (!name.trim() || !email.trim()) { | |
| toast.error('Please fill in all fields'); | |
| return; | |
| } | |
| onLogin({ name: name.trim(), email: email.trim() }); | |
| setShowLoginForm(false); | |
| setName(''); | |
| setEmail(''); | |
| toast.success(`Welcome, ${name}!`); | |
| }; | |
| const handleLogout = () => { | |
| onLogout(); | |
| setShowLoginForm(false); | |
| toast.success('Logged out successfully'); | |
| }; | |
| const scrollContainerRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| const container = scrollContainerRef.current; | |
| if (!container) return; | |
| const handleWheel = (e: WheelEvent) => { | |
| e.stopPropagation(); | |
| e.stopImmediatePropagation(); | |
| const { scrollTop, scrollHeight, clientHeight } = container; | |
| const isScrollable = scrollHeight > clientHeight; | |
| const isAtTop = scrollTop === 0; | |
| const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1; | |
| if (isScrollable && ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0))) { | |
| e.preventDefault(); | |
| } | |
| }; | |
| container.addEventListener('wheel', handleWheel, { passive: false, capture: true }); | |
| return () => { | |
| container.removeEventListener('wheel', handleWheel, { capture: true } as any); | |
| }; | |
| }, []); | |
| return ( | |
| <div | |
| ref={scrollContainerRef} | |
| className="flex-1 overflow-auto overscroll-contain flex flex-col" | |
| style={{ overscrollBehavior: 'contain' }} | |
| > | |
| {/* Profile/Login Section */} | |
| <div className="p-4 border-b border-border flex-shrink-0"> | |
| <h3 className="text-base font-medium mb-4">Profile</h3> | |
| <Card className="p-4"> | |
| {!isLoggedIn ? ( | |
| <div className="space-y-4"> | |
| <div className="flex flex-col items-center py-4"> | |
| <img | |
| 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" | |
| alt="Student studying" | |
| className="w-20 h-20 rounded-full object-cover mb-4" | |
| /> | |
| <h3 className="mb-2">Welcome to Clare!</h3> | |
| <p className="text-sm text-muted-foreground text-center mb-4"> | |
| Log in to start your personalized learning journey | |
| </p> | |
| </div> | |
| {!showLoginForm ? ( | |
| <Button onClick={() => setShowLoginForm(true)} className="w-full gap-2"> | |
| <LogIn className="h-4 w-4" /> | |
| Student Login | |
| </Button> | |
| ) : ( | |
| <div className="space-y-3"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="name">Name</Label> | |
| <Input | |
| id="name" | |
| value={name} | |
| onChange={(e) => setName(e.target.value)} | |
| placeholder="Enter your name" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="email">Email / Student ID</Label> | |
| <Input | |
| id="email" | |
| type="email" | |
| value={email} | |
| onChange={(e) => setEmail(e.target.value)} | |
| placeholder="Enter your email or ID" | |
| /> | |
| </div> | |
| <div className="flex gap-2"> | |
| <Button onClick={handleLogin} className="flex-1"> | |
| Enter | |
| </Button> | |
| <Button variant="outline" onClick={() => setShowLoginForm(false)}> | |
| Cancel | |
| </Button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="space-y-3"> | |
| <div className="flex items-start justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <img | |
| src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent( | |
| user?.email || '' | |
| )}`} | |
| alt={user?.name || 'User'} | |
| className="w-8 h-8 rounded-full object-cover flex-shrink-0 bg-muted" | |
| /> | |
| <div className="space-y-1"> | |
| <p className="text-sm text-muted-foreground">Hello,</p> | |
| <h4>{user?.name ?? ''}!</h4> | |
| </div> | |
| </div> | |
| <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onEditProfile}> | |
| <Edit className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| ID: {user?.email ? user.email.split('@')[0] : ''} | |
| </div> | |
| <Button variant="outline" className="w-full" onClick={handleLogout}> | |
| Log out | |
| </Button> | |
| </div> | |
| )} | |
| </Card> | |
| </div> | |
| {/* Group Members - Only show in group mode */} | |
| {spaceType === 'group' && ( | |
| <div className="p-4 border-b border-border flex-shrink-0"> | |
| <GroupMembers members={groupMembers} /> | |
| </div> | |
| )} | |
| {/* Tabs */} | |
| <Tabs defaultValue="review" className="flex flex-col flex-1 min-h-0 overflow-hidden"> | |
| <div className="px-4 pt-4"> | |
| {/* 关键:TabsList 已在 ui/tabs.tsx 改成 w-full flex,这里只需要三等分 */} | |
| <TabsList className="w-full"> | |
| <TabsTrigger value="review" className="flex-1 px-2 text-xs whitespace-nowrap"> | |
| Smart Review | |
| </TabsTrigger> | |
| <TabsTrigger value="quiz" className="flex-1 px-2 text-xs whitespace-nowrap"> | |
| Personal Quiz | |
| </TabsTrigger> | |
| <TabsTrigger value="guide" className="flex-1 px-2 text-xs whitespace-nowrap"> | |
| User Guide | |
| </TabsTrigger> | |
| </TabsList> | |
| </div> | |
| <TabsContent value="review" className="flex-1 mt-0 p-4 space-y-6 overflow-auto"> | |
| <SmartReview /> | |
| </TabsContent> | |
| <TabsContent value="quiz" className="flex-1 mt-0 p-4 overflow-auto"> | |
| <div className="space-y-4"> | |
| <div className="flex items-center gap-2"> | |
| <BookOpen className="h-5 w-5 text-red-500" /> | |
| <h3 className="text-base font-medium">Personal Quiz</h3> | |
| </div> | |
| <Card className="p-3 bg-muted/50 border-border"> | |
| <p className="text-xs text-muted-foreground leading-relaxed"> | |
| Clare analyzes your chat history and learning patterns to randomly select a personalized question that | |
| challenges your understanding of previously discussed topics. | |
| </p> | |
| </Card> | |
| <Button | |
| className="w-full bg-red-500 hover:bg-red-600 text-white" | |
| size="sm" | |
| onClick={() => { | |
| toast.success('Generating personalized quiz...'); | |
| }} | |
| > | |
| Test your memory | |
| </Button> | |
| </div> | |
| </TabsContent> | |
| <TabsContent value="guide" className="flex-1 mt-0 p-4 overflow-auto"> | |
| <UserGuide /> | |
| </TabsContent> | |
| </Tabs> | |
| </div> | |
| ); | |
| } | |