"feat:enhance-podcast-topic-ai"

This commit is contained in:
ajaysi
2026-03-11 19:09:27 +05:30
parent e472861967
commit 01881bb405
51 changed files with 3627 additions and 218 deletions

View File

@@ -3,7 +3,7 @@ import { Stack, Paper, Box } from "@mui/material";
import { CreateProjectPayload, Knobs } from "./types";
import { useSubscription } from "../../contexts/SubscriptionContext";
import { podcastApi } from "../../services/podcastApi";
import { fetchMediaBlobUrl } from "../../utils/fetchMediaBlobUrl";
import { fetchMediaBlobUrl, clearMediaCache } from "../../utils/fetchMediaBlobUrl";
import { getLatestBrandAvatar } from "../../api/brandAssets";
// Imported Components
@@ -12,6 +12,13 @@ import { TopicUrlInput, TOPIC_PLACEHOLDERS } from "./CreateStep/TopicUrlInput";
import { PodcastConfiguration } from "./CreateStep/PodcastConfiguration";
import { AvatarSelector } from "./CreateStep/AvatarSelector";
import { CreateActions } from "./CreateStep/CreateActions";
import { EnhancedTopicChoicesModal } from "./EnhancedTopicChoicesModal";
const ENHANCE_TOPIC_PROGRESS_MESSAGES = [
"Analyzing your topic idea...",
"Enhancing clarity and hook...",
"Aligning language for podcast listeners...",
];
interface CreateModalProps {
onCreate: (payload: CreateProjectPayload) => void;
@@ -33,11 +40,20 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [avatarPreviewBlobUrl, setAvatarPreviewBlobUrl] = useState<string | null>(null);
const [makingPresentable, setMakingPresentable] = useState(false);
const [enhancingTopic, setEnhancingTopic] = useState(false);
const [enhanceTopicProgressIndex, setEnhanceTopicProgressIndex] = useState(0);
const [knobs, setKnobs] = useState<Knobs>({ ...defaultKnobs });
const [placeholderIndex, setPlaceholderIndex] = useState(0);
const [avatarTab, setAvatarTab] = useState(0);
const [loadingBrandAvatar, setLoadingBrandAvatar] = useState(false);
const [brandAvatarFromDb, setBrandAvatarFromDb] = useState<string | null>(null);
const [cameraSelfieOpen, setCameraSelfieOpen] = useState(false);
// Enhanced topic choices state
const [enhancedChoices, setEnhancedChoices] = useState<string[]>([]);
const [enhancedRationales, setEnhancedRationales] = useState<string[]>([]);
const [choicesModalOpen, setChoicesModalOpen] = useState(false);
const [editedChoices, setEditedChoices] = useState<string[]>([]);
// Rotate placeholder every 3 seconds
useEffect(() => {
@@ -140,6 +156,11 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
let isMounted = true;
const loadBrandBlob = async () => {
try {
// Clear cache for this URL to ensure fresh data
if (brandAvatarFromDb) {
clearMediaCache(brandAvatarFromDb);
}
const blobUrl = await fetchMediaBlobUrl(brandAvatarFromDb);
if (isMounted) setBrandAvatarBlobUrl(blobUrl);
} catch (err) {
@@ -172,29 +193,57 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
};
const isUrl = useMemo(() => detectUrl(topicInput), [topicInput]);
const enhanceTopicMessage = enhancingTopic ? ENHANCE_TOPIC_PROGRESS_MESSAGES[enhanceTopicProgressIndex] : undefined;
useEffect(() => {
if (!enhancingTopic) {
setEnhanceTopicProgressIndex(0);
return;
}
const interval = setInterval(() => {
setEnhanceTopicProgressIndex((prev) => (prev + 1) % ENHANCE_TOPIC_PROGRESS_MESSAGES.length);
}, 1200);
return () => clearInterval(interval);
}, [enhancingTopic]);
// Handle AI Details button click
const handleAIDetailsClick = async () => {
if (!topicInput.trim() || makingPresentable) return;
if (!topicInput.trim() || enhancingTopic) return;
try {
setMakingPresentable(true);
setEnhancingTopic(true);
// We pass the current Bible context if we have it (unlikely here as it's generated in analysis)
// But the backend will generate it from onboarding data if missing
const result = await podcastApi.enhanceIdea({
idea: topicInput,
});
if (result.enhanced_idea) {
setTopicInput(result.enhanced_idea);
if (result.enhanced_ideas && result.enhanced_ideas.length === 3) {
setEnhancedChoices(result.enhanced_ideas);
setEnhancedRationales(result.rationales || []);
setEditedChoices(result.enhanced_ideas); // Initialize editable versions
setChoicesModalOpen(true);
}
} catch (error) {
console.error("Failed to enhance idea with AI:", error);
} finally {
setMakingPresentable(false);
setEnhancingTopic(false);
}
};
// Handle enhanced topic choice selection
const handleChoiceSelection = (selectedIndex: number, editedChoice: string) => {
const selectedTopic = editedChoice;
setTopicInput(selectedTopic);
setChoicesModalOpen(false);
// Reset choices state
setEnhancedChoices([]);
setEnhancedRationales([]);
setEditedChoices([]);
};
// Show AI details button when user starts typing (and it's not a URL)
useEffect(() => {
setShowAIDetailsButton(topicInput.trim().length > 0 && !isUrl);
@@ -203,7 +252,6 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
// Calculate estimated cost
const estimatedCost = useMemo(() => {
const chars = Math.max(1000, duration * 900); // ~900 chars per minute
const scenes = Math.ceil((duration * 60) / (knobs.scene_length_target || 45));
const secs = duration * 60;
const ttsCost = (chars / 1000) * 0.05;
@@ -282,6 +330,8 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
setAvatarPreview(null);
setAvatarUrl(null);
setMakingPresentable(false);
setEnhancingTopic(false);
setEnhanceTopicProgressIndex(0);
setKnobs({ ...defaultKnobs });
setPlaceholderIndex(0);
};
@@ -325,6 +375,34 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
}
};
const handleCameraSelfie = async (imageDataUrl: string) => {
try {
// Convert dataURL to File object
const response = await fetch(imageDataUrl);
const blob = await response.blob();
const file = new File([blob], 'selfie.jpg', { type: 'image/jpeg' });
// Set the file and preview
setAvatarFile(file);
setAvatarPreview(imageDataUrl);
// Upload image immediately to get URL (for "Make Presentable" feature)
try {
const { podcastApi } = await import("../../services/podcastApi");
const uploadResult = await podcastApi.uploadAvatar(file);
setAvatarUrl(uploadResult.avatar_url);
} catch (error) {
console.error('Avatar upload failed:', error);
// Continue with local preview - upload will happen on submit
}
// Close camera dialog
setCameraSelfieOpen(false);
} catch (error) {
console.error('Failed to process selfie:', error);
}
};
const handleRemoveAvatar = () => {
setAvatarFile(null);
setAvatarPreview(null);
@@ -442,7 +520,8 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
showAIDetailsButton={showAIDetailsButton}
onAIDetailsClick={handleAIDetailsClick}
placeholderIndex={placeholderIndex}
loading={makingPresentable}
loading={enhancingTopic}
loadingMessage={enhanceTopicMessage}
/>
</Box>
@@ -466,12 +545,15 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
handleUseBrandAvatar={handleUseBrandAvatar}
handleAvatarSelectFromLibrary={handleAvatarSelectFromLibrary}
handleAvatarChange={handleAvatarChange}
handleCameraSelfie={handleCameraSelfie}
handleRemoveAvatar={handleRemoveAvatar}
handleMakePresentable={handleMakePresentable}
makingPresentable={makingPresentable}
avatarPreviewBlobUrl={avatarPreviewBlobUrl}
brandAvatarFromDb={brandAvatarFromDb}
brandAvatarBlobUrl={brandAvatarBlobUrl}
cameraSelfieOpen={cameraSelfieOpen}
setCameraSelfieOpen={setCameraSelfieOpen}
/>
<CreateActions
@@ -480,6 +562,16 @@ export const CreateModal: React.FC<CreateModalProps> = ({ onCreate, open, defaul
canSubmit={canSubmit}
isSubmitting={isSubmitting}
/>
{/* Enhanced Topic Choices Modal */}
<EnhancedTopicChoicesModal
open={choicesModalOpen}
onClose={() => setChoicesModalOpen(false)}
enhancedChoices={enhancedChoices}
enhancedRationales={enhancedRationales}
onSelectChoice={handleChoiceSelection}
loading={enhancingTopic}
/>
</Stack>
</Paper>
);