Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { Brain, AlertTriangle, AlertCircle, CheckCircle, ChevronDown, ChevronRight, Info } from 'lucide-react'; | |
| import { Badge } from './ui/badge'; | |
| import { Button } from './ui/button'; | |
| import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible'; | |
| import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; | |
| interface ReviewItem { | |
| id: string; | |
| title: string; | |
| schedule: string; | |
| status: 'urgent' | 'review' | 'stable'; | |
| weight: number; | |
| lastReviewed: string; | |
| memoryRetention: number; | |
| previousQuestion: string; | |
| } | |
| interface SmartReviewProps { | |
| onReviewTopic?: (item: ReviewItem) => void; | |
| onReviewAll?: () => void; | |
| } | |
| export function SmartReview({ onReviewTopic, onReviewAll }: SmartReviewProps = {}) { | |
| // Initialize with the first item of W-4 (red zone) expanded by default | |
| const [expandedItems, setExpandedItems] = useState<string[]>(['w4-1']); | |
| const [selectedCategory, setSelectedCategory] = useState<string>('W-4'); // Default to red zone | |
| const reviewData = [ | |
| { | |
| label: 'W-4', | |
| percentage: 35, | |
| color: 'bg-red-500', | |
| textColor: 'text-red-500', | |
| icon: AlertTriangle, | |
| description: 'Higher forgetting risk', | |
| items: [ | |
| { | |
| id: 'w4-1', | |
| title: 'Main Concept of Lab 3', | |
| schedule: 'T+7', | |
| status: 'urgent' as const, | |
| weight: 35, | |
| lastReviewed: '7 days ago', | |
| memoryRetention: 25, | |
| previousQuestion: 'I\'ve read the instructions for Lab 3, but what is the main concept we\'re supposed to be learning here?' | |
| } | |
| ] | |
| }, | |
| { | |
| label: 'W-2', | |
| percentage: 20, | |
| color: 'bg-orange-500', | |
| textColor: 'text-orange-500', | |
| icon: AlertCircle, | |
| description: 'Medium forgetting risk', | |
| items: [ | |
| { | |
| id: 'w2-1', | |
| title: 'Effective Prompt Engineering', | |
| schedule: 'T+14', | |
| status: 'review' as const, | |
| weight: 20, | |
| lastReviewed: '3 days ago', | |
| memoryRetention: 60, | |
| previousQuestion: 'I understand what prompt engineering is, but what specifically makes a prompt effective versus ineffective?' | |
| } | |
| ] | |
| }, | |
| { | |
| label: 'W-1', | |
| percentage: 12, | |
| color: 'bg-green-500', | |
| textColor: 'text-green-500', | |
| icon: CheckCircle, | |
| description: 'Recently learned', | |
| items: [ | |
| { | |
| id: 'w1-1', | |
| title: 'Objective LLM Evaluation', | |
| schedule: 'T+7', | |
| status: 'stable' as const, | |
| weight: 12, | |
| lastReviewed: '1 day ago', | |
| memoryRetention: 90, | |
| previousQuestion: 'How can we objectively evaluate an LLM\'s performance when the output quality seems so subjective?' | |
| } | |
| ] | |
| }, | |
| ]; | |
| const totalPercentage = reviewData.reduce((sum, item) => sum + item.percentage, 0); | |
| const selectedData = reviewData.find(item => item.label === selectedCategory); | |
| const toggleItem = (itemId: string) => { | |
| setExpandedItems(prev => | |
| prev.includes(itemId) | |
| ? prev.filter(id => id !== itemId) | |
| : [...prev, itemId] | |
| ); | |
| }; | |
| // When category changes, automatically expand the first item of that category | |
| const handleCategoryChange = (categoryLabel: string) => { | |
| setSelectedCategory(categoryLabel); | |
| const categoryData = reviewData.find(item => item.label === categoryLabel); | |
| if (categoryData && categoryData.items.length > 0) { | |
| // Expand only the first item of the selected category | |
| setExpandedItems([categoryData.items[0].id]); | |
| } | |
| }; | |
| const getStatusBadge = (status: 'urgent' | 'review' | 'stable') => { | |
| const configs = { | |
| urgent: { label: 'URGENT', className: 'bg-red-500 text-white hover:bg-red-600' }, | |
| review: { label: 'DUE', className: 'bg-orange-500 text-white hover:bg-orange-600' }, | |
| stable: { label: 'STABLE', className: 'bg-green-500 text-white hover:bg-green-600' }, | |
| }; | |
| return configs[status]; | |
| }; | |
| const getButtonColorClass = (colorClass: string) => { | |
| // Extract color from bg-red-500, bg-orange-500, bg-green-500 | |
| if (colorClass.includes('red')) { | |
| return 'bg-red-500 hover:bg-red-600'; | |
| } else if (colorClass.includes('orange')) { | |
| return 'bg-orange-500 hover:bg-orange-600'; | |
| } else if (colorClass.includes('green')) { | |
| return 'bg-green-500 hover:bg-green-600'; | |
| } | |
| return 'bg-red-500 hover:bg-red-600'; // default | |
| }; | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="flex flex-col gap-1"> | |
| <div className="flex items-center gap-2"> | |
| <Brain className="h-5 w-5 text-red-500" /> | |
| <h3>Current Review Distribution</h3> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <button className="text-muted-foreground hover:text-foreground transition-colors"> | |
| <Info className="h-3 w-3" /> | |
| </button> | |
| </TooltipTrigger> | |
| <TooltipContent side="right" className="p-2" style={{ maxWidth: '170px', whiteSpace: 'normal', wordBreak: 'break-word' }}> | |
| <p className="text-[10px] leading-relaxed text-left" style={{ whiteSpace: 'normal' }}> | |
| 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. | |
| </p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </div> | |
| </div> | |
| {/* Combined Progress Bar - Clickable */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-3"> | |
| {reviewData.map((item) => ( | |
| <button | |
| key={item.label} | |
| 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 ${ | |
| selectedCategory === item.label ? 'bg-muted/80' : 'bg-transparent hover:bg-muted/40' | |
| }`} | |
| onClick={() => handleCategoryChange(item.label)} | |
| title={`Click to view ${item.label} items`} | |
| style={{ flex: item.percentage }} | |
| > | |
| <div className="flex items-center gap-1 justify-center whitespace-nowrap"> | |
| <span className="text-[10px]">{item.label}:</span> | |
| <span className={`text-[10px] font-medium ${item.textColor}`}>{item.percentage}%</span> | |
| </div> | |
| <div className={`h-2 ${item.color} rounded-full mb-2`} /> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Selected Category Expandable Items */} | |
| {selectedData && ( | |
| <div className="space-y-2"> | |
| {/* Expandable Review Items */} | |
| <div className="space-y-2"> | |
| {selectedData.items.map((reviewItem) => ( | |
| <Collapsible | |
| key={reviewItem.id} | |
| open={expandedItems.includes(reviewItem.id)} | |
| onOpenChange={() => toggleItem(reviewItem.id)} | |
| > | |
| <CollapsibleTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| className="w-full justify-between text-left h-auto p-2 hover:bg-muted/50" | |
| > | |
| <span className="text-sm">Review: {reviewItem.title}</span> | |
| {expandedItems.includes(reviewItem.id) ? ( | |
| <ChevronDown className="h-4 w-4 flex-shrink-0" /> | |
| ) : ( | |
| <ChevronRight className="h-4 w-4 flex-shrink-0" /> | |
| )} | |
| </Button> | |
| </CollapsibleTrigger> | |
| <CollapsibleContent> | |
| <div className="p-3 pl-4"> | |
| <div className="flex items-center gap-2 flex-wrap"> | |
| <Badge variant="outline" className="text-xs"> | |
| {reviewItem.schedule} | |
| </Badge> | |
| <Badge className={`text-xs ${getStatusBadge(reviewItem.status).className}`}> | |
| {getStatusBadge(reviewItem.status).label} | |
| </Badge> | |
| <span className="text-xs text-muted-foreground"> | |
| Weight: {reviewItem.weight}% | Last: {reviewItem.lastReviewed} | |
| </span> | |
| </div> | |
| {/* Empty line for spacing */} | |
| <div className="h-4"></div> | |
| <div className="space-y-2"> | |
| {/* Titles row - aligned horizontally */} | |
| <div className="flex gap-4 items-baseline"> | |
| <div className="flex-shrink-0"> | |
| <span className="text-xs text-muted-foreground leading-none">Memory Retention</span> | |
| </div> | |
| <div className="flex-1 min-w-0 pl-2"> | |
| <span className="text-xs text-muted-foreground leading-none">You previously asked:</span> | |
| </div> | |
| </div> | |
| {/* Content row */} | |
| <div className="flex gap-4 items-start"> | |
| {/* Left: Memory Retention Pie Chart */} | |
| <div className="flex-shrink-0 relative w-24 h-24"> | |
| <svg className="w-full h-full" viewBox="0 0 100 100" style={{ transform: 'rotate(-90deg)' }}> | |
| {/* Background circle */} | |
| <circle | |
| cx="50" | |
| cy="50" | |
| r="45" | |
| fill="none" | |
| stroke="currentColor" | |
| strokeWidth="8" | |
| className="text-muted" | |
| /> | |
| {/* Filled arc */} | |
| <circle | |
| cx="50" | |
| cy="50" | |
| r="45" | |
| fill="none" | |
| stroke="currentColor" | |
| strokeWidth="8" | |
| strokeDasharray={`${2 * Math.PI * 45}`} | |
| strokeDashoffset={`${2 * Math.PI * 45 * (1 - reviewItem.memoryRetention / 100)}`} | |
| strokeLinecap="round" | |
| className={selectedData.color.replace('bg-', 'text-')} | |
| /> | |
| </svg> | |
| <div className="absolute inset-0 flex items-center justify-center pointer-events-none"> | |
| <span className={`text-sm font-medium ${selectedData.textColor}`}> | |
| {reviewItem.memoryRetention}% | |
| </span> | |
| </div> | |
| </div> | |
| {/* Right: Previous Question and Review Button */} | |
| <div className="flex-1 h-24 flex flex-col justify-between min-w-0 pl-2"> | |
| <div className="flex-shrink-0"> | |
| <p className="text-xs bg-muted/50 p-2 rounded italic m-0"> | |
| "{reviewItem.previousQuestion}" | |
| </p> | |
| </div> | |
| <div className="flex-shrink-0 flex gap-2"> | |
| <Button | |
| className={`flex-1 ${getButtonColorClass(selectedData.color)} text-white`} | |
| size="sm" | |
| onClick={() => onReviewTopic?.(reviewItem)} | |
| > | |
| Review this topic | |
| </Button> | |
| <Button | |
| variant="outline" | |
| className="flex-1" | |
| size="sm" | |
| onClick={() => onReviewAll?.()} | |
| > | |
| Review All | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </CollapsibleContent> | |
| </Collapsible> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |