'use client'; // Ops large-course mode — course builder. // // Two-layer flow with an explicit confirmation gate: // 1. Create → framework generates (framework_generating) → framework_ready. // 2. Operator REVIEWS the framework, then confirms ("确认框架,开始逐模块生成") // — only then the single-courseware agent generates each module one by one // (generating), with live per-module progress from the classroom jobs. // Actions: cancel / resume, regenerate the framework, regenerate one module // plus its continuity-dependent tail, publish once every module is ready. import { useCallback, useEffect, useRef, useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { ArrowLeft, BookOpen, CheckCircle2, Copy, ExternalLink, Layers, Loader2, Play, RefreshCw, Rocket, Sparkles, Square, XCircle, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { useI18n } from '@/lib/hooks/use-i18n'; import type { CourseDetail } from '@/lib/course-framework/types'; import { toast } from 'sonner'; import { opsApiFetch } from '@/lib/ops/api-fetch'; const MODULE_STATUS_STYLES: Record = { pending: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300', generating: 'bg-blue-100 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300', succeeded: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300', failed: 'bg-red-100 text-red-700 dark:bg-red-950/60 dark:text-red-300', }; const COURSE_STATUS_STYLES: Record = { queued: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300', framework_generating: 'bg-blue-100 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300', framework_ready: 'bg-amber-100 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300', generating: 'bg-blue-100 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300', completed: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300', failed: 'bg-red-100 text-red-700 dark:bg-red-950/60 dark:text-red-300', cancelled: 'bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400', }; export default function CourseBuilderPage() { const params = useParams(); const router = useRouter(); const { t } = useI18n(); const courseId = String(params?.courseId ?? ''); const [course, setCourse] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [acting, setActing] = useState(null); const [copied, setCopied] = useState(false); const load = useCallback(async () => { if (!courseId) return; try { const response = await opsApiFetch(`/api/courses/${courseId}`); const body = (await response.json()) as { course?: CourseDetail }; if (!response.ok || !body.course) { throw new Error(`HTTP ${response.status}`); } setCourse(body.course); setError(null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, [courseId]); useEffect(() => { void load(); const timer = setInterval(() => void load(), 3000); return () => clearInterval(timer); }, [load]); const actingRef = useRef(false); const act = async (action: string, fn: () => Promise) => { if (actingRef.current) return; actingRef.current = true; setActing(action); try { const response = await fn(); const body = (await response.json()) as { error?: string; details?: string }; if (!response.ok) { toast.error(body.details ?? body.error ?? `HTTP ${response.status}`); } else { toast.success(t(`course.actionDone.${action}`)); } await load(); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } finally { actingRef.current = false; setActing(null); } }; const publishCourse = async () => { await act('publish', () => opsApiFetch(`/api/courses/${courseId}/publish`, { method: 'POST', }), ); }; const copyLearnerLink = async () => { try { await navigator.clipboard.writeText(`${window.location.origin}/learn/course/${courseId}`); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { /* ignore */ } }; const allGenerated = !!course && course.counts.total > 0 && course.counts.succeeded === course.counts.total; const allPublished = !!course && course.counts.published === course.counts.total; const busy = !!course && (course.status === 'generating' || course.status === 'framework_generating' || course.status === 'queued'); const formatTime = (iso?: string) => iso ? new Date(iso).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }) : ''; if (loading && !course) { return (
); } if (error && !course) { return (

{error}

); } if (!course) return null; const progressPercent = course.counts.total > 0 ? Math.round((course.counts.succeeded / course.counts.total) * 100) : 0; const generatingModule = course.modules.find((m) => m.status === 'generating'); return (
{/* Header */}

{course.frameworkTitle ?? course.requirement.slice(0, 40)} {t(`course.status.${course.status}`)}

{course.id} · {t('course.createdAt')} {formatTime(course.createdAt)}

{busy && ( )} {!busy && course.status !== 'completed' && course.status !== 'framework_ready' && ( )}
{course.error && (
{course.error}
)} {/* Framework generating banner */} {course.status === 'framework_generating' && (

{t('course.frameworkGenerating')}

{t('course.frameworkGeneratingHint')}

)} {/* Framework review — the confirmation gate before module generation */} {course.status === 'framework_ready' && course.framework && (

{t('course.frameworkReviewTitle')}

{t('course.frameworkReviewHint')}

{course.framework.summary}

{t('course.targetAudience')}

{course.framework.targetAudience}

{t('course.languageDirectiveLabel')}

{course.framework.languageDirective}

{t('course.courseGoals')}

    {course.framework.courseGoals.map((goal) => (
  • {goal}
  • ))}
{t('course.continuityContract')}
{t('course.terminology')}
{course.framework.continuityContract.terminology.join(' · ')}
{t('course.teachingStyle')}
{course.framework.continuityContract.teachingStyle}
{t('course.difficultyProgression')}
{course.framework.continuityContract.difficultyProgression}
{t('course.assessmentStrategy')}
{course.framework.continuityContract.assessmentStrategy}
    {course.framework.modules.map((module) => (
  1. {module.index}
    {module.title} {module.estimatedMinutes ? ( · ~{module.estimatedMinutes} min ) : null}

    {module.description}

    {t('course.moduleGenerationDetails')}

    {t('course.learningObjectives')}

      {module.learningObjectives.map((objective) => (
    • {objective}
    • ))}
    {t('course.prerequisites')}
    {module.prerequisites || t('course.none')}
    {t('course.incomingKnowledge')}
    {module.incomingKnowledge.join(' · ') || t('course.none')}
    {t('course.outgoingKnowledge')}
    {module.outgoingKnowledge.join(' · ')}
    {t('course.excludedTopics')}
    {module.excludedTopics.join(' · ') || t('course.none')}

    {t('course.generationPrompt')}

    {module.generationPrompt}

  2. ))}
{t('course.confirmCostHint', { count: course.framework.modules.length })}
)} {/* Progress */} {(course.status === 'generating' || course.status === 'completed' || course.status === 'failed' || course.status === 'cancelled') && (
{t('course.progressTitle')} {course.counts.succeeded}/{course.counts.total} ·{' '} {t('course.publishedCount', { count: course.counts.published })}
{generatingModule && (
{t('course.moduleGenerating', { index: generatingModule.index, title: generatingModule.title, })} {generatingModule.totalScenes !== undefined && generatingModule.totalScenes > 0 && ( {t('course.jobSceneProgress', { done: generatingModule.scenesGenerated ?? 0, total: generatingModule.totalScenes, })} )}
{generatingModule.jobMessage && (

{generatingModule.jobMessage}

)}
)}

{t('course.progressHint')}

)} {/* Framework info (post-confirmation summary) */} {course.framework && course.status !== 'framework_ready' && (

{t('course.frameworkTitle')}

{course.framework.summary}

{t('course.languageDirective')}

{course.framework.languageDirective}

)} {/* Modules */}

{t('course.modulesTitle')}

{course.modules.length === 0 && (

{t('course.noModules')}

)} {course.modules.map((module) => (
{module.index}

{module.title}

{t(`course.moduleStatus.${module.status}`)} {module.published && ( {t('course.publishedBadge')} )}

{module.description}

{module.outputDigest && (
{t('course.actualOutput')} · {module.outputDigest.sceneCount}{' '} {t('course.scenesUnit')}

{module.outputDigest.summary}

{t('course.semanticHash')}: {module.outputDigest.semanticHash}

{module.outputDigest.coveredConcepts.length > 0 && (

{t('course.actualCoverage')}:{' '} {module.outputDigest.coveredConcepts.slice(0, 12).join(' · ')}

)} {module.outputDigest.interactiveExperiences.length > 0 && (

{t('course.actualInteractions')}:{' '} {module.outputDigest.interactiveExperiences.join(' · ')}

)} {module.outputDigest.assessments.length > 0 && (

{t('course.actualAssessments')}:{' '} {module.outputDigest.assessments.join(' · ')}

)}
)} {module.status === 'generating' && (
{module.totalScenes !== undefined && module.totalScenes > 0 ? (

{t('course.jobSceneProgress', { done: module.scenesGenerated ?? 0, total: module.totalScenes, })}

) : null} {module.jobMessage && (

{module.jobMessage}

)}
)} {module.error && (

{module.error}

)}
{module.status === 'succeeded' && module.classroomId && ( )} {module.published && module.publishedCoursewareId && ( )} {(module.status === 'failed' || module.status === 'succeeded') && ( )} {module.status === 'generating' && ( )} {module.status === 'succeeded' && ( )} {module.status === 'failed' && }
))}
); }