Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements

This commit is contained in:
ajaysi
2026-02-28 20:06:26 +05:30
parent 08a1f4a1d8
commit 4828274cbf
162 changed files with 19489 additions and 4300 deletions

View File

@@ -8,6 +8,7 @@ import {
Knobs,
Job,
CreateProjectPayload,
PodcastBible,
} from '../components/PodcastMaker/types';
import { BlogResearchResponse, ResearchProvider } from '../services/blogWriterApi';
import { podcastApi } from '../services/podcastApi';
@@ -32,6 +33,7 @@ export interface PodcastProjectState {
rawResearch: BlogResearchResponse | null;
estimate: PodcastEstimate | null;
scriptData: Script | null;
bible: PodcastBible | null;
// Render jobs
renderJobs: Job[];
@@ -74,6 +76,7 @@ const DEFAULT_STATE: PodcastProjectState = {
rawResearch: null,
estimate: null,
scriptData: null,
bible: null,
renderJobs: [],
knobs: DEFAULT_KNOBS,
researchProvider: "exa",
@@ -150,6 +153,7 @@ export const usePodcastProjectState = () => {
raw_research: state.rawResearch,
estimate: state.estimate,
script_data: state.scriptData,
bible: state.bible,
render_jobs: state.renderJobs,
knobs: state.knobs,
research_provider: state.researchProvider,
@@ -182,6 +186,7 @@ export const usePodcastProjectState = () => {
state.scriptData,
state.renderJobs,
state.knobs,
state.bible,
state.researchProvider,
state.showScriptEditor,
state.showRenderQueue,
@@ -239,6 +244,10 @@ export const usePodcastProjectState = () => {
}));
}, []);
const setBible = useCallback((bible: PodcastBible | null) => {
setState((prev) => ({ ...prev, bible, updatedAt: new Date().toISOString() }));
}, []);
const setRenderJobs = useCallback((renderJobs: Job[]) => {
setState((prev) => ({ ...prev, renderJobs, updatedAt: new Date().toISOString() }));
}, []);
@@ -315,15 +324,19 @@ export const usePodcastProjectState = () => {
}, []);
// Initialize project from payload
const initializeProject = useCallback(async (payload: CreateProjectPayload, projectId: string) => {
const initializeProject = useCallback(async (payload: CreateProjectPayload, projectId: string, avatarUrlOverride?: string | null) => {
// Create project in database
let dbProject: any = null;
const finalAvatarUrl = avatarUrlOverride !== undefined ? avatarUrlOverride : payload.avatarUrl;
try {
await podcastApi.createProjectInDb({
dbProject = await podcastApi.createProjectInDb({
project_id: projectId,
idea: payload.ideaOrUrl,
duration: payload.duration,
speakers: payload.speakers,
budget_cap: payload.budgetCap,
avatar_url: finalAvatarUrl,
});
} catch (error) {
console.error('Error creating project in database:', error);
@@ -337,16 +350,19 @@ export const usePodcastProjectState = () => {
idea: payload.ideaOrUrl,
duration: payload.duration,
speakers: payload.speakers,
avatarUrl: payload.avatarUrl || null,
avatarUrl: finalAvatarUrl || null,
avatarPrompt: null, // Will be set when avatar is generated
avatarPersonaId: null,
},
knobs: payload.knobs,
budgetCap: payload.budgetCap,
bible: dbProject?.bible || null,
currentStep: 'analysis',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}));
return dbProject;
}, []);
// Load project from database
@@ -373,6 +389,7 @@ export const usePodcastProjectState = () => {
rawResearch: dbProject.raw_research,
estimate: dbProject.estimate,
scriptData: dbProject.script_data,
bible: dbProject.bible,
renderJobs: dbProject.render_jobs || [],
knobs: dbProject.knobs || DEFAULT_KNOBS,
researchProvider: dbProject.research_provider || 'exa',
@@ -403,6 +420,7 @@ export const usePodcastProjectState = () => {
setRawResearch,
setEstimate,
setScriptData,
setBible,
setRenderJobs,
updateRenderJob,
setKnobs,

View File

@@ -1,10 +1,10 @@
import { useState, useCallback, useEffect } from 'react';
import {
StoryGenerationRequest,
StoryPremiseResponse,
StoryOutlineResponse,
StoryContentResponse,
StoryFullGenerationResponse,
StoryProjectSummary,
CreateStoryProjectRequest,
UpdateStoryProjectRequest,
storyWriterApi,
} from '../services/storyWriterApi';
export interface SceneAnimationResume {
@@ -15,6 +15,12 @@ export interface SceneAnimationResume {
}
export interface StoryWriterState {
// Persistent project identity
projectId: string | null;
projectTitle: string | null;
// Mode and template selection
storyMode: 'marketing' | 'pure';
storyTemplate: 'product_story' | 'brand_manifesto' | 'founder_story' | 'customer_story' | null;
// Story parameters (Setup phase)
persona: string;
storySetting: string;
@@ -48,6 +54,9 @@ export interface StoryWriterState {
audioSlow: boolean;
audioRate: number;
// Anime-specific structured data
animeBible: any | null;
// Generated content
premise: string | null;
outline: string | null;
@@ -55,6 +64,7 @@ export interface StoryWriterState {
isOutlineStructured: boolean;
storyContent: string | null;
isComplete: boolean;
autoGenerateOnWriting: boolean;
sceneImages: Map<number, string> | null; // Generated image URLs by scene number
sceneAudio: Map<number, string> | null; // Generated audio URLs by scene number
storyVideo: string | null; // Generated video URL
@@ -75,6 +85,10 @@ export interface StoryWriterState {
}
const DEFAULT_STATE: Partial<StoryWriterState> = {
projectId: null,
projectTitle: null,
storyMode: 'marketing',
storyTemplate: 'product_story',
persona: '',
storySetting: '',
characters: '',
@@ -109,6 +123,7 @@ const DEFAULT_STATE: Partial<StoryWriterState> = {
isOutlineStructured: false,
storyContent: null,
isComplete: false,
autoGenerateOnWriting: false,
sceneImages: null,
sceneAudio: null,
storyVideo: null,
@@ -122,6 +137,7 @@ const DEFAULT_STATE: Partial<StoryWriterState> = {
generationMessage: null,
isLoading: false,
error: null,
animeBible: null,
};
// Mapping for old values to new values (for migration)
@@ -212,6 +228,7 @@ export const useStoryWriterState = () => {
sceneAnimationResumables: persistableState.sceneAnimationResumables
? Array.from(persistableState.sceneAnimationResumables.entries())
: null,
animeBible: persistableState.animeBible || null,
};
localStorage.setItem('story_writer_state', JSON.stringify(serializableState));
@@ -221,6 +238,20 @@ export const useStoryWriterState = () => {
}, [state]);
// Setters
const setStoryMode = useCallback((storyMode: 'marketing' | 'pure') => {
setState((prev) => ({ ...prev, storyMode }));
}, []);
const setStoryTemplate = useCallback(
(storyTemplate: 'product_story' | 'brand_manifesto' | 'founder_story' | 'customer_story' | null) => {
setState((prev) => ({ ...prev, storyTemplate }));
},
[]);
const setAnimeBible = useCallback((animeBible: any | null) => {
setState((prev) => ({ ...prev, animeBible }));
}, []);
const setPersona = useCallback((persona: string) => {
setState((prev) => ({ ...prev, persona }));
}, []);
@@ -388,6 +419,10 @@ export const useStoryWriterState = () => {
setState((prev) => ({ ...prev, isComplete: complete }));
}, []);
const setAutoGenerateOnWriting = useCallback((autoGenerate: boolean) => {
setState((prev) => ({ ...prev, autoGenerateOnWriting: autoGenerate }));
}, []);
const setCurrentTaskId = useCallback((taskId: string | null) => {
setState((prev) => ({ ...prev, currentTaskId: taskId }));
}, []);
@@ -408,6 +443,195 @@ export const useStoryWriterState = () => {
setState((prev) => ({ ...prev, error }));
}, []);
const setProjectMeta = useCallback((projectId: string | null, title: string | null) => {
setState((prev) => ({ ...prev, projectId, projectTitle: title }));
}, []);
const mapProjectToState = useCallback((project: StoryProjectSummary): StoryWriterState => {
const outlineScenes = project.outline && Array.isArray((project.outline as any).scenes)
? (project.outline as any).scenes
: null;
return {
...(DEFAULT_STATE as StoryWriterState),
projectId: project.project_id,
projectTitle: project.title || null,
storyMode: (project.story_mode as any) || (DEFAULT_STATE.storyMode as 'marketing' | 'pure'),
storyTemplate: (project.story_template as any) || DEFAULT_STATE.storyTemplate || null,
premise: (project.setup as any)?.premise || null,
persona: (project.setup as any)?.persona || '',
storySetting: (project.setup as any)?.story_setting || '',
characters: (project.setup as any)?.character_input || '',
plotElements: (project.setup as any)?.plot_elements || '',
writingStyle: (project.setup as any)?.writing_style || (DEFAULT_STATE.writingStyle as string),
storyTone: (project.setup as any)?.story_tone || (DEFAULT_STATE.storyTone as string),
narrativePOV: (project.setup as any)?.narrative_pov || (DEFAULT_STATE.narrativePOV as string),
audienceAgeGroup:
(project.setup as any)?.audience_age_group || (DEFAULT_STATE.audienceAgeGroup as string),
contentRating:
(project.setup as any)?.content_rating || (DEFAULT_STATE.contentRating as string),
endingPreference:
(project.setup as any)?.ending_preference || (DEFAULT_STATE.endingPreference as string),
storyLength: (project.setup as any)?.story_length || (DEFAULT_STATE.storyLength as string),
enableExplainer:
(project.setup as any)?.enable_explainer ??
(DEFAULT_STATE.enableExplainer as boolean),
enableIllustration:
(project.setup as any)?.enable_illustration ??
(DEFAULT_STATE.enableIllustration as boolean),
enableNarration:
(project.setup as any)?.enable_narration ??
(DEFAULT_STATE.enableNarration as boolean),
enableVideoNarration:
(project.setup as any)?.enable_video_narration ??
(DEFAULT_STATE.enableVideoNarration as boolean),
outline: (project.outline as any)?.outline_text || null,
outlineScenes,
isOutlineStructured: Boolean(outlineScenes && outlineScenes.length > 0),
storyContent: (project.story_content as any)?.story || null,
isComplete: project.is_complete,
animeBible: project.anime_bible || null,
sceneImages: null,
sceneAudio: null,
storyVideo: (project.media_state as any)?.story_video || null,
sceneHdVideos: null,
sceneAnimatedVideos: null,
sceneAnimationResumables: null,
hdVideoGenerationStatus: (project.media_state as any)?.hd_video_status ||
(DEFAULT_STATE.hdVideoGenerationStatus as any),
currentHdSceneIndex:
(project.media_state as any)?.current_hd_scene_index ||
(DEFAULT_STATE.currentHdSceneIndex as number),
currentTaskId: (project.media_state as any)?.current_task_id || null,
generationProgress:
(project.media_state as any)?.generation_progress ||
(DEFAULT_STATE.generationProgress as number),
generationMessage:
(project.media_state as any)?.generation_message ||
(DEFAULT_STATE.generationMessage as string | null),
isLoading: false,
error: null,
autoGenerateOnWriting: DEFAULT_STATE.autoGenerateOnWriting as boolean,
audioProvider: DEFAULT_STATE.audioProvider as string,
audioLang: DEFAULT_STATE.audioLang as string,
audioSlow: DEFAULT_STATE.audioSlow as boolean,
audioRate: DEFAULT_STATE.audioRate as number,
imageProvider: DEFAULT_STATE.imageProvider as string | null,
imageWidth: DEFAULT_STATE.imageWidth as number,
imageHeight: DEFAULT_STATE.imageHeight as number,
imageModel: DEFAULT_STATE.imageModel as string | null,
videoFps: DEFAULT_STATE.videoFps as number,
videoTransitionDuration: DEFAULT_STATE.videoTransitionDuration as number,
};
}, []);
const loadProjectFromDb = useCallback(async (projectId: string) => {
try {
setIsLoading(true);
setError(null);
const project = await storyWriterApi.loadStoryProject(projectId);
setState(() => mapProjectToState(project));
} catch (error) {
console.error('Error loading story project from database:', error);
throw error;
} finally {
setIsLoading(false);
}
}, [mapProjectToState, setIsLoading, setError]);
const saveProjectToDb = useCallback(async () => {
if (!state.projectId) {
return;
}
try {
const payload: UpdateStoryProjectRequest = {
title: state.projectTitle || undefined,
story_mode: state.storyMode,
story_template: state.storyTemplate,
setup: {
premise: state.premise,
persona: state.persona,
story_setting: state.storySetting,
character_input: state.characters,
plot_elements: state.plotElements,
writing_style: state.writingStyle,
story_tone: state.storyTone,
narrative_pov: state.narrativePOV,
audience_age_group: state.audienceAgeGroup,
content_rating: state.contentRating,
ending_preference: state.endingPreference,
story_length: state.storyLength,
enable_explainer: state.enableExplainer,
enable_illustration: state.enableIllustration,
enable_narration: state.enableNarration,
enable_video_narration: state.enableVideoNarration,
},
outline: state.outline
? {
outline_text: state.outline,
scenes: state.outlineScenes || [],
}
: undefined,
scenes: state.outlineScenes || undefined,
story_content: state.storyContent ? { story: state.storyContent } : undefined,
anime_bible: state.animeBible || undefined,
media_state: state.storyVideo
? {
story_video: state.storyVideo,
hd_video_status: state.hdVideoGenerationStatus,
current_hd_scene_index: state.currentHdSceneIndex,
current_task_id: state.currentTaskId,
generation_progress: state.generationProgress,
generation_message: state.generationMessage,
}
: undefined,
is_complete: state.isComplete,
};
await storyWriterApi.updateStoryProject(state.projectId, payload);
} catch (error) {
console.error('Error saving story project to database:', error);
}
}, [state]);
const initializeProject = useCallback(
async (projectId: string, title: string | null, initialSetup?: CreateStoryProjectRequest) => {
try {
const payload: CreateStoryProjectRequest = {
project_id: projectId,
title: title || undefined,
story_mode: state.storyMode,
story_template: state.storyTemplate,
setup: initialSetup?.setup || {
premise: state.premise,
persona: state.persona,
story_setting: state.storySetting,
character_input: state.characters,
plot_elements: state.plotElements,
writing_style: state.writingStyle,
story_tone: state.storyTone,
narrative_pov: state.narrativePOV,
audience_age_group: state.audienceAgeGroup,
content_rating: state.contentRating,
ending_preference: state.endingPreference,
story_length: state.storyLength,
enable_explainer: state.enableExplainer,
enable_illustration: state.enableIllustration,
enable_narration: state.enableNarration,
enable_video_narration: state.enableVideoNarration,
},
};
await storyWriterApi.createStoryProject(payload);
setProjectMeta(projectId, title);
} catch (error) {
console.error('Error creating story project in database:', error);
setProjectMeta(projectId, title);
}
},
[state, setProjectMeta],
);
// Helper to get request object
const getRequest = useCallback((): StoryGenerationRequest => {
return {
@@ -439,30 +663,29 @@ export const useStoryWriterState = () => {
audio_lang: state.audioLang,
audio_slow: state.audioSlow,
audio_rate: state.audioRate,
anime_bible: state.animeBible || null,
};
}, [state]);
// Reset state
const resetState = useCallback(() => {
setState(DEFAULT_STATE as StoryWriterState);
// Clear story writer state from localStorage
localStorage.removeItem('story_writer_state');
// Clear phase navigation from localStorage
try {
if (typeof window !== 'undefined') {
localStorage.removeItem('story_writer_state');
localStorage.removeItem('storywriter_current_phase');
localStorage.removeItem('storywriter_user_selected_phase');
}
} catch (error) {
console.error('Error clearing phase navigation from localStorage:', error);
console.error('Error clearing story studio state from localStorage:', error);
}
}, []);
return {
// State
...state,
// Setters
setStoryMode,
setStoryTemplate,
setAnimeBible,
setPersona,
setStorySetting,
setCharacters,
@@ -508,8 +731,14 @@ export const useStoryWriterState = () => {
setIsLoading,
setError,
setAutoGenerateOnWriting,
// Helpers
getRequest,
resetState,
setProjectMeta,
loadProjectFromDb,
saveProjectToDb,
initializeProject,
};
};