Spaces:
Sleeping
Sleeping
| import { useState, useRef, useLayoutEffect, memo } from 'react'; | |
| // Move constants outside component to avoid re-creation and make them accessible to memoized components | |
| const scopeOptions = [ | |
| { key: 'entire_paper', title: 'Entire Paper', desc: 'I want to understand the whole paper' }, | |
| { key: 'specific_section', title: 'Specific Section', desc: 'Focus on a particular section, chapter, equation, or formula' } | |
| ]; | |
| const depthOptions = [ | |
| { key: 'gist', title: 'Get the gist', desc: 'High-level understanding and main takeaways' }, | |
| { key: 'working_understanding', title: 'Working understanding', desc: 'Detailed comprehension I can discuss and apply' }, | |
| { key: 'reproduce', title: 'Reproduce results', desc: 'Deep enough to implement or reproduce the work' } | |
| ]; | |
| const styleOptions = [ | |
| { key: 'concepts', title: 'Concepts', desc: 'Focus on ideas, theories, and conceptual understanding' }, | |
| { key: 'mathematics', title: 'Mathematics', desc: 'Emphasize equations, proofs, and mathematical reasoning' }, | |
| { key: 'methods', title: 'Methods', desc: 'Concentrate on procedures, techniques, and implementation' }, | |
| { key: 'figures', title: 'Figures', desc: 'Focus on charts, diagrams, graphs, and visual elements' } | |
| ]; | |
| const chunkSizeOptions = [ | |
| { key: 'small', title: 'Small chunks', desc: 'Short, focused sections with frequent checkpoints' }, | |
| { key: 'medium', title: 'Medium chunks', desc: 'Balanced pace and depth' }, | |
| { key: 'large', title: 'Large chunks', desc: 'Longer sections, faster pace' } | |
| ]; | |
| const chunkingOptions = [ | |
| { key: 'guided', title: 'Generate learning chunks for me', badge: 'AI-Powered', desc: 'We\'ll automatically break down the paper into optimal learning sections' }, | |
| { key: 'manual', title: 'I\'ll highlight sections myself', desc: 'Use highlighting tools to select what you want to focus on' } | |
| ]; | |
| const familiarityLabels = ['New to it', 'Somewhat new', 'Comfortable', 'Very familiar', 'I\'ve taught this']; | |
| const OnboardingWizard = ({ fileName, academicBackground, onComplete }) => { | |
| const [currentStep, setCurrentStep] = useState('scope'); | |
| const [scope, setScope] = useState(null); | |
| const [scopeDetails, setScopeDetails] = useState(''); | |
| const [depth, setDepth] = useState(null); | |
| const [style, setStyle] = useState(null); | |
| const [familiarity, setFamiliarity] = useState(2); | |
| const [chunkSize, setChunkSize] = useState('medium'); | |
| const [useChunking, setUseChunking] = useState(null); | |
| const [familiarityText, setFamiliarityText] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [loadingPhase, setLoadingPhase] = useState(0); | |
| const stepNumbers = { | |
| scope: 1, | |
| depth: 2, | |
| style: 3, | |
| chunking: 4, | |
| familiarity: 5 | |
| }; | |
| const totalSteps = 5; | |
| const stepNumber = stepNumbers[currentStep]; | |
| const progressPct = (stepNumber / totalSteps) * 100; | |
| function onNext() { | |
| // Force sync of local state before navigating | |
| if (currentStep === 'scope' && scope === 'specific_section') { | |
| // Make sure the textarea is synced before navigation | |
| const textarea = document.getElementById('scope-details'); | |
| if (textarea) { | |
| setScopeDetails(textarea.value); | |
| } | |
| } | |
| if (currentStep === 'scope') setCurrentStep('depth'); | |
| else if (currentStep === 'depth') setCurrentStep('style'); | |
| else if (currentStep === 'style') setCurrentStep('chunking'); | |
| else if (currentStep === 'chunking') setCurrentStep('familiarity'); | |
| } | |
| function onBack() { | |
| if (currentStep === 'depth') setCurrentStep('scope'); | |
| else if (currentStep === 'style') setCurrentStep('depth'); | |
| else if (currentStep === 'chunking') setCurrentStep('style'); | |
| else if (currentStep === 'familiarity') setCurrentStep('chunking'); | |
| } | |
| function handleStart() { | |
| setIsLoading(true); | |
| setLoadingPhase(0); | |
| // Simulate loading phases | |
| const phases = [1200, 1200, 1400]; | |
| let i = 0; | |
| const timer = setInterval(() => { | |
| setLoadingPhase(prev => prev + 1); | |
| i += 1; | |
| if (i >= phases.length) { | |
| clearInterval(timer); | |
| // Call onComplete with all the collected data | |
| setTimeout(() => { | |
| onComplete({ | |
| scope, | |
| scopeDetails, | |
| depth, | |
| style, | |
| familiarity, | |
| chunkSize, | |
| useChunking, | |
| familiarityText, | |
| academicBackground | |
| }); | |
| }, 1000); | |
| } | |
| }, phases[0]); | |
| } | |
| if (isLoading) { | |
| return <LoadingScreen fileName={fileName} phase={loadingPhase} />; | |
| } | |
| return ( | |
| <div className="h-full flex flex-col bg-white"> | |
| <div className="border border-gray-200 rounded-t-lg overflow-hidden flex-1 flex flex-col"> | |
| <div className="h-2 bg-gray-200"> | |
| <div className="h-2 bg-indigo-500 transition-all duration-500" style={{ width: progressPct + '%' }} /> | |
| </div> | |
| <div className="p-6 flex-1 flex flex-col"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <div className="text-sm text-gray-500">Step {stepNumber} of {totalSteps}</div> | |
| <div className="text-sm text-gray-500">Setup</div> | |
| </div> | |
| <div className="flex-1"> | |
| {currentStep === 'scope' && <StepScope scope={scope} setScope={setScope} initialScopeDetails={scopeDetails} onScopeDetailsChange={setScopeDetails} />} | |
| {currentStep === 'depth' && <StepDepth depth={depth} setDepth={setDepth} />} | |
| {currentStep === 'style' && <StepStyle style={style} setStyle={setStyle} />} | |
| {currentStep === 'chunking' && <StepChunking useChunking={useChunking} setUseChunking={setUseChunking} chunkSize={chunkSize} setChunkSize={setChunkSize} />} | |
| {currentStep === 'familiarity' && ( | |
| <StepFamiliarity | |
| familiarity={familiarity} | |
| setFamiliarity={setFamiliarity} | |
| familiarityText={familiarityText} | |
| setFamiliarityText={setFamiliarityText} | |
| academicBackground={academicBackground} | |
| /> | |
| )} | |
| </div> | |
| <div className="mt-6 flex items-center justify-between"> | |
| <button | |
| onClick={onBack} | |
| className="rounded-lg px-4 py-2 bg-gray-100 hover:bg-gray-200 border border-gray-300 text-gray-700 transition-colors" | |
| disabled={currentStep === 'scope'} | |
| > | |
| Back | |
| </button> | |
| {currentStep !== 'familiarity' && ( | |
| <button | |
| onClick={onNext} | |
| disabled={(currentStep === 'scope' && (!scope || (scope === 'specific_section' && !scopeDetails.trim()))) || | |
| (currentStep === 'depth' && !depth) || | |
| (currentStep === 'style' && !style) || | |
| (currentStep === 'chunking' && !useChunking)} | |
| className="rounded-lg px-5 py-2.5 bg-indigo-500 disabled:bg-indigo-300 hover:bg-indigo-600 transition text-white font-medium shadow-md disabled:cursor-not-allowed" | |
| > | |
| Next | |
| </button> | |
| )} | |
| {currentStep === 'familiarity' && ( | |
| <button | |
| onClick={handleStart} | |
| className="rounded-lg px-5 py-2.5 bg-emerald-500 hover:bg-emerald-600 transition text-white font-medium shadow-md" | |
| > | |
| Let's go — start | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| function StepDepth({ depth, setDepth }) { | |
| return ( | |
| <div> | |
| <h3 className="text-2xl font-semibold text-gray-900">How deep do you want to go?</h3> | |
| <p className="mt-2 text-gray-600">Select the level of understanding you're aiming for.</p> | |
| <div className="mt-6 space-y-3"> | |
| {depthOptions.map(option => ( | |
| <SelectableCard | |
| key={option.key} | |
| selected={depth === option.key} | |
| onClick={() => setDepth(option.key)} | |
| title={option.title} | |
| desc={option.desc} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function StepStyle({ style, setStyle }) { | |
| return ( | |
| <div> | |
| <h3 className="text-2xl font-semibold text-gray-900">What's your learning style preference?</h3> | |
| <p className="mt-2 text-gray-600">Choose the approach that works best for you.</p> | |
| <div className="mt-6 space-y-3"> | |
| {styleOptions.map(option => ( | |
| <SelectableCard | |
| key={option.key} | |
| selected={style === option.key} | |
| onClick={() => setStyle(option.key)} | |
| title={option.title} | |
| desc={option.desc} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function StepChunking({ useChunking, setUseChunking, chunkSize, setChunkSize }) { | |
| return ( | |
| <div> | |
| <h3 className="text-2xl font-semibold text-gray-900">How would you like to structure your learning?</h3> | |
| <p className="mt-2 text-gray-600">Choose how you want to break down the content.</p> | |
| <div className="mt-6 space-y-4"> | |
| {chunkingOptions.map(option => ( | |
| <SelectableCard | |
| key={option.key} | |
| selected={useChunking === option.key} | |
| onClick={() => setUseChunking(option.key)} | |
| title={option.title} | |
| desc={option.desc} | |
| badge={option.badge} | |
| /> | |
| ))} | |
| </div> | |
| {useChunking === 'guided' && ( | |
| <div className="mt-6"> | |
| <h4 className="text-lg font-medium text-gray-800 mb-3">Preferred chunk size</h4> | |
| <div className="space-y-3"> | |
| {chunkSizeOptions.map(option => ( | |
| <SelectableCard | |
| key={option.key} | |
| selected={chunkSize === option.key} | |
| onClick={() => setChunkSize(option.key)} | |
| title={option.title} | |
| desc={option.desc} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function StepFamiliarity({ familiarity, setFamiliarity, familiarityText, setFamiliarityText, academicBackground }) { | |
| return ( | |
| <div> | |
| <h3 className="text-2xl font-semibold text-gray-900">How familiar are you with this topic?</h3> | |
| <p className="mt-2 text-gray-600">This helps us adjust our teaching approach.</p> | |
| {academicBackground && ( | |
| <div className="mt-4 p-3 bg-gray-50 rounded-lg border border-gray-200"> | |
| <p className="text-sm text-gray-600">Your background: <span className="text-gray-800">{academicBackground}</span></p> | |
| </div> | |
| )} | |
| <div className="mt-6"> | |
| <input | |
| type="range" | |
| min={0} | |
| max={4} | |
| step={1} | |
| value={familiarity} | |
| onChange={e => setFamiliarity(parseInt(e.target.value))} | |
| className="w-full accent-indigo-500" | |
| /> | |
| <div className="flex justify-between text-xs text-gray-500 mt-2"> | |
| {familiarityLabels.map((label, i) => ( | |
| <div key={i} className={`text-center ${i === familiarity ? 'text-gray-800 font-medium' : ''}`}> | |
| {label} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="mt-6"> | |
| <label htmlFor="familiarity-details" className="block text-sm font-medium text-gray-700 mb-2"> | |
| Additional context (optional) | |
| </label> | |
| <textarea | |
| id="familiarity-details" | |
| value={familiarityText} | |
| onChange={(e) => setFamiliarityText(e.target.value)} | |
| placeholder="Any specific knowledge, challenges, or questions about this topic..." | |
| className="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none" | |
| rows="3" | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| }; | |
| function LoadingScreen({ fileName, phase }) { | |
| const messages = [ | |
| 'Analyzing your paper...', | |
| 'Setting up your learning path...', | |
| 'Preparing personalized tutoring...', | |
| 'All set. Launching your tutor...' | |
| ]; | |
| const message = messages[Math.min(phase, messages.length - 1)]; | |
| return ( | |
| <div className="h-full flex items-center justify-center p-10 bg-white"> | |
| <div className="text-center"> | |
| <div className="mx-auto h-16 w-16 rounded-full border-4 border-gray-200 border-t-indigo-500 animate-spin" /> | |
| <h3 className="mt-6 text-2xl font-semibold text-gray-900">{message}</h3> | |
| <p className="mt-2 text-gray-600">{fileName}</p> | |
| <p className="mt-6 text-xs text-gray-500">Setting up your personalized learning experience...</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function SelectableCard({ selected, onClick, title, desc, badge }) { | |
| return ( | |
| <button | |
| type="button" | |
| // Prevent the button from taking focus on mouse/pointer down | |
| onMouseDown={(e) => e.preventDefault()} | |
| onPointerDown={(e) => e.preventDefault()} | |
| onClick={onClick} | |
| className={`text-left rounded-lg border transition p-4 hover:-translate-y-0.5 active:translate-y-0 bg-white hover:bg-gray-50 w-full ${ | |
| selected ? 'border-indigo-500 ring-2 ring-indigo-200 bg-indigo-50' : 'border-gray-200' | |
| }`} | |
| > | |
| <div className="flex items-start justify-between"> | |
| <div className="text-base font-medium text-gray-900">{title}</div> | |
| {badge && ( | |
| <span className="text-[10px] uppercase tracking-wide bg-indigo-100 text-indigo-700 px-2 py-1 rounded-md border border-indigo-200"> | |
| {badge} | |
| </span> | |
| )} | |
| </div> | |
| {desc && <p className="text-sm text-gray-600 mt-1">{desc}</p>} | |
| </button> | |
| ); | |
| } | |
| // Memoized StepScope to prevent re-renders from parent | |
| const StepScope = memo(({ scope, setScope, initialScopeDetails, onScopeDetailsChange }) => { | |
| // Local state for immediate UI updates - doesn't trigger parent re-renders | |
| const [localScopeDetails, setLocalScopeDetails] = useState(initialScopeDetails); | |
| const localScopeDetailsRef = useRef(null); | |
| // Auto-focus when textarea appears | |
| useLayoutEffect(() => { | |
| if (scope === 'specific_section' && localScopeDetailsRef.current) { | |
| const textarea = localScopeDetailsRef.current; | |
| textarea.focus(); | |
| textarea.setSelectionRange(textarea.value.length, textarea.value.length); | |
| } | |
| }, [scope]); | |
| // Sync local state to parent when user finishes typing (onBlur) or navigates | |
| const handleBlur = () => { | |
| onScopeDetailsChange(localScopeDetails); | |
| }; | |
| // Update parent immediately when switching scope types | |
| const handleScopeChange = (optionKey) => { | |
| setScope(optionKey); | |
| if (optionKey !== 'specific_section') { | |
| setLocalScopeDetails(''); | |
| onScopeDetailsChange(''); | |
| } | |
| }; | |
| return ( | |
| <div> | |
| <h3 className="text-2xl font-semibold text-gray-900">What's the scope of your learning?</h3> | |
| <p className="mt-2 text-gray-600">Choose what you want to focus on in this paper.</p> | |
| <div className="mt-6 space-y-3"> | |
| {scopeOptions.map(option => ( | |
| <SelectableCard | |
| key={option.key} | |
| selected={scope === option.key} | |
| onClick={() => handleScopeChange(option.key)} | |
| title={option.title} | |
| desc={option.desc} | |
| /> | |
| ))} | |
| {scope === 'specific_section' && ( | |
| <div className="mt-3 ml-4"> | |
| <label htmlFor="scope-details" className="block text-sm font-medium text-gray-700 mb-2"> | |
| Which section would you like to focus on? <span className="text-red-500">*</span> | |
| </label> | |
| <textarea | |
| ref={localScopeDetailsRef} | |
| id="scope-details" | |
| value={localScopeDetails} | |
| onChange={(e) => setLocalScopeDetails(e.target.value)} // Only local state change! | |
| onBlur={handleBlur} // Sync to parent when done typing | |
| placeholder="e.g., Introduction, Methods section, Equation 3.2, Figure 4 analysis, Discussion of results..." | |
| className="w-full px-4 py-3 rounded-lg border border-gray-300 text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none" | |
| rows="2" | |
| required | |
| /> | |
| {!localScopeDetails.trim() && ( | |
| <p className="mt-1 text-sm text-red-500">Please specify which section you want to focus on.</p> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }); | |
| export default OnboardingWizard; |