← back to ClawCoder
src/app/onboarding/page.tsx
220 lines
'use client'
import { useCallback, useState } from 'react'
import { useStore } from '@/lib/store'
import { ONBOARDING_QUESTIONS } from '@/lib/onboarding/questions'
import { resolveProfile } from '@/lib/onboarding/resolver'
import { QuestionScreen } from '@/components/onboarding/question-screen'
import { ReviewScreen } from '@/components/onboarding/review-screen'
import { ResultScreen } from '@/components/onboarding/result-screen'
const TOTAL_STEPS = ONBOARDING_QUESTIONS.length
interface GeneratedBundle {
claudeMd: string
rulesFiles: { path: string; content: string }[]
settings: object
settingsJson: string
profile: {
slug: string
displayName: string
ageGroup: string
skillLevel: string
persona: string
}
}
type Phase = 'questions' | 'review' | 'generating' | 'result'
export default function OnboardingPage() {
const {
currentStep,
setStep,
onboardingAnswers,
setAnswer,
isLoading,
setLoading,
} = useStore()
const [phase, setPhase] = useState<Phase>('questions')
const [bundle, setBundle] = useState<GeneratedBundle | null>(null)
const [fullProfile, setFullProfile] = useState<{
skillBundle: { name: string; description: string }[]
mcpServers: {
name: string
package: string
installCmd: string
requiresKey: boolean
trustLevel: string
}[]
learningPath: {
title: string
url: string
type: string
level: string
}[]
} | null>(null)
const [error, setError] = useState<string | null>(null)
// The step index is 0-based internally; questions are 1-based.
const stepIndex = currentStep
const question = ONBOARDING_QUESTIONS[stepIndex]
const handleSelect = useCallback(
(value: string) => {
if (!question) return
setAnswer(question.id, value)
},
[question, setAnswer]
)
const handleNext = useCallback(() => {
if (!question) return
// On the final step, check the user's choice
if (stepIndex === TOTAL_STEPS - 1) {
const finishAnswer = onboardingAnswers.finish
if (finishAnswer === 'review') {
setPhase('review')
} else {
// generate immediately
doGenerate()
}
return
}
setStep(stepIndex + 1)
}, [stepIndex, question, onboardingAnswers, setStep])
const handleBack = useCallback(() => {
if (phase === 'review') {
setPhase('questions')
return
}
if (stepIndex > 0) {
setStep(stepIndex - 1)
}
}, [stepIndex, phase, setStep])
const handleEditFromReview = useCallback(
(step: number) => {
// step is 1-based in questions data, store index is 0-based
setStep(step - 1)
setPhase('questions')
},
[setStep]
)
const doGenerate = useCallback(async () => {
setPhase('generating')
setLoading(true)
setError(null)
try {
const resolved = resolveProfile(onboardingAnswers)
const res = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
profileSlug: resolved.slug,
vars: {
projectName: 'My Project',
projectDescription:
'A project configured by ClawCoder onboarding wizard.',
techStack: 'To be configured',
},
}),
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'Generation failed' }))
throw new Error(err.error || 'Generation failed')
}
const data: GeneratedBundle = await res.json()
setBundle(data)
// Store full profile data for Skills/MCP/Learning tabs
setFullProfile({
skillBundle: resolved.skillBundle,
mcpServers: resolved.mcpServers,
learningPath: resolved.learningPath,
})
setPhase('result')
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong')
setPhase('questions')
// Go back to the finish step so the user can retry
setStep(TOTAL_STEPS - 1)
} finally {
setLoading(false)
}
}, [onboardingAnswers, setLoading, setStep])
// --- Render by phase ---
if (phase === 'generating') {
return (
<div className="flex min-h-screen flex-col items-center justify-center px-4">
<div className="flex flex-col items-center gap-6">
<div className="h-12 w-12 animate-spin rounded-full border-4 border-slate-200 border-t-blue-600" />
<h2 className="text-xl font-semibold text-slate-900">
Generating your setup...
</h2>
<p className="text-sm text-slate-500">
Building CLAUDE.md, rules, hooks, and more.
</p>
</div>
</div>
)
}
if (phase === 'result' && bundle) {
return <ResultScreen bundle={bundle} fullProfile={fullProfile} />
}
if (phase === 'review') {
return (
<ReviewScreen
answers={onboardingAnswers}
onEdit={handleEditFromReview}
onGenerate={doGenerate}
/>
)
}
// Default: questions phase
if (!question) {
// Shouldn't happen, but safety net
setStep(0)
return null
}
return (
<>
{error && (
<div className="fixed left-1/2 top-4 z-[60] -translate-x-1/2 rounded-xl border border-red-200 bg-red-50 px-6 py-3 text-sm text-red-700 shadow-lg">
{error}
<button
type="button"
onClick={() => setError(null)}
className="ml-3 font-medium text-red-900 hover:text-red-700"
>
Dismiss
</button>
</div>
)}
<QuestionScreen
question={question}
totalSteps={TOTAL_STEPS}
selectedValue={onboardingAnswers[question.id]}
onSelect={handleSelect}
onBack={handleBack}
onNext={handleNext}
isFirst={stepIndex === 0}
/>
</>
)
}