Release Candidate: Production Release with Multi-Tenant & Onboarding Enhancements
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)" }} />
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user