Move podcast cost estimates to backend pricing catalog

This commit is contained in:
ي
2026-04-19 16:23:00 +05:30
parent bcf62017aa
commit e71cf65802
9 changed files with 256 additions and 111 deletions

View File

@@ -253,26 +253,6 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
setShowAIDetailsButton(topicInput.trim().length > 0 && !isUrl);
}, [topicInput, isUrl]);
// Calculate estimated cost
const estimatedCost = useMemo(() => {
const chars = Math.max(1000, duration * 900); // ~900 chars per minute
const secs = duration * 60;
const ttsCost = (chars / 1000) * 0.05;
const avatarCost = speakers * 0.15;
const videoRate = knobs.bitrate === 'hd' ? 0.06 : 0.03;
const videoCost = secs * videoRate;
const researchCost = 0.3; // Fixed research cost
return {
ttsCost: +ttsCost.toFixed(2),
avatarCost: +avatarCost.toFixed(2),
videoCost: +videoCost.toFixed(2),
researchCost: +researchCost.toFixed(2),
total: +(ttsCost + avatarCost + videoCost + researchCost).toFixed(2),
};
}, [duration, speakers, knobs.bitrate, knobs.scene_length_target]);
// Check if avatar is present (from any source: upload, brand avatar, or generated)
const hasAvatar = Boolean(
avatarFile || // User uploaded an image
@@ -560,7 +540,7 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
placeholderIndex={placeholderIndex}
loading={enhancingTopic}
loadingMessage={enhanceTopicMessage}
estimatedCost={estimatedCost}
estimatedCost={null}
duration={duration}
speakers={speakers}
knobs={knobs}

View File

@@ -27,7 +27,7 @@ interface TopicUrlInputProps {
videoCost: number;
researchCost: number;
total: number;
};
} | null;
duration?: number;
speakers?: number;
knobs?: Knobs;
@@ -115,9 +115,9 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
</Typography>
</Stack>
{estimatedCost && (
<Tooltip
title={
<Tooltip
title={
estimatedCost ? (
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Estimated Cost Breakdown:
@@ -135,43 +135,49 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
</Typography>
</Typography>
</Box>
}
arrow
placement="top"
componentsProps={{
tooltip: {
sx: {
bgcolor: "#0f172a",
color: "#ffffff",
maxWidth: 280,
fontSize: "0.875rem",
p: 1.5,
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
},
) : (
"Estimate unavailable until returned by the server."
)
}
arrow
placement="top"
componentsProps={{
tooltip: {
sx: {
bgcolor: "#0f172a",
color: "#ffffff",
maxWidth: 280,
fontSize: "0.875rem",
p: 1.5,
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
},
arrow: {
sx: {
color: "#0f172a",
},
},
arrow: {
sx: {
color: "#0f172a",
},
},
}}
>
<Chip
icon={<AttachMoneyIcon sx={{ fontSize: "0.875rem !important" }} />}
label={estimatedCost ? `Est. $${estimatedCost.total}` : "Est. Unavailable"}
size="small"
sx={{
background: estimatedCost
? "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)"
: "rgba(100, 116, 139, 0.12)",
color: estimatedCost ? "#059669" : "#475569",
fontWeight: 600,
border: estimatedCost
? "1px solid rgba(16, 185, 129, 0.2)"
: "1px solid rgba(100, 116, 139, 0.25)",
fontSize: "0.75rem",
height: 26,
cursor: "help",
}}
>
<Chip
icon={<AttachMoneyIcon sx={{ fontSize: "0.875rem !important" }} />}
label={`Est. $${estimatedCost.total}`}
size="small"
sx={{
background: "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)",
color: "#059669",
fontWeight: 600,
border: "1px solid rgba(16, 185, 129, 0.2)",
fontSize: "0.75rem",
height: 26,
cursor: "help",
}}
/>
</Tooltip>
)}
/>
</Tooltip>
</Stack>
<Tooltip
title={

View File

@@ -354,7 +354,7 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
try {
console.log('[Research] Starting research with:', { topic: project.idea, approvedQueries, provider: researchProvider });
console.log('[Research] Calling podcastApi.runResearch...');
const { research: mapped, raw } = await podcastApi.runResearch({
const { research: mapped, raw, estimate } = await podcastApi.runResearch({
projectId: project.id,
topic: project.idea,
approvedQueries,
@@ -369,6 +369,9 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
console.log('[Research] Response received:', { mapped, raw });
setResearch(mapped);
setRawResearch(raw);
if (estimate) {
setEstimate(estimate);
}
setAnnouncement("Research complete — review fact cards below");
} catch (researchError) {
const errorMessage = researchError instanceof Error
@@ -392,7 +395,7 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
} finally {
setIsResearching(false);
}
}, [isResearching, project, selectedQueries, queries, researchProvider, preflightCheck, analysis, setResearch, setRawResearch, setScriptData, setShowScriptEditor, setShowRenderQueue, projectState.bible]);
}, [isResearching, project, selectedQueries, queries, researchProvider, preflightCheck, analysis, setResearch, setRawResearch, setEstimate, setScriptData, setShowScriptEditor, setShowRenderQueue, projectState.bible]);
// Add a ref to track if we're currently generating to prevent double calls
const isGeneratingRef = useRef(false);
@@ -625,4 +628,3 @@ export const usePodcastWorkflow = ({ projectState, onError }: UsePodcastWorkflow
handleDeleteQuery,
};
};

View File

@@ -186,7 +186,7 @@ export type CreateProjectPayload = {
export type CreateProjectResult = {
projectId: string;
analysis: PodcastAnalysis;
estimate: PodcastEstimate;
estimate: PodcastEstimate | null;
queries: Query[];
bible?: PodcastBible;
avatar_url?: string | null;
@@ -222,4 +222,3 @@ export type TaskStatus = {
created_at?: string;
updated_at?: string;
};