WIP: AI Podcast Maker and YouTube Creator Studio integration
This commit is contained in:
372
frontend/src/hooks/usePodcastProjectState.ts
Normal file
372
frontend/src/hooks/usePodcastProjectState.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
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 } | null;
|
||||
|
||||
// Step results
|
||||
analysis: PodcastAnalysis | null;
|
||||
queries: Query[];
|
||||
selectedQueries: Set<string>;
|
||||
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;
|
||||
|
||||
// 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: "google",
|
||||
budgetCap: 50,
|
||||
showScriptEditor: false,
|
||||
showRenderQueue: false,
|
||||
currentStep: null,
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'podcast_project_state';
|
||||
|
||||
export const usePodcastProjectState = () => {
|
||||
const [state, setState] = useState<PodcastProjectState>(() => {
|
||||
// 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<NodeJS.Timeout | null>(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<string> | ((prev: Set<string>) => Set<string>)) => {
|
||||
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<Job>) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
renderJobs: prev.renderJobs.map((job) =>
|
||||
job.sceneId === sceneId ? { ...job, ...updates } : job
|
||||
),
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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 || 'google',
|
||||
budgetCap: dbProject.budget_cap || 50,
|
||||
showScriptEditor: dbProject.show_script_editor || false,
|
||||
showRenderQueue: dbProject.show_render_queue || false,
|
||||
currentStep: dbProject.current_step || 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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user