Production fixes: modal stays open, gradient UI, source links, stepper cleanup
- Fixed progress modals closing prematurely during analysis/research - Enhanced Create Your Voice Clone button with gradient styling - Light gradient themes for Expert Quotes, Listener CTAs, Mapped Angles - Made source reference chips clickable with links in new tab - Removed duplicate stepper (kept in Header only) - Skip api-stats endpoint in podcast-only mode - Combined 3 voice scripts into 1 example - Added force-include step4_assets router in podcast mode
This commit is contained in:
@@ -131,6 +131,12 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
|
||||
};
|
||||
|
||||
const fetchDetailedStats = async () => {
|
||||
// Skip detailed stats in podcast-only mode (endpoint not available)
|
||||
if (isPodcastOnlyDemoMode()) {
|
||||
setChartData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/api/content-planning/monitoring/api-stats');
|
||||
const result = response?.data;
|
||||
@@ -176,8 +182,10 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus();
|
||||
// Prime cache performance occasionally even when dashboard is closed
|
||||
fetchDetailedStats();
|
||||
// Skip detailed stats in podcast-only mode
|
||||
if (!isPodcastOnlyDemoMode()) {
|
||||
fetchDetailedStats();
|
||||
}
|
||||
|
||||
// Refresh every 120 seconds
|
||||
const interval = setInterval(fetchStatus, 120000);
|
||||
|
||||
@@ -827,19 +827,13 @@ export const VoiceAvatarPlaceholder: React.FC<{ domainName?: string; onVoiceSet?
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: '#4B5563', mb: 1, display: 'block', fontSize: '0.7rem' }}>
|
||||
Read one of these scripts to capture your voice:
|
||||
Read this script to capture your voice:
|
||||
</Typography>
|
||||
<Stack spacing={1}>
|
||||
{[
|
||||
"Hi, I'm excited to use AI to scale my content creation. This voice clone will help me stay consistent across all my channels.",
|
||||
"At our company, we value transparency and innovation. We strive to deliver the best solutions for our clients every single day.",
|
||||
"Imagine a world where creativity knows no bounds. Where your ideas can take flight and reach millions of people instantly."
|
||||
].map((text, i) => (
|
||||
<Paper key={i} elevation={0} sx={{ p: 1, bgcolor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { borderColor: '#7C3AED', bgcolor: '#F9FAFB', transform: 'translateY(-1px)' } }}>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', color: '#374151', lineHeight: 1.4, fontStyle: 'italic' }}>"{text}"</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
<Paper elevation={0} sx={{ p: 2, bgcolor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s', '&:hover': { borderColor: '#7C3AED', bgcolor: '#F9FAFB', transform: 'translateY(-1px)' } }}>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.8rem', color: '#374151', lineHeight: 1.5, fontStyle: 'italic' }}>
|
||||
"Hi, I'm excited to use AI to scale my content creation. This voice clone will help me stay consistent across all my channels. At our company, we value transparency and innovation, and we strive to deliver the best solutions for our clients every single day. Imagine a world where creativity knows no bounds, where your ideas can take flight and reach millions of people instantly."
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -392,30 +392,39 @@ export const CreateActions: React.FC<CreateActionsProps> = ({ reset, submit, can
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// Close modal when analysis completes OR when there's an error
|
||||
// Close modal only AFTER analysis fully completes (wait for project/analysis to be set)
|
||||
// Use a ref to track previous isSubmitting to detect the transition from true to false
|
||||
const prevIsSubmittingRef = useRef(isSubmitting);
|
||||
const [analysisCompleteRef, setAnalysisCompleteRef] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Detect transition from submitting to not submitting (analysis complete)
|
||||
// Track if analysis transitioned from true to false (completed)
|
||||
const wasSubmitting = prevIsSubmittingRef.current;
|
||||
const nowNotSubmitting = !isSubmitting;
|
||||
|
||||
if (showAnalysisModal && analysisStarted && wasSubmitting && nowNotSubmitting) {
|
||||
console.warn('[CreateActions] Analysis complete — closing modal and clearing announcement');
|
||||
// Only close modal if:
|
||||
// 1. Modal is still shown
|
||||
// 2. analysisStarted is true
|
||||
// 3. isSubmitting transitioned from true to false
|
||||
// 4. AND we're not showing an error
|
||||
if (showAnalysisModal && analysisStarted && wasSubmitting && nowNotSubmitting && !error) {
|
||||
// Mark analysis as complete and close after a delay
|
||||
setAnalysisCompleteRef(true);
|
||||
console.warn('[CreateActions] Analysis complete — will close modal after delay');
|
||||
setTimeout(() => {
|
||||
setShowAnalysisModal(false);
|
||||
onAnnouncementClear?.();
|
||||
}, 100);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Update ref for next render
|
||||
prevIsSubmittingRef.current = isSubmitting;
|
||||
|
||||
// If there's an error, also ensure modal is usable
|
||||
// If there's an error, keep modal open so user can see error message
|
||||
if (error && showAnalysisModal) {
|
||||
console.warn('[CreateActions] Error detected:', error);
|
||||
console.warn('[CreateActions] Error detected — keeping modal open:', error);
|
||||
}
|
||||
}, [isSubmitting, showAnalysisModal, analysisStarted, onAnnouncementClear, error]);
|
||||
}, [isSubmitting, showAnalysisModal, analysisStarted, onAnnouncementClear, error, analysisCompleteRef]);
|
||||
|
||||
// Sequential progress - increment every few seconds
|
||||
useEffect(() => {
|
||||
@@ -472,7 +481,14 @@ export const CreateActions: React.FC<CreateActionsProps> = ({ reset, submit, can
|
||||
|
||||
<Dialog
|
||||
open={showAnalysisModal}
|
||||
onClose={() => !isSubmitting && setShowAnalysisModal(false)}
|
||||
disableEscapeKeyDown={isSubmitting}
|
||||
onClose={(event, reason) => {
|
||||
// Only allow closing if NOT submitting and analysis hasn't started
|
||||
// This prevents modal from closing when user clicks outside while analysis runs
|
||||
if (!isSubmitting && !analysisStarted) {
|
||||
setShowAnalysisModal(false);
|
||||
}
|
||||
}}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={isMobile}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ProjectList } from "./ProjectList";
|
||||
import { PreflightBlockDialog } from "./PreflightBlockDialog";
|
||||
import {
|
||||
Header,
|
||||
ProgressStepper,
|
||||
EstimateCard,
|
||||
QuerySelection,
|
||||
ResearchSummary,
|
||||
@@ -175,55 +174,7 @@ const PodcastDashboard: React.FC = () => {
|
||||
|
||||
<Divider sx={{ borderColor: "rgba(0,0,0,0.08)" }} />
|
||||
|
||||
{/* Progress Stepper */}
|
||||
{project && workflow.activeStep >= 0 && (
|
||||
<ProgressStepper
|
||||
activeStep={workflow.activeStep}
|
||||
completedSteps={[
|
||||
...(analysis ? [0] : []), // Analysis step
|
||||
...(research ? [1] : []), // Research step
|
||||
...(scriptData ? [2] : []), // Script step
|
||||
...(scriptData && renderJobs.length > 0 ? [3] : []), // Render step (if script exists and has jobs)
|
||||
]}
|
||||
onStepClick={(stepIndex) => {
|
||||
// Navigate to the clicked step
|
||||
// Step indices: 0 = Analysis, 1 = Research, 2 = Script, 3 = Render
|
||||
if (stepIndex === 0) {
|
||||
// Navigate to Analysis
|
||||
setShowScriptEditor(false);
|
||||
setShowRenderQueue(false);
|
||||
setCurrentStep('analysis');
|
||||
} else if (stepIndex === 1) {
|
||||
// Navigate to Research
|
||||
if (!analysis) {
|
||||
workflow.setAnnouncement("Complete Analysis first to access Research.");
|
||||
return;
|
||||
}
|
||||
setShowScriptEditor(false);
|
||||
setShowRenderQueue(false);
|
||||
setCurrentStep('research');
|
||||
} else if (stepIndex === 2) {
|
||||
// Navigate to Script
|
||||
if (!research) {
|
||||
workflow.setAnnouncement("Complete Research first to access Script Editor.");
|
||||
return;
|
||||
}
|
||||
setShowRenderQueue(false);
|
||||
setShowScriptEditor(true);
|
||||
setCurrentStep('script');
|
||||
} else if (stepIndex === 3) {
|
||||
// Navigate to Render
|
||||
if (!scriptData) {
|
||||
workflow.setAnnouncement("Generate and approve script first to access Render Queue.");
|
||||
return;
|
||||
}
|
||||
setShowScriptEditor(false);
|
||||
setShowRenderQueue(true);
|
||||
setCurrentStep('render');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Progress stepper is in Header - keeping UI clean */}
|
||||
|
||||
{/* Resume Alert */}
|
||||
{workflow.showResumeAlert && project && (
|
||||
|
||||
@@ -424,7 +424,13 @@ export const QuerySelection: React.FC<QuerySelectionProps> = ({
|
||||
{/* Research Progress Modal */}
|
||||
<Dialog
|
||||
open={showResearchModal}
|
||||
onClose={() => !isResearching && setShowResearchModal(false)}
|
||||
disableEscapeKeyDown={isResearching}
|
||||
onClose={(event, reason) => {
|
||||
// Only allow closing if NOT researching and research hasn't started
|
||||
if (!isResearching && !researchStarted) {
|
||||
setShowResearchModal(false);
|
||||
}
|
||||
}}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={isMobile}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useCallback } from "react";
|
||||
import { Stack, Typography, Chip, Divider, Box, alpha, Paper, Stepper, Step, StepLabel, CircularProgress } from "@mui/material";
|
||||
import { Stack, Typography, Chip, Divider, Box, alpha, Paper, CircularProgress } from "@mui/material";
|
||||
import {
|
||||
Insights as InsightsIcon,
|
||||
Search as SearchIcon,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Article as ArticleIcon,
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
ArrowForward as ArrowForwardIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { Research, ResearchInsight } from "../types";
|
||||
import { GlassyCard, glassyCardSx, PrimaryButton } from "../ui";
|
||||
@@ -55,34 +54,6 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
|
||||
return (
|
||||
<GlassyCard sx={glassyCardSx}>
|
||||
<Stack spacing={3}>
|
||||
{/* Step Indicator */}
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<Stepper activeStep={1} alternativeLabel>
|
||||
<Step completed>
|
||||
<StepLabel
|
||||
StepIconComponent={() => <CheckCircleIcon sx={{ color: "#22c55e", fontSize: 24 }} />}
|
||||
>
|
||||
Analysis
|
||||
</StepLabel>
|
||||
</Step>
|
||||
<Step active>
|
||||
<StepLabel>
|
||||
Research
|
||||
</StepLabel>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepLabel>
|
||||
Script
|
||||
</StepLabel>
|
||||
</Step>
|
||||
<Step>
|
||||
<StepLabel>
|
||||
Render
|
||||
</StepLabel>
|
||||
</Step>
|
||||
</Stepper>
|
||||
</Box>
|
||||
|
||||
<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 }}>
|
||||
@@ -285,27 +256,153 @@ export const ResearchSummary: React.FC<ResearchSummaryProps> = ({
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Expert Quotes */}
|
||||
{/* Expert Quotes */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 700 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{
|
||||
px: 1.5, py: 0.5,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%)',
|
||||
color: '#fff',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700
|
||||
}}>
|
||||
NEW
|
||||
</Box>
|
||||
Expert Quotes
|
||||
</Typography>
|
||||
{research.expertQuotes && research.expertQuotes.length > 0 ? (
|
||||
<Stack spacing={1}>
|
||||
{research.expertQuotes.slice(0, 4).map((quote, idx) => (
|
||||
<Paper key={`${quote.source_index}-${idx}`} elevation={0} sx={{ p: 1.5, border: "1px solid rgba(0,0,0,0.06)", borderRadius: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: "#334155", lineHeight: 1.55 }}>
|
||||
“{quote.quote}”
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: "#64748b", display: "block", mt: 0.5 }}>
|
||||
Source S{quote.source_index}
|
||||
<Stack spacing={1.5}>
|
||||
{research.expertQuotes.slice(0, 4).map((quote, idx) => {
|
||||
const sourceUrl = research.sources?.[quote.source_index - 1]?.url;
|
||||
return (
|
||||
<Paper key={`${quote.source_index}-${idx}`} elevation={0} sx={{
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%)',
|
||||
border: '1px solid rgba(139, 92, 246, 0.15)'
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ color: "#1E1B4B", lineHeight: 1.65, fontStyle: 'italic', fontWeight: 500 }}>
|
||||
"{quote.quote}"
|
||||
</Typography>
|
||||
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
{sourceUrl ? (
|
||||
<Chip
|
||||
label={`Source S${quote.source_index}`}
|
||||
size="small"
|
||||
clickable
|
||||
component="a"
|
||||
href={sourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%)',
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #7C3AED 0%, #6D28D9 100%)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Chip
|
||||
label={`Source S${quote.source_index}`}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #8B5CF6 0%, #7C3AED 100%)',
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "#64748b" }}>
|
||||
No expert quotes extracted yet.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Listener CTAs */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{
|
||||
px: 1.5, py: 0.5,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #10B981 0%, #14B8A6 100%)',
|
||||
color: '#fff',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700
|
||||
}}>
|
||||
NEW
|
||||
</Box>
|
||||
Listener CTAs
|
||||
</Typography>
|
||||
{research.listenerCta && research.listenerCta.length > 0 ? (
|
||||
<Stack spacing={1.5}>
|
||||
{research.listenerCta.slice(0, 4).map((cta, idx) => (
|
||||
<Paper key={`cta-${idx}`} elevation={0} sx={{
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #ECFDF5 0%, #D1FAE5 100%)',
|
||||
border: '1px solid rgba(16, 185, 129, 0.15)'
|
||||
}}>
|
||||
<Typography variant="body2" sx={{ color: "#064E3B", lineHeight: 1.65, fontWeight: 500 }}>
|
||||
{cta}
|
||||
</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "#64748b" }}>
|
||||
No expert quotes extracted yet.
|
||||
No listener CTAs suggested yet.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Mapped Angles */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1.5, color: "#0f172a", fontWeight: 700, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{
|
||||
px: 1.5, py: 0.5,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #0EA5E9 0%, #06B6D4 100%)',
|
||||
color: '#fff',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700
|
||||
}}>
|
||||
NEW
|
||||
</Box>
|
||||
Mapped Angles
|
||||
</Typography>
|
||||
{research.mappedAngles && research.mappedAngles.length > 0 ? (
|
||||
<Stack spacing={1.5}>
|
||||
{research.mappedAngles.slice(0, 4).map((angle, idx) => (
|
||||
<Paper key={`angle-${idx}`} elevation={0} sx={{
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
background: 'linear-gradient(135deg, #F0F9FF 0%, #E0F2FE 100%)',
|
||||
border: '1px solid rgba(14, 165, 233, 0.15)'
|
||||
}}>
|
||||
<Typography variant="subtitle2" sx={{ color: "#0C4A6E", fontWeight: 700, mb: 0.5 }}>
|
||||
{angle.title || `Angle ${idx + 1}`}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "#075985", lineHeight: 1.65 }}>
|
||||
{angle.why || "No rationale provided."}
|
||||
</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: "#64748b" }}>
|
||||
No mapped angles available yet.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -47,6 +47,7 @@ export type Research = {
|
||||
summary: string;
|
||||
keyInsights: ResearchInsight[];
|
||||
factCards: Fact[];
|
||||
sources?: { title: string; url: string; excerpt?: string }[];
|
||||
mappedAngles: {
|
||||
title: string;
|
||||
why: string;
|
||||
|
||||
@@ -982,28 +982,32 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
||||
startIcon={showVoiceClonePanel ? <ExpandLess /> : redoingClone ? <RestartAlt /> : <AutoAwesome />}
|
||||
endIcon={showVoiceClonePanel ? <ExpandLess /> : <ExpandMore />}
|
||||
sx={{
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
py: 2,
|
||||
px: 3,
|
||||
width: "100%",
|
||||
background: showVoiceClonePanel
|
||||
? "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)"
|
||||
: "linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%)",
|
||||
? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||||
: "linear-gradient(135deg, #8B5CF6 0%, #EC4899 50%, #F59E0B 100%)",
|
||||
border: showVoiceClonePanel
|
||||
? "1px solid rgba(102, 126, 234, 0.3)"
|
||||
: "1px dashed rgba(102, 126, 234, 0.4)",
|
||||
borderRadius: 2,
|
||||
color: "#667eea",
|
||||
fontWeight: 600,
|
||||
? "1px solid rgba(102, 126, 234, 0.5)"
|
||||
: "none",
|
||||
borderRadius: 2.5,
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
textTransform: "none",
|
||||
fontSize: "0.875rem",
|
||||
fontSize: "0.95rem",
|
||||
boxShadow: showVoiceClonePanel
|
||||
? "0 4px 15px rgba(102, 126, 234, 0.35)"
|
||||
: "0 4px 20px rgba(139, 92, 246, 0.4), 0 0 30px rgba(236, 72, 153, 0.2)",
|
||||
"&:hover": {
|
||||
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%)",
|
||||
borderColor: "#667eea",
|
||||
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.15)",
|
||||
background: "linear-gradient(135deg, #7C3AED 0%, #9333EA 50%, #D97706 100%)",
|
||||
boxShadow: "0 6px 25px rgba(139, 92, 246, 0.5)",
|
||||
transform: "translateY(-1px)",
|
||||
},
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
{redoingClone ? "Redo Voice Clone" : showVoiceClonePanel ? "Hide Voice Cloning" : "Create Your Voice Clone"}
|
||||
{redoingClone ? "Redo Voice Clone" : showVoiceClonePanel ? "Hide Voice Cloning" : "Create Your Voice Clone ✨"}
|
||||
</Button>
|
||||
|
||||
<Collapse in={showVoiceClonePanel}>
|
||||
|
||||
@@ -213,10 +213,17 @@ const mapExaResearchResponse = (response: any): Research => {
|
||||
mappedFactIds: angle.mapped_fact_ids || angle.mappedFactIds || []
|
||||
}));
|
||||
|
||||
const sources = (response.sources || []).map((source: any) => ({
|
||||
title: source.title || "",
|
||||
url: source.url || "",
|
||||
excerpt: source.excerpt || source.highlights?.[0] || ""
|
||||
}));
|
||||
|
||||
return {
|
||||
summary,
|
||||
keyInsights,
|
||||
factCards,
|
||||
sources,
|
||||
mappedAngles,
|
||||
expertQuotes,
|
||||
listenerCta,
|
||||
|
||||
Reference in New Issue
Block a user