AI_Agent_V4 / web /src /components /LeftSidebar.tsx
SarahXia0405's picture
Update web/src/components/LeftSidebar.tsx
366b739 verified
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>
);
}