SokratesAI / frontend /src /components /OnboardingWizard.jsx
Alleinzellgaenger's picture
Implement comprehensive onboarding flow with white theme
70e74ea
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;