import { useState, useCallback, useEffect, useRef } from 'react'; import { PodcastAnalysis, PodcastEstimate, Query, Research, Script, Knobs, Job, CreateProjectPayload, } from '../components/PodcastMaker/types'; import { BlogResearchResponse, ResearchProvider } from '../services/blogWriterApi'; import { podcastApi } from '../services/podcastApi'; export interface PodcastProjectState { // Project metadata project: { id: string; idea: string; duration: number; speakers: number; avatarUrl?: string | null; avatarPrompt?: string | null; avatarPersonaId?: string | null; } | null; // Step results analysis: PodcastAnalysis | null; queries: Query[]; selectedQueries: Set; research: Research | null; rawResearch: BlogResearchResponse | null; estimate: PodcastEstimate | null; scriptData: Script | null; // Render jobs renderJobs: Job[]; // Settings knobs: Knobs; researchProvider: ResearchProvider; budgetCap: number; // UI state showScriptEditor: boolean; showRenderQueue: boolean; // Current step tracking currentStep: 'create' | 'analysis' | 'research' | 'script' | 'render' | null; // Final combined video finalVideoUrl?: string | null; // Timestamps createdAt?: string; updatedAt?: string; } const DEFAULT_KNOBS: Knobs = { voice_emotion: "neutral", voice_speed: 1, resolution: "720p", scene_length_target: 45, sample_rate: 24000, bitrate: "standard", }; const DEFAULT_STATE: PodcastProjectState = { project: null, analysis: null, queries: [], selectedQueries: new Set(), research: null, rawResearch: null, estimate: null, scriptData: null, renderJobs: [], knobs: DEFAULT_KNOBS, researchProvider: "exa", budgetCap: 50, showScriptEditor: false, showRenderQueue: false, currentStep: null, }; const STORAGE_KEY = 'podcast_project_state'; export const usePodcastProjectState = () => { const [state, setState] = useState(() => { // Initialize from localStorage if available try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); // Restore Sets from arrays const restoredState: PodcastProjectState = { ...DEFAULT_STATE, ...parsed, selectedQueries: parsed.selectedQueries ? new Set(parsed.selectedQueries) : new Set(), renderJobs: parsed.renderJobs || [], }; return restoredState; } } catch (error) { console.error('Error loading podcast project state from localStorage:', error); } return DEFAULT_STATE; }); // Debounce ref for database sync const syncTimeoutRef = useRef(null); // Persist state to localStorage on every change useEffect(() => { try { // Convert Sets to arrays for JSON serialization const serializableState = { ...state, selectedQueries: Array.from(state.selectedQueries), }; localStorage.setItem(STORAGE_KEY, JSON.stringify(serializableState)); } catch (error) { console.error('Error saving podcast project state to localStorage:', error); } }, [state]); // Sync to database after major steps (debounced) useEffect(() => { if (!state.project || !state.project.id) return; // Capture project ID to avoid closure issues const projectId = state.project.id; // Clear existing timeout if (syncTimeoutRef.current) { clearTimeout(syncTimeoutRef.current); } // Debounce database sync (wait 2 seconds after last change) syncTimeoutRef.current = setTimeout(async () => { try { const dbState = { analysis: state.analysis, queries: state.queries, selected_queries: Array.from(state.selectedQueries), research: state.research, raw_research: state.rawResearch, estimate: state.estimate, script_data: state.scriptData, render_jobs: state.renderJobs, knobs: state.knobs, research_provider: state.researchProvider, show_script_editor: state.showScriptEditor, show_render_queue: state.showRenderQueue, current_step: state.currentStep, status: state.currentStep === 'render' && state.renderJobs.every(j => j.status === 'completed') ? 'completed' : 'in_progress', }; await podcastApi.saveProject(projectId, dbState); } catch (error) { console.error('Error syncing project to database:', error); // Don't throw - localStorage is still working } }, 2000); return () => { if (syncTimeoutRef.current) { clearTimeout(syncTimeoutRef.current); } }; }, [ state.project, state.analysis, state.queries, state.selectedQueries, state.research, state.rawResearch, state.estimate, state.scriptData, state.renderJobs, state.knobs, state.researchProvider, state.showScriptEditor, state.showRenderQueue, state.currentStep, ]); // Setters const setProject = useCallback((project: PodcastProjectState['project']) => { setState((prev) => ({ ...prev, project, currentStep: project ? 'analysis' : null, updatedAt: new Date().toISOString() })); }, []); const setAnalysis = useCallback((analysis: PodcastProjectState['analysis']) => { setState((prev) => ({ ...prev, analysis, currentStep: analysis ? 'research' : prev.currentStep, updatedAt: new Date().toISOString() })); }, []); const setQueries = useCallback((queries: Query[]) => { setState((prev) => ({ ...prev, queries, updatedAt: new Date().toISOString() })); }, []); const setSelectedQueries = useCallback((selectedQueries: Set | ((prev: Set) => Set)) => { setState((prev) => { const newQueries = typeof selectedQueries === 'function' ? selectedQueries(prev.selectedQueries) : selectedQueries; return { ...prev, selectedQueries: newQueries, updatedAt: new Date().toISOString() }; }); }, []); const setResearch = useCallback((research: PodcastProjectState['research']) => { setState((prev) => ({ ...prev, research, currentStep: research ? 'script' : prev.currentStep, updatedAt: new Date().toISOString() })); }, []); const setRawResearch = useCallback((rawResearch: PodcastProjectState['rawResearch']) => { setState((prev) => ({ ...prev, rawResearch, updatedAt: new Date().toISOString() })); }, []); const setEstimate = useCallback((estimate: PodcastProjectState['estimate']) => { setState((prev) => ({ ...prev, estimate, updatedAt: new Date().toISOString() })); }, []); const setScriptData = useCallback((scriptData: PodcastProjectState['scriptData']) => { setState((prev) => ({ ...prev, scriptData, currentStep: scriptData ? 'render' : prev.currentStep, updatedAt: new Date().toISOString() })); }, []); const setRenderJobs = useCallback((renderJobs: Job[]) => { setState((prev) => ({ ...prev, renderJobs, updatedAt: new Date().toISOString() })); }, []); const updateRenderJob = useCallback((sceneId: string, updates: Partial) => { setState((prev) => { const existingJob = prev.renderJobs.find((job) => job.sceneId === sceneId); if (existingJob) { // Update existing job return { ...prev, renderJobs: prev.renderJobs.map((job) => job.sceneId === sceneId ? { ...job, ...updates } : job ), updatedAt: new Date().toISOString(), }; } else { // Create new job if it doesn't exist const newJob: Job = { sceneId, title: updates.title || sceneId, status: updates.status || "idle", progress: updates.progress || 0, previewUrl: updates.previewUrl || null, finalUrl: updates.finalUrl || null, videoUrl: updates.videoUrl || null, imageUrl: updates.imageUrl || null, jobId: updates.jobId || null, taskId: updates.taskId || null, cost: updates.cost || null, provider: updates.provider || null, voiceId: updates.voiceId || null, fileSize: updates.fileSize || null, avatarImageUrl: updates.avatarImageUrl || null, }; return { ...prev, renderJobs: [...prev.renderJobs, newJob], updatedAt: new Date().toISOString(), }; } }); }, []); const setKnobs = useCallback((knobs: Knobs) => { setState((prev) => ({ ...prev, knobs, updatedAt: new Date().toISOString() })); }, []); const setResearchProvider = useCallback((provider: ResearchProvider) => { setState((prev) => ({ ...prev, researchProvider: provider, updatedAt: new Date().toISOString() })); }, []); const setBudgetCap = useCallback((cap: number) => { setState((prev) => ({ ...prev, budgetCap: cap, updatedAt: new Date().toISOString() })); }, []); const setShowScriptEditor = useCallback((show: boolean) => { setState((prev) => ({ ...prev, showScriptEditor: show, updatedAt: new Date().toISOString() })); }, []); const setShowRenderQueue = useCallback((show: boolean) => { setState((prev) => ({ ...prev, showRenderQueue: show, updatedAt: new Date().toISOString() })); }, []); const setCurrentStep = useCallback((step: PodcastProjectState['currentStep']) => { setState((prev) => ({ ...prev, currentStep: step, updatedAt: new Date().toISOString() })); }, []); // Reset state const resetState = useCallback(() => { setState(DEFAULT_STATE); localStorage.removeItem(STORAGE_KEY); }, []); // Initialize project from payload const initializeProject = useCallback(async (payload: CreateProjectPayload, projectId: string) => { // Create project in database try { await podcastApi.createProjectInDb({ project_id: projectId, idea: payload.ideaOrUrl, duration: payload.duration, speakers: payload.speakers, budget_cap: payload.budgetCap, }); } catch (error) { console.error('Error creating project in database:', error); // Continue anyway - localStorage fallback } setState((prev) => ({ ...prev, project: { id: projectId, idea: payload.ideaOrUrl, duration: payload.duration, speakers: payload.speakers, avatarUrl: payload.avatarUrl || null, avatarPrompt: null, // Will be set when avatar is generated avatarPersonaId: null, }, knobs: payload.knobs, budgetCap: payload.budgetCap, currentStep: 'analysis', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), })); }, []); // Load project from database const loadProjectFromDb = useCallback(async (projectId: string) => { try { const dbProject = await podcastApi.loadProject(projectId); // Restore state from database setState((prev) => ({ ...prev, project: { id: dbProject.project_id, idea: dbProject.idea, duration: dbProject.duration, speakers: dbProject.speakers, avatarUrl: dbProject.avatar_url || null, avatarPrompt: dbProject.avatar_prompt || null, avatarPersonaId: dbProject.avatar_persona_id || null, }, analysis: dbProject.analysis, queries: dbProject.queries || [], selectedQueries: new Set(dbProject.selected_queries || []), research: dbProject.research, rawResearch: dbProject.raw_research, estimate: dbProject.estimate, scriptData: dbProject.script_data, renderJobs: dbProject.render_jobs || [], knobs: dbProject.knobs || DEFAULT_KNOBS, researchProvider: dbProject.research_provider || 'exa', budgetCap: dbProject.budget_cap || 50, showScriptEditor: dbProject.show_script_editor || false, showRenderQueue: dbProject.show_render_queue || false, currentStep: dbProject.current_step || null, finalVideoUrl: dbProject.final_video_url || null, createdAt: dbProject.created_at, updatedAt: dbProject.updated_at, })); } catch (error) { console.error('Error loading project from database:', error); throw error; } }, []); return { // State ...state, // Setters setProject, setAnalysis, setQueries, setSelectedQueries, setResearch, setRawResearch, setEstimate, setScriptData, setRenderJobs, updateRenderJob, setKnobs, setResearchProvider, setBudgetCap, setShowScriptEditor, setShowRenderQueue, setCurrentStep, // Helpers resetState, initializeProject, loadProjectFromDb, }; };