← 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}
      />
    </>
  )
}