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

@@ -1,5 +1,5 @@
import React from "react";
import { Box, Stack, Typography } from "@mui/material";
import { Stack, Typography } from "@mui/material";
import {
Mic as MicIcon,
Info as InfoIcon,
@@ -19,21 +19,19 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
const navigate = useNavigate();
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-start"
flexWrap="wrap"
gap={2}
sx={{ width: "100%", minWidth: 0 }} // Ensure full width and allow wrapping
>
<Box sx={{ minWidth: 0, flex: { xs: "1 1 100%", md: "0 1 auto" } }}>
<Stack sx={{ width: "100%", minWidth: 0 }} spacing={1.5}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
flexWrap="wrap"
gap={2}
>
<Typography
variant="h3"
sx={{
color: "#1e293b",
fontWeight: 800,
mb: 0.5,
display: "flex",
alignItems: "center",
gap: 1.5,
@@ -43,89 +41,86 @@ export const Header: React.FC<HeaderProps> = ({ onShowProjects, onNewEpisode })
<MicIcon fontSize="large" sx={{ color: "#667eea" }} />
AI Podcast Maker
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ display: { xs: "none", sm: "block" } }}>
Create professional podcast episodes with AI-powered research, smart scriptwriting, and natural voice narration
</Typography>
</Box>
<Stack
direction="row"
spacing={1}
alignItems="center"
flexWrap="wrap"
useFlexGap
sx={{
justifyContent: { xs: "flex-start", md: "flex-end" },
gap: { xs: 0.5, md: 1 },
minWidth: 0,
width: { xs: "100%", md: "auto" }, // Full width on mobile to allow wrapping
flex: { xs: "1 1 100%", md: "0 1 auto" }, // Take full width on mobile
}}
>
<HeaderControls colorMode="light" showAlerts={true} showUser={true} />
<SecondaryButton
onClick={() => window.open("/docs", "_blank")}
startIcon={<InfoIcon />}
sx={{
display: { xs: "none", lg: "flex" },
// Override for light theme
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
<Stack
direction="row"
spacing={1}
alignItems="center"
flexWrap="wrap"
useFlexGap
sx={{
justifyContent: { xs: "flex-start", md: "flex-end" },
gap: { xs: 0.5, md: 1 },
minWidth: 0,
}}
>
Help
</SecondaryButton>
<SecondaryButton
onClick={() => navigate("/asset-library?source_module=podcast_maker&asset_type=audio")}
startIcon={<LibraryMusicIcon />}
tooltip="View all podcast episodes in Asset Library"
sx={{
display: { xs: "none", xl: "flex" },
// Override for light theme
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
}}
>
My Episodes
</SecondaryButton>
<SecondaryButton
onClick={onShowProjects}
startIcon={<MicIcon />}
tooltip="View and resume saved projects"
sx={{
flexShrink: 0,
display: "flex !important", // Always show "My Projects" - force display
order: { xs: 1, md: 0 }, // Show first on mobile
// Override button colors for light theme
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
}}
>
My Projects
</SecondaryButton>
<PrimaryButton
onClick={onNewEpisode}
startIcon={<AutoAwesomeIcon />}
sx={{
flexShrink: 0,
display: "flex", // Always show "New Episode"
order: { xs: 0, md: 1 }, // Show first on mobile
}}
>
New Episode
</PrimaryButton>
<HeaderControls colorMode="light" showAlerts={true} showUser={true} />
<SecondaryButton
onClick={() => window.open("/docs", "_blank")}
startIcon={<InfoIcon />}
sx={{
display: { xs: "none", lg: "flex" },
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
}}
>
Help
</SecondaryButton>
<SecondaryButton
onClick={() => navigate("/asset-library?source_module=podcast_maker&asset_type=audio")}
startIcon={<LibraryMusicIcon />}
tooltip="View all podcast episodes in Asset Library"
sx={{
display: { xs: "none", xl: "flex" },
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
}}
>
My Episodes
</SecondaryButton>
<SecondaryButton
onClick={onShowProjects}
startIcon={<MicIcon />}
tooltip="View and resume saved projects"
sx={{
flexShrink: 0,
display: "flex !important",
borderColor: "rgba(102, 126, 234, 0.3) !important",
color: "#667eea !important",
"&:hover": {
borderColor: "rgba(102, 126, 234, 0.5) !important",
background: "rgba(102, 126, 234, 0.1) !important",
},
}}
>
My Projects
</SecondaryButton>
<PrimaryButton
onClick={onNewEpisode}
startIcon={<AutoAwesomeIcon />}
sx={{
flexShrink: 0,
display: "flex",
}}
>
New Episode
</PrimaryButton>
</Stack>
</Stack>
<Typography
variant="body2"
color="text.secondary"
sx={{ display: { xs: "none", sm: "block" } }}
>
Create professional podcast episodes with AI-powered research, smart scriptwriting, and natural voice narration
</Typography>
</Stack>
);
};

View File

@@ -0,0 +1,168 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
TextField,
Box,
Stack,
Chip,
alpha,
IconButton
} from '@mui/material';
import {
Psychology as PsychologyIcon,
Close as CloseIcon,
AutoAwesome as AutoAwesomeIcon,
RecordVoiceOver as VoiceIcon,
Groups as AudienceIcon,
FormatListBulleted as OutlineIcon
} from '@mui/icons-material';
interface RegenerationFeedbackModalProps {
open: boolean;
onClose: () => void;
onConfirm: (feedback: string) => void;
isSubmitting?: boolean;
}
const feedbackOptions = [
{ label: 'Audience is wrong', icon: <AudienceIcon fontSize="small" />, text: 'The target audience is not quite right. It should be more focused on...' },
{ label: 'Too generic', icon: <AutoAwesomeIcon fontSize="small" />, text: 'The analysis feels a bit generic. Can we make it more specific to...' },
{ label: 'Outline needs work', icon: <OutlineIcon fontSize="small" />, text: 'The suggested episode outlines don\'t capture the depth I want. Let\'s try...' },
{ label: 'Wrong tone', icon: <VoiceIcon fontSize="small" />, text: 'The content type and tone don\'t match my brand. I want it to be more...' },
];
export const RegenerationFeedbackModal: React.FC<RegenerationFeedbackModalProps> = ({
open,
onClose,
onConfirm,
isSubmitting = false
}) => {
const [feedback, setFeedback] = useState('');
const handleOptionClick = (text: string) => {
setFeedback(prev => prev ? `${prev}\n${text}` : text);
};
const handleSubmit = () => {
onConfirm(feedback.trim());
setFeedback('');
};
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
bgcolor: '#ffffff',
backgroundImage: 'none'
}
}}
>
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Stack direction="row" alignItems="center" spacing={1}>
<PsychologyIcon sx={{ color: '#4f46e5' }} />
<Typography variant="h6" fontWeight={800} sx={{ color: '#1e293b' }}>
Improve AI Analysis
</Typography>
</Stack>
<IconButton onClick={onClose} size="small" sx={{ color: '#64748b' }}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 3, pt: 1 }}>
<Typography variant="body2" sx={{ color: '#475569', mb: 3 }}>
Tell us what you'd like to change or improve about the previous analysis. Your feedback will help the AI generate a more accurate plan for your podcast.
</Typography>
<Stack spacing={3}>
<Box>
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, display: 'block', mb: 1.5 }}>
QUICK SUGGESTIONS
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{feedbackOptions.map((opt) => (
<Chip
key={opt.label}
label={opt.label}
icon={opt.icon}
onClick={() => handleOptionClick(opt.text)}
sx={{
bgcolor: alpha('#4f46e5', 0.05),
color: '#4f46e5',
border: '1px solid',
borderColor: alpha('#4f46e5', 0.2),
fontWeight: 600,
'&:hover': {
bgcolor: alpha('#4f46e5', 0.1),
borderColor: '#4f46e5',
}
}}
/>
))}
</Stack>
</Box>
<TextField
fullWidth
multiline
rows={4}
placeholder="e.g. Focus more on technical details for developers, or make the tone more humorous and conversational..."
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
variant="outlined"
autoFocus
sx={{
'& .MuiOutlinedInput-root': {
bgcolor: '#f8fafc',
borderRadius: 2,
'& fieldset': { borderColor: '#e2e8f0' },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#4f46e5' },
},
'& .MuiInputBase-input': {
color: '#1e293b',
fontSize: '0.95rem'
}
}}
/>
</Stack>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button
onClick={onClose}
sx={{ color: '#64748b', textTransform: 'none', fontWeight: 600 }}
>
Cancel
</Button>
<Button
variant="contained"
onClick={handleSubmit}
disabled={!feedback.trim() || isSubmitting}
sx={{
bgcolor: '#4f46e5',
color: 'white',
px: 4,
borderRadius: 2,
textTransform: 'none',
fontWeight: 700,
'&:hover': { bgcolor: '#4338ca' },
'&.Mui-disabled': { bgcolor: '#e2e8f0', color: '#94a3b8' }
}}
>
{isSubmitting ? 'Regenerating...' : 'Regenerate Analysis'}
</Button>
</DialogActions>
</Dialog>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from "react";
import React, { useMemo, useCallback } from "react";
import { Stack, Typography, Chip, Divider, Box, alpha, Paper } from "@mui/material";
import {
Insights as InsightsIcon,
@@ -6,8 +6,9 @@ import {
AttachMoney as AttachMoneyIcon,
EditNote as EditNoteIcon,
Article as ArticleIcon,
AutoAwesome as AutoAwesomeIcon,
} from "@mui/icons-material";
import { Research } from "../types";
import { Research, ResearchInsight } from "../types";
import { GlassyCard, glassyCardSx, PrimaryButton } from "../ui";
import { FactCard } from "../FactCard";
@@ -22,75 +23,46 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
canGenerateScript,
onGenerateScript,
}) => {
// Extract key insights from summary if it's long
const summaryParts = useMemo(() => {
const fullSummary = research.summary || "";
if (fullSummary.length > 500) {
// Try to split into paragraphs or sentences
const sentences = fullSummary.split(/[.!?]\s+/).filter(s => s.trim().length > 20);
const keyPoints = sentences.slice(0, 3);
const remainingText = sentences.slice(3).join(". ") + (sentences.length > 3 ? "." : "");
return { keyPoints, remainingText };
}
return { keyPoints: [], remainingText: fullSummary };
}, [research.summary]);
// Simple markdown-to-HTML converter
const renderMarkdown = useCallback((text: string) => {
if (!text) return null;
return text
.split('\n')
.filter(line => line.trim() !== '') // Remove empty lines
.map((line, i) => {
// Handle bold
let processedLine = line.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Handle lists
if (processedLine.trim().startsWith('- ') || processedLine.trim().startsWith('* ')) {
return <li key={i} dangerouslySetInnerHTML={{ __html: processedLine.trim().substring(2) }} style={{ marginBottom: '4px', fontSize: '0.9rem' }} />;
}
// Handle headers - make them smaller
if (processedLine.startsWith('### ')) {
return <Typography key={i} variant="subtitle2" fontWeight={700} sx={{ mt: 1.5, mb: 0.5, color: '#1e293b' }}>{processedLine.substring(4)}</Typography>;
}
if (processedLine.startsWith('## ')) {
return <Typography key={i} variant="subtitle1" fontWeight={700} sx={{ mt: 1.5, mb: 0.5, color: '#0f172a' }}>{processedLine.substring(3)}</Typography>;
}
// Paragraphs - compact spacing
return processedLine.trim() ? <p key={i} dangerouslySetInnerHTML={{ __html: processedLine }} style={{ margin: '4px 0', fontSize: '0.9rem' }} /> : null;
});
}, []);
return (
<GlassyCard sx={glassyCardSx}>
<Stack spacing={3}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" flexWrap="wrap" gap={2}>
<Box sx={{ flex: 1, minWidth: { xs: "100%", md: "60%" } }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1.5 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap" gap={2}>
<Stack direction="row" alignItems="center" spacing={2} sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ display: "flex", alignItems: "center", gap: 1, color: "#0f172a", fontWeight: 700 }}>
<InsightsIcon />
Research Summary
</Typography>
{/* Key Insights */}
{summaryParts.keyPoints.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ mb: 1, color: "#0f172a", fontWeight: 600, display: "flex", alignItems: "center", gap: 0.5 }}>
<ArticleIcon fontSize="small" />
Key Insights
</Typography>
<Stack spacing={1}>
{summaryParts.keyPoints.map((point, idx) => (
<Paper
key={idx}
sx={{
p: 1.25,
background: alpha("#667eea", 0.05),
border: "1px solid rgba(102, 126, 234, 0.15)",
borderRadius: 1.5,
}}
>
<Typography variant="body2" sx={{ color: "#0f172a", lineHeight: 1.6, fontSize: "0.875rem" }}>
{point}
</Typography>
</Paper>
))}
</Stack>
</Box>
)}
{/* Full Summary Text */}
<Typography
variant="body2"
color="text.secondary"
sx={{
mb: 2,
lineHeight: 1.7,
fontSize: "0.875rem",
color: "#475569",
}}
>
{summaryParts.remainingText || research.summary}
</Typography>
{/* Research Metadata */}
<Stack direction="row" spacing={2} flexWrap="wrap" useFlexGap sx={{ mb: 2 }}>
{/* Research Metadata - Moved alongside title */}
<Stack direction="row" spacing={1.5} flexWrap="wrap">
{research.searchQueries && research.searchQueries.length > 0 && (
<Chip
icon={<SearchIcon />}
icon={<SearchIcon sx={{ fontSize: "1rem !important" }} />}
label={`${research.searchQueries.length} search${research.searchQueries.length > 1 ? "es" : ""}`}
size="small"
sx={{
@@ -139,32 +111,8 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
/>
)}
</Stack>
</Stack>
{/* Search Queries Used */}
{research.searchQueries && research.searchQueries.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ mb: 1, color: "#0f172a", fontWeight: 600 }}>
Search Queries Used
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{research.searchQueries.map((query, idx) => (
<Chip
key={idx}
label={query}
size="small"
variant="outlined"
sx={{
borderColor: "rgba(102, 126, 234, 0.3)",
color: "#475569",
background: alpha("#f8fafc", 0.8),
fontSize: "0.8125rem",
}}
/>
))}
</Stack>
</Box>
)}
</Box>
<PrimaryButton
onClick={onGenerateScript}
disabled={!canGenerateScript}
@@ -175,6 +123,153 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
</PrimaryButton>
</Stack>
<Box sx={{ width: "100%" }}>
{/* Main Summary */}
{research.summary && (
<Paper
elevation={0}
sx={{
p: 2.5,
mb: 3,
background: "#f8fafc",
border: "1px solid rgba(0,0,0,0.06)",
borderRadius: 2,
}}
>
<Typography variant="subtitle2" sx={{ mb: 1.5, color: "#64748b", fontWeight: 700, fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em", display: "flex", alignItems: "center", gap: 1 }}>
<AutoAwesomeIcon fontSize="small" sx={{ color: "#667eea", fontSize: "1rem" }} />
Executive Summary
</Typography>
<Box sx={{
lineHeight: 1.6,
fontSize: "0.9rem",
color: "#334155",
"& p": { m: 0, mb: 1 },
"& ul": { m: 0, mb: 1, pl: 2.5 },
"& li": { mb: 0.5 },
"& strong": { color: "#0f172a", fontWeight: 600 }
}}>
{renderMarkdown(research.summary)}
</Box>
</Paper>
)}
{/* Deep Insights */}
{(research.keyInsights && research.keyInsights.length > 0) ? (
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 2, color: "#0f172a", fontWeight: 700, display: "flex", alignItems: "center", gap: 1 }}>
<ArticleIcon sx={{ color: "#667eea" }} />
Deep Insights
</Typography>
<Stack spacing={2.5}>
{research.keyInsights.map((insight: ResearchInsight, idx: number) => (
<Paper
key={idx}
elevation={0}
sx={{
p: 2.5,
background: "#ffffff",
border: "1px solid rgba(0,0,0,0.06)",
boxShadow: "0 2px 12px rgba(0,0,0,0.03)",
borderRadius: 2,
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" sx={{ mb: 1.5 }}>
<Typography variant="subtitle1" sx={{ color: "#0f172a", fontWeight: 700 }}>
{insight.title}
</Typography>
{insight.source_indices && insight.source_indices.length > 0 && (
<Stack direction="row" spacing={0.5}>
{insight.source_indices.map(sIdx => (
<Chip
key={sIdx}
label={`S${sIdx}`}
size="small"
variant="outlined"
sx={{
height: 18,
fontSize: '0.65rem',
fontWeight: 700,
borderColor: alpha("#667eea", 0.3),
color: "#667eea",
bgcolor: alpha("#667eea", 0.05)
}}
/>
))}
</Stack>
)}
</Stack>
<Box sx={{
color: "#475569",
lineHeight: 1.7,
fontSize: "0.9rem",
"& p": { m: 0, mb: 1.5 },
"& ul": { m: 0, mb: 1.5, pl: 2 }
}}>
{renderMarkdown(insight.content)}
</Box>
</Paper>
))}
</Stack>
</Box>
) : (
/* Fallback if keyInsights is missing but we have summary paragraphs */
research.summary && research.summary.length > 500 && !research.keyInsights && (
<Box sx={{ mb: 4 }}>
<Typography variant="h6" sx={{ mb: 2, color: "#0f172a", fontWeight: 700, display: "flex", alignItems: "center", gap: 1 }}>
<ArticleIcon sx={{ color: "#667eea" }} />
Additional Insights
</Typography>
<Paper
elevation={0}
sx={{
p: 2.5,
background: "#ffffff",
border: "1px solid rgba(0,0,0,0.06)",
boxShadow: "0 2px 12px rgba(0,0,0,0.03)",
borderRadius: 2,
}}
>
<Box sx={{
color: "#475569",
lineHeight: 1.7,
fontSize: "0.9rem",
}}>
{/* Render parts of summary that might contain insights if structured data is missing */}
{renderMarkdown(research.summary.split('\n\n').slice(1).join('\n\n'))}
</Box>
</Paper>
</Box>
)
)}
{/* Search Queries Used */}
{research.searchQueries && research.searchQueries.length > 0 && (
<Box sx={{ mt: 4, pt: 3, borderTop: "1px solid rgba(0,0,0,0.04)" }}>
<Typography variant="subtitle2" sx={{ mb: 1.5, color: "#64748b", fontWeight: 700, fontSize: "0.7rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Search Queries Used
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{research.searchQueries.map((query, idx) => (
<Chip
key={idx}
label={query}
size="small"
variant="outlined"
sx={{
borderColor: "rgba(102, 126, 234, 0.15)",
color: "#94a3b8",
background: alpha("#f8fafc", 0.3),
fontSize: "0.7rem",
borderRadius: 1,
}}
/>
))}
</Stack>
</Box>
)}
</Box>
{research.factCards.length > 0 && (
<>
<Divider sx={{ borderColor: "rgba(0,0,0,0.08)" }} />

View File

@@ -3,6 +3,6 @@ export { ProgressStepper } from "./ProgressStepper";
export { EstimateCard } from "./EstimateCard";
export { QuerySelection } from "./QuerySelection";
export { ResearchSummary } from "./ResearchSummary";
export { RegenerationFeedbackModal } from "./RegenerationFeedbackModal";
export { usePodcastWorkflow } from "./usePodcastWorkflow";
export * from "./utils";

View File

@@ -1,9 +1,8 @@
import { useState, useEffect, useMemo, useCallback } from "react";
import { ResearchProvider, ResearchConfig } from "../../../services/blogWriterApi";
import { podcastApi } from "../../../services/podcastApi";
import { usePreflightCheck } from "../../../hooks/usePreflightCheck";
import { useBudgetTracking } from "../../../hooks/useBudgetTracking";
import { CreateProjectPayload, Query, Research, Script, Job } from "../types";
import { CreateProjectPayload, Script } from "../types";
import { usePodcastProjectState } from "../../../hooks/usePodcastProjectState";
import { sanitizeExaConfig, announceError, getStepLabel } from "./utils";
@@ -43,6 +42,7 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
setBudgetCap,
updateRenderJob,
initializeProject,
setBible,
} = projectState;
const [isAnalyzing, setIsAnalyzing] = useState(false);
@@ -75,7 +75,7 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
setShowResumeAlert(true);
setTimeout(() => setShowResumeAlert(false), 5000);
}
}, []); // Only on mount
}, [project, currentStep]);
useEffect(() => {
if (announcement) {
@@ -85,7 +85,7 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
return undefined;
}, [announcement]);
const handleCreate = useCallback(async (payload: CreateProjectPayload) => {
const handleCreate = useCallback(async (payload: CreateProjectPayload, feedback?: string) => {
if (isAnalyzing) return;
setResearch(null);
setRawResearch(null);
@@ -95,8 +95,8 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
try {
setIsAnalyzing(true);
// Upload avatar if provided, or generate presenters
let avatarUrl: string | null = null;
// Use existing avatar URL if provided (e.g. brand avatar), or upload new file
let avatarUrl: string | null = payload.avatarUrl || null;
if (payload.files.avatarFile) {
try {
setAnnouncement("Uploading presenter avatar...");
@@ -108,10 +108,46 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
}
}
setAnnouncement("Analyzing your idea — AI suggestions incoming");
const result = await podcastApi.createProject({ ...payload, avatarUrl });
await initializeProject(payload, result.projectId);
setProject({ id: result.projectId, idea: payload.ideaOrUrl, duration: payload.duration, speakers: payload.speakers, avatarUrl });
// NEW FLOW: Create project first to generate/get the Podcast Bible
// This allows the analysis to be personalized using the Bible context
const projectId = project?.id || `podcast_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
setAnnouncement("Initializing project and brand context...");
const dbProject = project ? null : await initializeProject(payload, projectId, avatarUrl);
const bible = dbProject?.bible || projectState.bible;
setAnnouncement(feedback ? "Regenerating analysis using your feedback..." : "Analyzing your idea — AI suggestions incoming");
const result = await podcastApi.createProject(payload, bible, feedback);
if (result.bible) {
setBible(result.bible);
} else if (dbProject?.bible) {
setBible(dbProject.bible);
}
// Update the project in database with the analysis results
try {
await podcastApi.updateProject(projectId, {
analysis: result.analysis,
estimate: result.estimate,
queries: result.queries,
selected_queries: result.queries.map(q => q.id),
avatar_url: result.avatar_url,
avatar_prompt: result.avatar_prompt,
});
} catch (error) {
console.error('Failed to update project with analysis results:', error);
}
setProject({
id: projectId,
idea: payload.ideaOrUrl,
duration: payload.duration,
speakers: payload.speakers,
avatarUrl: result.avatar_url || avatarUrl,
avatarPrompt: result.avatar_prompt || null,
avatarPersonaId: null,
});
setAnalysis(result.analysis);
setEstimate(result.estimate);
setQueries(result.queries);
@@ -188,7 +224,7 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
} finally {
setIsAnalyzing(false);
}
}, [isAnalyzing, setResearch, setRawResearch, setScriptData, setShowScriptEditor, setShowRenderQueue, initializeProject, setProject, setAnalysis, setEstimate, setQueries, setSelectedQueries, setKnobs, setBudgetCap]);
}, [isAnalyzing, setResearch, setRawResearch, setScriptData, setShowScriptEditor, setShowRenderQueue, initializeProject, setProject, setAnalysis, setEstimate, setQueries, setSelectedQueries, setKnobs, setBudgetCap, setBible]);
const handleRunResearch = useCallback(async () => {
if (isResearching) return;
@@ -230,6 +266,8 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
approvedQueries,
provider: researchProvider,
exaConfig: sanitizeExaConfig(analysis?.exaSuggestedConfig),
bible: projectState.bible,
analysis: analysis,
onProgress: (message) => {
setAnnouncement(message);
},
@@ -258,7 +296,7 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
} finally {
setIsResearching(false);
}
}, [isResearching, project, selectedQueries, queries, researchProvider, preflightCheck, analysis, setResearch, setRawResearch, setScriptData, setShowScriptEditor, setShowRenderQueue]);
}, [isResearching, project, selectedQueries, queries, researchProvider, preflightCheck, analysis, setResearch, setRawResearch, setScriptData, setShowScriptEditor, setShowRenderQueue, projectState.bible]);
const handleGenerateScript = useCallback(async () => {
if (showScriptEditor) return;
@@ -282,7 +320,25 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
setScriptData(null);
setShowRenderQueue(false);
setShowScriptEditor(true);
}, [showScriptEditor, project, research, preflightCheck, setScriptData, setShowRenderQueue, setShowScriptEditor]);
try {
const result = await podcastApi.generateScript({
projectId: project.id,
idea: project.idea,
research: rawResearch,
knobs: projectState.knobs,
speakers: project.speakers,
durationMinutes: project.duration,
bible: projectState.bible,
outline: analysis?.suggestedOutlines?.[0], // Pass the first (possibly refined) outline
analysis: analysis, // Pass full analysis context
});
setScriptData(result);
} catch (error) {
announceError(setAnnouncement, error);
}
}, [showScriptEditor, project, research, preflightCheck, setScriptData, setShowRenderQueue, setShowScriptEditor, rawResearch, projectState.knobs, projectState.bible])
const handleProceedToRendering = useCallback((script: Script) => {
setScriptData(script);
@@ -316,13 +372,30 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
const activeStep = useMemo(() => {
if (showRenderQueue) return 3;
if (showScriptEditor) return 2;
if (research) return 1;
if (analysis) return 0;
if (currentStep === 'research' || research) return 1;
if (currentStep === 'analysis' || analysis) return 0;
return -1;
}, [showRenderQueue, showScriptEditor, research, analysis]);
}, [showRenderQueue, showScriptEditor, currentStep, research, analysis]);
const canGenerateScript = Boolean(project && research && rawResearch);
const handleRegenerate = useCallback(async (feedback?: string) => {
if (!project) return;
// Prepare the payload from existing project state
const payload: CreateProjectPayload = {
ideaOrUrl: project.idea,
duration: project.duration,
speakers: project.speakers,
knobs: projectState.knobs,
budgetCap: projectState.budgetCap,
avatarUrl: project.avatarUrl,
files: {} // No new files for regeneration
};
await handleCreate(payload, feedback);
}, [project, projectState.knobs, projectState.budgetCap, handleCreate]);
return {
// State
isAnalyzing,
@@ -336,6 +409,7 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
canGenerateScript,
// Handlers
handleCreate,
handleRegenerate,
handleRunResearch,
handleGenerateScript,
handleProceedToRendering,