|
|
|
|
| import { Stage } from '@/components/stage'; |
| import { ThemeProvider } from '@/lib/hooks/use-theme'; |
| import { useStageStore } from '@/lib/store'; |
| import { loadImageMapping } from '@/lib/utils/image-storage'; |
| import { useEffect, useRef, useState, useCallback } from 'react'; |
| import { useParams } from 'react-router-dom'; |
| import { useSceneGenerator } from '@/lib/hooks/use-scene-generator'; |
| import { useMediaGenerationStore } from '@/lib/store/media-generation'; |
| import { useWhiteboardHistoryStore } from '@/lib/store/whiteboard-history'; |
| import { createLogger } from '@/lib/logger'; |
| import { MediaStageProvider } from '@/lib/contexts/media-stage-context'; |
| import { generateMediaForOutlines } from '@/lib/media/media-orchestrator'; |
|
|
| const log = createLogger('Classroom'); |
|
|
| export default function ClassroomDetailPage() { |
| const params = useParams(); |
| const classroomId = params?.id as string; |
|
|
| const { loadFromStorage } = useStageStore(); |
|
|
| const [loading, setLoading] = useState(true); |
| const [error, setError] = useState<string | null>(null); |
|
|
| const generationStartedRef = useRef(false); |
|
|
| const { generateRemaining, retrySingleOutline, stop } = useSceneGenerator({ |
| onComplete: () => { |
| log.info('[Classroom] All scenes generated'); |
| }, |
| }); |
|
|
| const loadClassroom = useCallback(async () => { |
| try { |
| await loadFromStorage(classroomId); |
|
|
| |
| if (!useStageStore.getState().stage) { |
| log.info('No IndexedDB data, trying server-side storage for:', classroomId); |
| try { |
| const res = await fetch(`/api/classroom?id=${encodeURIComponent(classroomId)}`); |
| if (res.ok) { |
| const json = await res.json(); |
| if (json.success && json.classroom) { |
| const { stage, scenes } = json.classroom; |
| useStageStore.getState().setStage(stage); |
| useStageStore.setState({ |
| scenes, |
| currentSceneId: scenes[0]?.id ?? null, |
| }); |
| log.info('Loaded from server-side storage:', classroomId); |
|
|
| |
| |
| |
| if (stage.generatedAgentConfigs?.length) { |
| const { saveGeneratedAgents } = await import('@/lib/orchestration/registry/store'); |
| await saveGeneratedAgents(stage.id, stage.generatedAgentConfigs); |
| log.info('Hydrated server-generated agents for stage:', stage.id); |
| } |
| } |
| } |
| } catch (fetchErr) { |
| log.warn('Server-side storage fetch failed:', fetchErr); |
| } |
| } |
|
|
| |
| await useMediaGenerationStore.getState().restoreFromDB(classroomId); |
| |
| const { loadGeneratedAgentsForStage, useAgentRegistry } = |
| await import('@/lib/orchestration/registry/store'); |
| const generatedAgentIds = await loadGeneratedAgentsForStage(classroomId); |
| const { useSettingsStore } = await import('@/lib/store/settings'); |
| if (generatedAgentIds.length > 0) { |
| |
| useSettingsStore.getState().setAgentMode('auto'); |
| useSettingsStore.getState().setSelectedAgentIds(generatedAgentIds); |
| } else { |
| |
| |
| |
| const stage = useStageStore.getState().stage; |
| const stageAgentIds = stage?.agentIds; |
| const registry = useAgentRegistry.getState(); |
| const cleanIds = stageAgentIds?.filter((id) => { |
| const a = registry.getAgent(id); |
| return a && !a.isGenerated; |
| }); |
| useSettingsStore.getState().setAgentMode('preset'); |
| useSettingsStore |
| .getState() |
| .setSelectedAgentIds( |
| cleanIds && cleanIds.length > 0 ? cleanIds : ['default-1', 'default-2', 'default-3'], |
| ); |
| } |
| } catch (error) { |
| log.error('Failed to load classroom:', error); |
| setError(error instanceof Error ? error.message : 'Failed to load classroom'); |
| } finally { |
| setLoading(false); |
| } |
| }, [classroomId, loadFromStorage]); |
|
|
| useEffect(() => { |
| |
| |
| setLoading(true); |
| setError(null); |
| generationStartedRef.current = false; |
|
|
| |
| |
| |
| const mediaStore = useMediaGenerationStore.getState(); |
| mediaStore.revokeObjectUrls(); |
| useMediaGenerationStore.setState({ tasks: {} }); |
|
|
| |
| useWhiteboardHistoryStore.getState().clearHistory(); |
|
|
| loadClassroom(); |
|
|
| |
| return () => { |
| stop(); |
| }; |
| }, [classroomId, loadClassroom, stop]); |
|
|
| |
| useEffect(() => { |
| if (loading || error || generationStartedRef.current) return; |
|
|
| const state = useStageStore.getState(); |
| const { outlines, scenes, stage } = state; |
|
|
| |
| const completedOrders = new Set(scenes.map((s) => s.order)); |
| const hasPending = outlines.some((o) => !completedOrders.has(o.order)); |
|
|
| if (hasPending && stage) { |
| generationStartedRef.current = true; |
|
|
| |
| const genParamsStr = sessionStorage.getItem('generationParams'); |
| const params = genParamsStr ? JSON.parse(genParamsStr) : {}; |
|
|
| |
| const storageIds = (params.pdfImages || []) |
| .map((img: { storageId?: string }) => img.storageId) |
| .filter(Boolean); |
|
|
| loadImageMapping(storageIds).then((imageMapping) => { |
| generateRemaining({ |
| pdfImages: params.pdfImages, |
| imageMapping, |
| stageInfo: { |
| name: stage.name || '', |
| description: stage.description, |
| style: stage.style, |
| }, |
| agents: params.agents, |
| userProfile: params.userProfile, |
| languageDirective: params.languageDirective || stage.languageDirective, |
| }); |
| }); |
| } else if (outlines.length > 0 && stage) { |
| |
| |
| |
| generationStartedRef.current = true; |
| generateMediaForOutlines(outlines, stage.id).catch((err) => { |
| log.warn('[Classroom] Media generation resume error:', err); |
| }); |
| } |
| }, [loading, error, generateRemaining]); |
|
|
| return ( |
| <ThemeProvider> |
| <MediaStageProvider value={classroomId}> |
| <div className="h-screen flex flex-col overflow-hidden"> |
| {loading ? ( |
| <div className="flex-1 flex items-center justify-center bg-gray-50 dark:bg-gray-900"> |
| <div className="text-center text-muted-foreground"> |
| <p>Loading classroom...</p> |
| </div> |
| </div> |
| ) : error ? ( |
| <div className="flex-1 flex items-center justify-center bg-gray-50 dark:bg-gray-900"> |
| <div className="text-center"> |
| <p className="text-destructive mb-4">Error: {error}</p> |
| <button |
| onClick={() => { |
| setError(null); |
| setLoading(true); |
| loadClassroom(); |
| }} |
| className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" |
| > |
| Retry |
| </button> |
| </div> |
| </div> |
| ) : ( |
| <Stage onRetryOutline={retrySingleOutline} /> |
| )} |
| </div> |
| </MediaStageProvider> |
| </ThemeProvider> |
| ); |
| } |
|
|