AI Story Writer Backend Migration Complete, Frontend UI Components Added

This commit is contained in:
ajaysi
2025-11-16 19:25:26 +05:30
parent 3b9356e2c8
commit 4901b7eb72
70 changed files with 4765 additions and 1439 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Container, Typography, useTheme } from '@mui/material';
import React, { useState } from 'react';
import { Box, Container, Typography, useTheme, Dialog, DialogTitle, DialogContent, IconButton } from '@mui/material';
import { useStoryWriterState } from '../../hooks/useStoryWriterState';
import { useStoryWriterPhaseNavigation } from '../../hooks/useStoryWriterPhaseNavigation';
import StorySetup from './Phases/StorySetup';
@@ -7,6 +7,12 @@ import StoryOutline from './Phases/StoryOutline';
import StoryWriting from './Phases/StoryWriting';
import StoryExport from './Phases/StoryExport';
import PhaseNavigation from './PhaseNavigation';
import { MultimediaToolbar } from './components/MultimediaToolbar';
import { storyWriterApi } from '../../services/storyWriterApi';
import { triggerSubscriptionError } from '../../api/client';
import CloseIcon from '@mui/icons-material/Close';
import { MultimediaSection } from './components/MultimediaSection';
import StoryWriterLanding from './StoryWriterLanding';
export const StoryWriter: React.FC = () => {
const theme = useTheme();
@@ -14,6 +20,15 @@ export const StoryWriter: React.FC = () => {
// State management
const state = useStoryWriterState();
// Multimedia generation state
const [isGeneratingAudio, setIsGeneratingAudio] = useState(false);
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
const [isMultimediaDialogOpen, setIsMultimediaDialogOpen] = useState(false);
const [landingDismissed, setLandingDismissed] = useState(() => {
if (typeof window === 'undefined') return false;
return window.localStorage.getItem('storywriter:landingDismissed') === 'true';
});
// Phase navigation
const {
phases,
@@ -30,12 +45,138 @@ export const StoryWriter: React.FC = () => {
const handleReset = () => {
// Reset story state (this also clears localStorage)
state.resetState();
if (typeof window !== 'undefined') {
window.localStorage.removeItem('storywriter:landingDismissed');
}
// Simplest approach: reload the page to ensure a clean slate
if (typeof window !== 'undefined') {
window.location.reload();
}
};
const handleOpenMultimediaDialog = () => {
setIsMultimediaDialogOpen(true);
};
const handleCloseMultimediaDialog = () => {
setIsMultimediaDialogOpen(false);
};
// Audio generation handler
const handleGenerateAudio = async () => {
if (!state.enableNarration) {
return;
}
if (!state.outlineScenes || state.outlineScenes.length === 0) {
return;
}
setIsGeneratingAudio(true);
try {
const response = await storyWriterApi.generateSceneAudio({
scenes: state.outlineScenes,
provider: state.audioProvider,
lang: state.audioLang,
slow: state.audioSlow,
rate: state.audioRate,
});
if (response.success && response.audio_files) {
const audioMap = new Map<number, string>();
response.audio_files.forEach((audio) => {
if (audio.audio_url && !audio.error) {
audioMap.set(audio.scene_number, audio.audio_url);
}
});
state.setSceneAudio(audioMap);
state.setError(null);
}
} catch (err: any) {
const status = err?.response?.status;
if (status === 429 || status === 402) {
await triggerSubscriptionError(err);
}
console.error('Audio generation failed:', err);
} finally {
setIsGeneratingAudio(false);
}
};
// Video generation handler
const handleGenerateVideo = async () => {
if (!state.enableVideoNarration) {
return;
}
if (!state.outlineScenes || state.outlineScenes.length === 0) {
return;
}
if (!state.sceneImages || state.sceneImages.size === 0) {
return;
}
if (!state.sceneAudio || state.sceneAudio.size === 0) {
return;
}
setIsGeneratingVideo(true);
try {
const imageUrls: string[] = [];
const audioUrls: string[] = [];
const scenes = state.outlineScenes;
for (const scene of scenes) {
const sceneNumber = scene.scene_number || scenes.indexOf(scene) + 1;
const imageUrl = state.sceneImages?.get(sceneNumber);
const audioUrl = state.sceneAudio?.get(sceneNumber);
if (imageUrl && audioUrl) {
imageUrls.push(imageUrl);
audioUrls.push(audioUrl);
}
}
if (imageUrls.length !== scenes.length || audioUrls.length !== scenes.length) {
throw new Error('Number of images and audio files must match number of scenes');
}
const response = await storyWriterApi.generateStoryVideo({
scenes: scenes,
image_urls: imageUrls,
audio_urls: audioUrls,
story_title: state.storySetting || 'Story',
fps: state.videoFps,
transition_duration: state.videoTransitionDuration,
});
if (response.success && response.video) {
state.setStoryVideo(response.video.video_url);
state.setError(null);
}
} catch (err: any) {
const status = err?.response?.status;
if (status === 429 || status === 402) {
await triggerSubscriptionError(err);
}
console.error('Video generation failed:', err);
} finally {
setIsGeneratingVideo(false);
}
};
const hasStoryProgress = Boolean(state.premise || state.outline || state.storyContent);
const showLanding = !landingDismissed && !hasStoryProgress;
const handleLandingStart = () => {
setLandingDismissed(true);
if (typeof window !== 'undefined') {
window.localStorage.setItem('storywriter:landingDismissed', 'true');
}
navigateToPhase('setup');
};
// Render phase content
const renderPhaseContent = () => {
switch (currentPhase) {
@@ -52,6 +193,22 @@ export const StoryWriter: React.FC = () => {
}
};
if (showLanding) {
return (
<Box
sx={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
padding: theme.spacing(4),
}}
>
<Container maxWidth="xl">
<StoryWriterLanding onStart={handleLandingStart} />
</Container>
</Box>
);
}
return (
<Box
sx={{
@@ -90,29 +247,62 @@ export const StoryWriter: React.FC = () => {
zIndex: 1,
}}
>
{/* Header */}
{/* Header with Phase Navigation and Multimedia Toolbar */}
<Box sx={{ mb: 4 }}>
<Typography variant="h3" component="h1" gutterBottom>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 2 }}>
<Box>
<Typography variant="h3" component="h1" gutterBottom sx={{ color: 'white' }}>
Story Writer
</Typography>
<Typography variant="body1" color="text.secondary">
<Typography variant="body1" sx={{ color: 'rgba(255, 255, 255, 0.8)' }}>
Create compelling stories with AI assistance
</Typography>
</Box>
{/* Phase Navigation */}
{/* Compact Phase Navigation */}
<Box sx={{ flex: '1 1 auto', minWidth: { xs: '100%', md: '600px' }, display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ flex: 1 }}>
<PhaseNavigation
phases={phases}
currentPhase={currentPhase}
onPhaseClick={navigateToPhase}
onReset={handleReset}
/>
</Box>
{/* Multimedia Toolbar */}
<MultimediaToolbar
state={state}
onGenerateAudio={handleGenerateAudio}
onGenerateVideo={handleGenerateVideo}
isGeneratingAudio={isGeneratingAudio}
isGeneratingVideo={isGeneratingVideo}
onOpenPanel={(_section) => handleOpenMultimediaDialog()}
/>
</Box>
</Box>
</Box>
{/* Phase Content */}
<Box sx={{ mt: 4 }}>
{renderPhaseContent()}
</Box>
</Container>
<Dialog
open={isMultimediaDialogOpen}
onClose={handleCloseMultimediaDialog}
maxWidth="md"
fullWidth
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
Multimedia Controls
<IconButton size="small" onClick={handleCloseMultimediaDialog}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers>
<MultimediaSection state={state} />
</DialogContent>
</Dialog>
</Box>
);
};