Issue #543 — Validate Estimated Cost Accuracy (UI vs Backend) Backend: - cost_estimator.py uses pricing catalog (APIProviderPricing) as single source of truth - All 7 cost components: analysis, research (search+LLM), script, TTS, voice clone, avatar, video - initialize_default_pricing() runs on every app startup for auto-sync Frontend cost estimation fixes: - Added missing analysisCost, scriptCost, voiceCloneCost to PodcastEstimate type - toPodcastEstimate() now extracts all 7 backend fields (was dropping 3) - headerCostEst maps analysisCost->Analyze, scriptCost->Write, voiceCloneCost->Produce - EstimateCard shows 5 chips: Analysis, Research, Script, Voice(TTS+clone), Visuals(avatar+video) - Chip sum now equals backend total for all configurations Subscription & plan fixes: - Removed Stripe re-verification from checkSubscription() (downgrade regression fix #539) - Added verifyCheckoutRef pattern for reliable mount-time checkout polling - One-time Stripe sync effect with pending_subscription_change flag for Customer Portal returns - Free plan limits: stability_calls 3->10, audio_calls 5->10 (supports 2 podcasts) - Image enforcement uses actual provider (GPT_PROVIDER), not hardcoded Stability - Billing/pricing pages bypass onboarding check in ProtectedRoute - Gradient buttons + loading spinner on plan chip in UserBadge - Added metadata-based Stripe lookup fallback (Issue #538) Documentation: - TESTING_GUIDE.md: comprehensive testing instructions for non-technical testers - Free plan limits, usage tracking, cost estimation formulas - 10 test cases for UI verification - Troubleshooting guide - Quick-reference cost formulas with all default rates Cleanup: removed legacy ToBeMigrated directory (70+ files, ~22K LOC) GSC Brainstorm: service, hook, modal, and UI components for blog topic brainstorming
583 lines
21 KiB
TypeScript
583 lines
21 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
||
import { Stack, Box, Typography, Divider, Chip, alpha, Button, IconButton, Popover, TextField, Tooltip } from "@mui/material";
|
||
import PsychologyIcon from "@mui/icons-material/Psychology";
|
||
import PersonIcon from "@mui/icons-material/Person";
|
||
import EditIcon from "@mui/icons-material/Edit";
|
||
import SaveIcon from "@mui/icons-material/Save";
|
||
import CloseIcon from "@mui/icons-material/Close";
|
||
import InputIcon from "@mui/icons-material/Input";
|
||
import GroupsIcon from "@mui/icons-material/Groups";
|
||
import ListAltIcon from "@mui/icons-material/ListAlt";
|
||
import TipsIcon from "@mui/icons-material/Lightbulb";
|
||
import ArticleIcon from "@mui/icons-material/Article";
|
||
import BibleIcon from "@mui/icons-material/AutoFixHigh";
|
||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||
import { PodcastAnalysis, PodcastEstimate, PodcastBible } from "./types";
|
||
import { GlassyCard, glassyCardSx, SecondaryButton } from "./ui";
|
||
import { aiApiClient } from "../../api/client";
|
||
import { InputsTab, AudienceTab, OutlineTab, EpisodeDetailsTab, TakeawaysTab, GuestTab } from "./AnalysisPanel/tabs";
|
||
|
||
interface AnalysisPanelProps {
|
||
analysis: PodcastAnalysis | null;
|
||
estimate: PodcastEstimate | null;
|
||
idea?: string;
|
||
duration?: number;
|
||
speakers?: number;
|
||
voiceName?: string;
|
||
podcastMode?: "audio_only" | "video_only" | "audio_video";
|
||
avatarUrl?: string | null;
|
||
avatarPrompt?: string | null;
|
||
bible?: PodcastBible | null;
|
||
onRegenerate?: () => void;
|
||
onUpdateAnalysis?: (updatedAnalysis: PodcastAnalysis) => void;
|
||
onUpdateBible?: (updatedBible: PodcastBible) => void;
|
||
}
|
||
|
||
type TabId = 'inputs' | 'audience' | 'outline' | 'details' | 'takeaways' | 'guest';
|
||
|
||
interface TabConfig {
|
||
id: TabId;
|
||
label: string;
|
||
icon: React.ReactNode;
|
||
}
|
||
|
||
const inputStyles = {
|
||
'& .MuiInputBase-input': {
|
||
color: '#111827 !important',
|
||
fontWeight: 500,
|
||
WebkitTextFillColor: '#111827 !important', // Fix for some browsers
|
||
},
|
||
'& .MuiInputLabel-root': {
|
||
color: '#4b5563 !important',
|
||
},
|
||
'& .MuiOutlinedInput-root': {
|
||
bgcolor: '#ffffff !important',
|
||
'& fieldset': {
|
||
borderColor: '#d1d5db !important',
|
||
},
|
||
'&:hover fieldset': {
|
||
borderColor: '#4f46e5 !important',
|
||
},
|
||
'&.Mui-focused fieldset': {
|
||
borderColor: '#4f46e5 !important',
|
||
}
|
||
},
|
||
'& .MuiSelect-select': {
|
||
color: '#111827 !important',
|
||
WebkitTextFillColor: '#111827 !important',
|
||
}
|
||
};
|
||
|
||
export const AnalysisPanel: React.FC<AnalysisPanelProps> = ({
|
||
analysis,
|
||
estimate,
|
||
idea,
|
||
duration,
|
||
speakers,
|
||
voiceName,
|
||
podcastMode,
|
||
avatarUrl,
|
||
avatarPrompt,
|
||
bible,
|
||
onRegenerate,
|
||
onUpdateAnalysis,
|
||
onUpdateBible
|
||
}) => {
|
||
const [activeTab, setActiveTab] = useState<TabId>('inputs');
|
||
const [avatarBlobUrl, setAvatarBlobUrl] = useState<string | null>(null);
|
||
const [avatarLoading, setAvatarLoading] = useState(false);
|
||
const [avatarError, setAvatarError] = useState(false);
|
||
const [bibleAnchorEl, setBibleAnchorEl] = useState<HTMLElement | null>(null);
|
||
|
||
// Edit states
|
||
const [isEditing, setIsEditing] = useState(false);
|
||
const [editedAnalysis, setEditedAnalysis] = useState<PodcastAnalysis | null>(null);
|
||
const [editedBible, setEditedBible] = useState<PodcastBible | null>(null);
|
||
|
||
const tabs: TabConfig[] = [
|
||
{ id: 'inputs', label: 'Your Inputs', icon: <InputIcon /> },
|
||
{ id: 'audience', label: 'Audience & Keywords', icon: <GroupsIcon /> },
|
||
{ id: 'outline', label: 'Outline', icon: <ListAltIcon /> },
|
||
{ id: 'details', label: 'Titles, Hook & CTA', icon: <ArticleIcon /> },
|
||
{ id: 'takeaways', label: 'Takeaways', icon: <TipsIcon /> },
|
||
{ id: 'guest', label: 'Guest Talking Points', icon: <PersonIcon /> },
|
||
];
|
||
|
||
const tabButtonStyles = (isActive: boolean) => ({
|
||
background: isActive
|
||
? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||
: "#f8fafc",
|
||
color: isActive ? "#fff" : "#475569",
|
||
border: isActive
|
||
? "none"
|
||
: "1px solid #e2e8f0",
|
||
borderRadius: 2.5,
|
||
px: 2.5,
|
||
py: 1.25,
|
||
fontSize: "0.8rem",
|
||
fontWeight: 600,
|
||
textTransform: "none" as const,
|
||
transition: "all 0.25s ease",
|
||
boxShadow: isActive
|
||
? "0 4px 12px rgba(102, 126, 234, 0.3)"
|
||
: "0 1px 2px rgba(0, 0, 0, 0.05)",
|
||
"&:hover": {
|
||
background: isActive
|
||
? "linear-gradient(135deg, #764ba2 0%, #667eea 100%)"
|
||
: "#e2e8f0",
|
||
transform: isActive ? "translateY(-1px)" : "none",
|
||
boxShadow: isActive
|
||
? "0 6px 16px rgba(102, 126, 234, 0.35)"
|
||
: "0 2px 4px rgba(0, 0, 0, 0.08)",
|
||
},
|
||
"&:active": {
|
||
transform: "translateY(0)",
|
||
},
|
||
});
|
||
|
||
// Sync editedAnalysis with analysis initially
|
||
useEffect(() => {
|
||
if (analysis && !editedAnalysis) {
|
||
setEditedAnalysis(JSON.parse(JSON.stringify(analysis)));
|
||
}
|
||
}, [analysis, editedAnalysis]);
|
||
|
||
const handleSave = () => {
|
||
if (editedAnalysis && onUpdateAnalysis) {
|
||
onUpdateAnalysis(JSON.parse(JSON.stringify(editedAnalysis)));
|
||
}
|
||
setIsEditing(false);
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
setIsEditing(false);
|
||
setEditedAnalysis(JSON.parse(JSON.stringify(analysis)));
|
||
};
|
||
|
||
const updateExaConfig = (field: string, value: any) => {
|
||
if (!editedAnalysis) return;
|
||
setEditedAnalysis({
|
||
...editedAnalysis,
|
||
exaSuggestedConfig: {
|
||
...(editedAnalysis.exaSuggestedConfig || {}),
|
||
[field]: value
|
||
}
|
||
});
|
||
};
|
||
|
||
const handleAddKeyword = (keyword: string) => {
|
||
if (!editedAnalysis || !keyword.trim()) return;
|
||
if (editedAnalysis.topKeywords.includes(keyword.trim())) return;
|
||
setEditedAnalysis({
|
||
...editedAnalysis,
|
||
topKeywords: [...editedAnalysis.topKeywords, keyword.trim()]
|
||
});
|
||
};
|
||
|
||
const handleRemoveKeyword = (keyword: string) => {
|
||
if (!editedAnalysis) return;
|
||
setEditedAnalysis({
|
||
...editedAnalysis,
|
||
topKeywords: editedAnalysis.topKeywords.filter(k => k !== keyword)
|
||
});
|
||
};
|
||
|
||
const handleAddTitle = (title: string) => {
|
||
if (!editedAnalysis || !title.trim()) return;
|
||
setEditedAnalysis({
|
||
...editedAnalysis,
|
||
titleSuggestions: [...editedAnalysis.titleSuggestions, title.trim()]
|
||
});
|
||
};
|
||
|
||
const handleRemoveTitle = (title: string) => {
|
||
if (!editedAnalysis) return;
|
||
setEditedAnalysis({
|
||
...editedAnalysis,
|
||
titleSuggestions: editedAnalysis.titleSuggestions.filter(t => t !== title)
|
||
});
|
||
};
|
||
|
||
const handleUpdateOutline = (id: string | number, field: 'title' | 'segments', value: any) => {
|
||
if (!editedAnalysis) return;
|
||
setEditedAnalysis({
|
||
...editedAnalysis,
|
||
suggestedOutlines: editedAnalysis.suggestedOutlines.map(o =>
|
||
o.id === id ? { ...o, [field]: value } : o
|
||
)
|
||
});
|
||
};
|
||
|
||
// Load avatar image as blob for authenticated URLs
|
||
useEffect(() => {
|
||
if (!avatarUrl) {
|
||
setAvatarBlobUrl(null);
|
||
setAvatarError(false);
|
||
return;
|
||
}
|
||
|
||
// Check if it's already a blob URL
|
||
if (avatarUrl.startsWith('blob:')) {
|
||
setAvatarBlobUrl(avatarUrl);
|
||
return;
|
||
}
|
||
|
||
// Check if it's an authenticated endpoint
|
||
const isAuthenticatedEndpoint = avatarUrl.includes('/api/podcast/images/') || avatarUrl.includes('/api/podcast/avatar/');
|
||
|
||
let currentBlobUrl: string | null = null;
|
||
|
||
if (isAuthenticatedEndpoint) {
|
||
setAvatarLoading(true);
|
||
setAvatarError(false);
|
||
|
||
const loadAvatarBlob = async () => {
|
||
try {
|
||
const response = await aiApiClient.get(avatarUrl, { responseType: 'blob' });
|
||
const blobUrl = URL.createObjectURL(response.data);
|
||
currentBlobUrl = blobUrl;
|
||
setAvatarBlobUrl(blobUrl);
|
||
setAvatarError(false);
|
||
} catch (error) {
|
||
console.error('[AnalysisPanel] Failed to load avatar as blob:', error);
|
||
// Fallback: try with query token
|
||
try {
|
||
const token = localStorage.getItem('clerk_dashboard_token') || '';
|
||
if (token) {
|
||
const urlWithToken = `${avatarUrl}?token=${encodeURIComponent(token)}`;
|
||
setAvatarBlobUrl(urlWithToken);
|
||
} else {
|
||
setAvatarError(true);
|
||
}
|
||
} catch (fallbackError) {
|
||
console.error('[AnalysisPanel] Fallback avatar loading failed:', fallbackError);
|
||
setAvatarError(true);
|
||
}
|
||
} finally {
|
||
setAvatarLoading(false);
|
||
}
|
||
};
|
||
|
||
loadAvatarBlob();
|
||
|
||
// Cleanup blob URL on unmount or when avatarUrl changes
|
||
return () => {
|
||
if (currentBlobUrl && currentBlobUrl.startsWith('blob:')) {
|
||
URL.revokeObjectURL(currentBlobUrl);
|
||
}
|
||
// Also cleanup any previous blob URL from state
|
||
setAvatarBlobUrl((prev) => {
|
||
if (prev && prev.startsWith('blob:') && prev !== currentBlobUrl) {
|
||
URL.revokeObjectURL(prev);
|
||
}
|
||
return null;
|
||
});
|
||
};
|
||
} else {
|
||
// Direct URL, use as-is
|
||
setAvatarBlobUrl(avatarUrl);
|
||
}
|
||
}, [avatarUrl]);
|
||
|
||
if (!analysis) return null;
|
||
const currentAnalysis = isEditing && editedAnalysis ? editedAnalysis : analysis;
|
||
|
||
return (
|
||
<GlassyCard
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.28 }}
|
||
className="light-theme-container"
|
||
sx={{
|
||
...glassyCardSx,
|
||
background: "#ffffff",
|
||
border: "1px solid rgba(0,0,0,0.06)",
|
||
boxShadow: "0 10px 28px rgba(15,23,42,0.06)",
|
||
color: "#111827",
|
||
}}
|
||
aria-label="analysis-panel"
|
||
>
|
||
<Stack spacing={3}>
|
||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||
<Stack direction="row" alignItems="center" spacing={2} flex={1}>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
color: "#1e293b",
|
||
fontWeight: 800,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 1,
|
||
whiteSpace: "nowrap"
|
||
}}
|
||
>
|
||
<PsychologyIcon sx={{ color: "#4f46e5" }} />
|
||
AI Analysis
|
||
</Typography>
|
||
|
||
{estimate && (
|
||
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ ml: 2, flex: 1, overflow: 'hidden' }}>
|
||
<Divider orientation="vertical" flexItem sx={{ height: 24, alignSelf: 'center', borderColor: "rgba(0,0,0,0.1)" }} />
|
||
<Typography variant="subtitle2" fontWeight={700} sx={{ color: "#4f46e5" }}>
|
||
Est. Cost: ${estimate.total.toFixed(2)}
|
||
</Typography>
|
||
<Stack direction="row" spacing={1} sx={{ display: { xs: 'none', lg: 'flex' } }}>
|
||
<Chip
|
||
label={`Analysis: $${estimate.analysisCost.toFixed(2)}`}
|
||
size="small"
|
||
variant="outlined"
|
||
sx={{ height: 20, fontSize: '0.7rem', color: "#64748b", borderColor: "rgba(0,0,0,0.15)", bgcolor: "rgba(0,0,0,0.02)" }}
|
||
/>
|
||
<Chip
|
||
label={`Research: $${estimate.researchCost.toFixed(2)}`}
|
||
size="small"
|
||
variant="outlined"
|
||
sx={{ height: 20, fontSize: '0.7rem', color: "#64748b", borderColor: "rgba(0,0,0,0.15)", bgcolor: "rgba(0,0,0,0.02)" }}
|
||
/>
|
||
<Chip
|
||
label={`Script: $${estimate.scriptCost.toFixed(2)}`}
|
||
size="small"
|
||
variant="outlined"
|
||
sx={{ height: 20, fontSize: '0.7rem', color: "#64748b", borderColor: "rgba(0,0,0,0.15)", bgcolor: "rgba(0,0,0,0.02)" }}
|
||
/>
|
||
<Chip
|
||
label={`Voice: $${(estimate.ttsCost + estimate.voiceCloneCost).toFixed(2)}`}
|
||
size="small"
|
||
variant="outlined"
|
||
title={`Voice narration ($${estimate.ttsCost.toFixed(2)}) + cloning ($${estimate.voiceCloneCost.toFixed(2)})`}
|
||
sx={{ height: 20, fontSize: '0.7rem', color: "#64748b", borderColor: "rgba(0,0,0,0.15)", bgcolor: "rgba(0,0,0,0.02)" }}
|
||
/>
|
||
<Chip
|
||
label={`Visuals: $${(estimate.avatarCost + estimate.videoCost).toFixed(2)}`}
|
||
size="small"
|
||
variant="outlined"
|
||
sx={{ height: 20, fontSize: '0.7rem', color: "#64748b", borderColor: "rgba(0,0,0,0.15)", bgcolor: "rgba(0,0,0,0.02)" }}
|
||
/>
|
||
</Stack>
|
||
</Stack>
|
||
)}
|
||
</Stack>
|
||
|
||
<Stack direction="row" spacing={1}>
|
||
{/* Bible Button */}
|
||
{bible && (
|
||
<Tooltip title="Podcast Bible - Hyper-personalized context">
|
||
<IconButton
|
||
onClick={(e) => setBibleAnchorEl(e.currentTarget)}
|
||
sx={{
|
||
bgcolor: bibleAnchorEl ? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)" : "rgba(102, 126, 234, 0.1)",
|
||
border: "1px solid",
|
||
borderColor: bibleAnchorEl ? "transparent" : "rgba(102, 126, 234, 0.3)",
|
||
borderRadius: 2,
|
||
p: 1,
|
||
transition: "all 0.2s ease",
|
||
"&:hover": {
|
||
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||
borderColor: "transparent",
|
||
},
|
||
}}
|
||
>
|
||
<BibleIcon sx={{ color: bibleAnchorEl ? "#fff" : "#667eea", fontSize: 20 }} />
|
||
</IconButton>
|
||
</Tooltip>
|
||
)}
|
||
|
||
{isEditing ? (
|
||
<>
|
||
<SecondaryButton
|
||
onClick={handleSave}
|
||
startIcon={<SaveIcon />}
|
||
sx={{
|
||
color: '#059669',
|
||
borderColor: '#10b981',
|
||
bgcolor: 'white',
|
||
fontWeight: 600,
|
||
'&:hover': { bgcolor: alpha('#10b981', 0.05) }
|
||
}}
|
||
>
|
||
Save Changes
|
||
</SecondaryButton>
|
||
<SecondaryButton
|
||
onClick={handleCancel}
|
||
startIcon={<CloseIcon />}
|
||
sx={{ color: '#4b5563', borderColor: '#d1d5db', bgcolor: 'white' }}
|
||
>
|
||
Cancel
|
||
</SecondaryButton>
|
||
</>
|
||
) : (
|
||
<>
|
||
<SecondaryButton
|
||
onClick={() => setIsEditing(true)}
|
||
startIcon={<EditIcon />}
|
||
sx={{ color: '#4f46e5', borderColor: '#4f46e5', bgcolor: 'white', fontWeight: 600 }}
|
||
>
|
||
Edit Analysis
|
||
</SecondaryButton>
|
||
<SecondaryButton
|
||
onClick={onRegenerate}
|
||
startIcon={<RefreshIcon />}
|
||
tooltip="Regenerate analysis with different parameters"
|
||
sx={{ color: '#4b5563', borderColor: '#d1d5db', bgcolor: 'white' }}
|
||
>
|
||
Regenerate
|
||
</SecondaryButton>
|
||
</>
|
||
)}
|
||
</Stack>
|
||
</Stack>
|
||
|
||
<Divider sx={{ borderColor: "rgba(0,0,0,0.06)" }} />
|
||
|
||
{/* AI Futuristic Tab Navigation */}
|
||
<Stack direction="row" flexWrap="wrap" gap={1} sx={{ mb: 2 }}>
|
||
{tabs.map((tab) => (
|
||
<Button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id)}
|
||
startIcon={tab.icon}
|
||
sx={tabButtonStyles(activeTab === tab.id)}
|
||
>
|
||
{tab.label}
|
||
</Button>
|
||
))}
|
||
</Stack>
|
||
|
||
{/* Tab Content */}
|
||
<Box sx={{ minHeight: 300 }}>
|
||
{activeTab === 'inputs' && (
|
||
<InputsTab
|
||
idea={idea}
|
||
duration={duration}
|
||
speakers={speakers}
|
||
voiceName={voiceName}
|
||
podcastMode={podcastMode}
|
||
avatarUrl={avatarUrl}
|
||
avatarPrompt={avatarPrompt}
|
||
avatarBlobUrl={avatarBlobUrl}
|
||
avatarLoading={avatarLoading}
|
||
avatarError={avatarError}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'audience' && (
|
||
<AudienceTab
|
||
analysis={currentAnalysis}
|
||
isEditing={isEditing}
|
||
editedAnalysis={editedAnalysis}
|
||
setEditedAnalysis={setEditedAnalysis}
|
||
handleRemoveKeyword={handleRemoveKeyword}
|
||
handleAddKeyword={handleAddKeyword}
|
||
handleRemoveTitle={handleRemoveTitle}
|
||
handleAddTitle={handleAddTitle}
|
||
handleUpdateOutline={handleUpdateOutline}
|
||
updateExaConfig={updateExaConfig}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'outline' && (
|
||
<OutlineTab
|
||
analysis={currentAnalysis}
|
||
isEditing={isEditing}
|
||
onUpdateOutline={handleUpdateOutline}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'details' && (
|
||
<EpisodeDetailsTab
|
||
analysis={currentAnalysis}
|
||
isEditing={isEditing}
|
||
handleRemoveTitle={handleRemoveTitle}
|
||
handleAddTitle={handleAddTitle}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'takeaways' && (
|
||
<TakeawaysTab analysis={currentAnalysis} />
|
||
)}
|
||
|
||
{activeTab === 'guest' && (
|
||
<GuestTab analysis={currentAnalysis} />
|
||
)}
|
||
</Box>
|
||
|
||
{/* Bible Popover */}
|
||
<Popover
|
||
open={Boolean(bibleAnchorEl)}
|
||
anchorEl={bibleAnchorEl}
|
||
onClose={() => setBibleAnchorEl(null)}
|
||
anchorOrigin={{
|
||
vertical: 'bottom',
|
||
horizontal: 'right',
|
||
}}
|
||
transformOrigin={{
|
||
vertical: 'top',
|
||
horizontal: 'right',
|
||
}}
|
||
PaperProps={{
|
||
sx: {
|
||
mt: 1,
|
||
maxWidth: 420,
|
||
borderRadius: 3,
|
||
background: "linear-gradient(135deg, #1e293b 0%, #0f172a 100%)",
|
||
border: "1px solid rgba(102, 126, 234, 0.3)",
|
||
boxShadow: "0 10px 40px rgba(102, 126, 234, 0.25)",
|
||
},
|
||
}}
|
||
>
|
||
<Box sx={{ p: 2.5 }}>
|
||
<Stack spacing={2}>
|
||
<Stack direction="row" alignItems="center" spacing={1}>
|
||
<BibleIcon sx={{ color: "#a78bfa", fontSize: 24 }} />
|
||
<Typography variant="h6" sx={{ color: "#fff", fontWeight: 700 }}>
|
||
Podcast Bible
|
||
</Typography>
|
||
<Tooltip title="Hyper-personalized context derived from your onboarding data. This grounds all research and script generation.">
|
||
<IconButton size="small" sx={{ ml: 'auto' }}>
|
||
<Typography variant="caption" sx={{ color: "#94a3b8" }}>ℹ️</Typography>
|
||
</IconButton>
|
||
</Tooltip>
|
||
</Stack>
|
||
|
||
{/* Host Persona */}
|
||
<Box sx={{ p: 1.5, borderRadius: 2, bgcolor: "rgba(99, 102, 241, 0.1)", border: "1px solid rgba(99, 102, 241, 0.2)" }}>
|
||
<Typography variant="caption" sx={{ color: "#a78bfa", fontWeight: 600, mb: 0.5, display: "block" }}>
|
||
Host Persona
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ color: "rgba(255,255,255,0.8)", fontSize: "0.8rem" }}>
|
||
{bible?.host?.name || "Not set"} • {bible?.host?.background || "No background"} • {bible?.host?.vocal_style || "No style"}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* Audience DNA */}
|
||
<Box sx={{ p: 1.5, borderRadius: 2, bgcolor: "rgba(34, 197, 94, 0.1)", border: "1px solid rgba(34, 197, 94, 0.2)" }}>
|
||
<Typography variant="caption" sx={{ color: "#22c55e", fontWeight: 600, mb: 0.5, display: "block" }}>
|
||
Audience DNA
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ color: "rgba(255,255,255,0.8)", fontSize: "0.8rem" }}>
|
||
{bible?.audience?.expertise_level || "General"} • {(bible?.audience?.interests || []).slice(0, 3).join(", ") || "Various interests"}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* Brand DNA */}
|
||
<Box sx={{ p: 1.5, borderRadius: 2, bgcolor: "rgba(249, 115, 22, 0.1)", border: "1px solid rgba(249, 115, 22, 0.2)" }}>
|
||
<Typography variant="caption" sx={{ color: "#f97316", fontWeight: 600, mb: 0.5, display: "block" }}>
|
||
Brand DNA
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ color: "rgba(255,255,255,0.8)", fontSize: "0.8rem" }}>
|
||
{bible?.brand?.industry || "No industry"} • {bible?.brand?.tone || "No tone"} • {bible?.brand?.communication_style || "No style"}
|
||
</Typography>
|
||
</Box>
|
||
|
||
<Typography variant="caption" sx={{ color: "rgba(255,255,255,0.5)", textAlign: "center", fontSize: "0.7rem" }}>
|
||
Podcast Bible personalizes all AI generation for your unique voice
|
||
</Typography>
|
||
</Stack>
|
||
</Box>
|
||
</Popover>
|
||
</Stack>
|
||
</GlassyCard>
|
||
);
|
||
};
|
||
|