feat(podcast): add pre-estimate endpoint, enhance cost estimator with multi-model support, cleanup alpha pricing seeding

- Add POST /podcast/pre-estimate endpoint for cost estimation before analysis
- Enhance cost_estimator.py with multi-model support (gemini, audio, voice clone, image, video)
- Add detailed cost breakdown (llm, audio, media costs + per-phase breakdown)
- Remove redundant pricing seeding from init_alpha_subscription_tiers.py
- Add SSOT pricing via PricingService.initialize_default_pricing()
- Update TopicUrlInput tooltip to show estimate details
- Add debug logging for pricing seeding and pre-estimate
- Clean up verbose podcast mode debug logs in app.py
This commit is contained in:
ajaysi
2026-05-06 15:29:12 +05:30
parent a7d2ef1c09
commit 3f984e8d0c
31 changed files with 4926 additions and 1011 deletions

View File

@@ -1,7 +1,9 @@
import React from "react";
import { Box, Typography, TextField, Tooltip, Button, CircularProgress, alpha, Stack, Chip } from "@mui/material";
import { AutoAwesome as AutoAwesomeIcon, AttachMoney as AttachMoneyIcon, TrendingUp as TrendingUpIcon } from "@mui/icons-material";
import React, { useState, useCallback, useEffect, useRef } from "react";
import { Box, Typography, TextField, Tooltip, Button, CircularProgress, alpha, Stack, Chip, IconButton, Collapse } from "@mui/material";
import { AutoAwesome as AutoAwesomeIcon, AttachMoney as AttachMoneyIcon, TrendingUp as TrendingUpIcon, Mic as MicIcon, Stop as StopIcon, Language as LanguageIcon, Newspaper as NewspaperIcon, ShowChart as ShowChartIcon, School as SchoolIcon, Public as PublicIcon, Lightbulb as LightbulbIcon } from "@mui/icons-material";
import { Knobs } from "../types";
import { podcastApi } from "../../../services/podcastApi";
import { WebsitePreviewModal } from "./WebsitePreviewModal";
export const TOPIC_PLACEHOLDERS = [
"Industry insights: Latest trends in AI for Content Marketing",
@@ -19,11 +21,14 @@ interface TopicUrlInputProps {
showAIDetailsButton: boolean;
onAIDetailsClick?: () => void;
onTrendingTopicsClick?: () => void;
onCategoryResearchClick?: (category: "news" | "finance" | "research-paper" | "personal-site", websiteUrl?: string) => void;
placeholderIndex: number;
loading?: boolean;
loadingMessage?: string;
trendingLoading?: boolean;
estimatedCost?: {
categoryResearchLoading?: boolean;
// Estimated cost - can be a number (from pre-estimate) or object (from analyze response)
estimatedCost?: number | {
ttsCost: number;
avatarCost: number;
videoCost: number;
@@ -33,6 +38,40 @@ interface TopicUrlInputProps {
duration?: number;
speakers?: number;
knobs?: Knobs;
podcastMode?: string;
// Website extraction data - passed from parent for use with AI enhance
extractedData?: {
title?: string;
text?: string;
summary?: string;
highlights?: string[];
url: string;
image?: string;
favicon?: string;
subpages?: Array<{id?: string; title?: string; url?: string; summary?: string; text?: string}>;
} | null;
setExtractedData?: (data: any) => void;
}
interface SpeechRecognitionType {
lang: string;
continuous: boolean;
interimResults: boolean;
maxAlternatives: number;
onresult: ((event: { results: { isFinal: boolean; [index: number]: { transcript: string } }[], resultIndex: number }) => void) | null;
onerror: ((event: { error: string }) => void) | null;
onend: (() => void) | null;
onstart: (() => void) | null;
start: () => void;
stop: () => void;
abort: () => void;
}
declare global {
interface Window {
SpeechRecognition: new () => SpeechRecognitionType;
webkitSpeechRecognition: new () => SpeechRecognitionType;
}
}
export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
@@ -42,26 +81,161 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
showAIDetailsButton,
onAIDetailsClick,
onTrendingTopicsClick,
onCategoryResearchClick,
placeholderIndex,
loading = false,
loadingMessage,
trendingLoading = false,
categoryResearchLoading = false,
estimatedCost,
duration = 1,
speakers = 1,
knobs,
podcastMode = "audio_video",
extractedData: extractedDataProp,
setExtractedData: setExtractedDataProp,
}) => {
// Helper to get total cost from various estimate formats (number | object | null)
const getTotalCost = (cost: number | { total: number } | null | undefined): number | null => {
if (cost === null || cost === undefined) return null;
if (typeof cost === "number") return cost;
if (typeof cost === "object" && "total" in cost) return cost.total;
return null;
};
const totalCost = getTotalCost(estimatedCost);
const [isListening, setIsListening] = useState(false);
const [error, setError] = useState<string | null>(null);
const recognitionRef = useRef<SpeechRecognitionType | null>(null);
// Use props if provided, otherwise use local state (for backward compatibility)
const [localExtractedData, setLocalExtractedData] = useState<any>(null);
const _extractedData = extractedDataProp !== undefined ? extractedDataProp : localExtractedData;
const _setExtractedData = setExtractedDataProp || setLocalExtractedData;
// Website extraction state
const [showWebsiteInput, setShowWebsiteInput] = useState(false);
const [websiteUrl, setWebsiteUrl] = useState("");
const [isExtracting, setIsExtracting] = useState(false);
const [extractedData, setExtractedData] = useState<{title?: string; text?: string; summary?: string; highlights?: string[]; url: string; image?: string; favicon?: string; subpages?: Array<{id?: string; title?: string; url?: string; summary?: string; text?: string}>} | null>(null);
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [websiteError, setWebsiteError] = useState<string | null>(null);
const isSupported = typeof window !== 'undefined' && (window.SpeechRecognition !== undefined || window.webkitSpeechRecognition !== undefined);
const getBrowserLanguage = (): string => {
const lang = (navigator.language || '').toLowerCase();
if (lang.startsWith('en')) return 'en-US';
if (lang.startsWith('hi')) return 'hi-IN';
if (lang.startsWith('es')) return 'es-ES';
if (lang.startsWith('fr')) return 'fr-FR';
if (lang.startsWith('de')) return 'de-DE';
if (lang.startsWith('zh')) return 'zh-CN';
if (lang.startsWith('ja')) return 'ja-JP';
if (lang.startsWith('ko')) return 'ko-KR';
return 'en-US';
};
const startListening = useCallback(() => {
if (!isSupported) {
setError('Speech recognition is not supported in this browser. Try Chrome or Edge.');
return;
}
setError(null);
const SpeechRecognitionAPI = window.SpeechRecognition || (window as any).webkitSpeechRecognition;
if (!recognitionRef.current) {
const recognition = new SpeechRecognitionAPI() as SpeechRecognitionType;
recognition.lang = getBrowserLanguage();
recognition.continuous = false;
recognition.interimResults = true;
recognition.maxAlternatives = 1;
recognition.onresult = (event) => {
let transcript = '';
let isFinal = false;
for (let i = 0; i < event.results.length; i++) {
transcript += event.results[i][0].transcript;
if (event.results[i].isFinal) {
isFinal = true;
}
}
if (isFinal) {
const newValue = value ? `${value} ${transcript.trim()}`.trim() : transcript.trim();
onChange(newValue);
}
};
recognition.onerror = (event) => {
console.error('[Speech] Error:', event.error);
if (event.error === 'not-allowed') {
setError('Microphone access denied. Please allow microphone access in your browser settings.');
} else if (event.error === 'network') {
setError('Network error. Please check your internet connection.');
} else if (event.error !== 'aborted') {
setError(`Speech recognition error: ${event.error}`);
}
setIsListening(false);
};
recognition.onend = () => {
setIsListening(false);
};
recognitionRef.current = recognition;
}
recognitionRef.current.onstart = () => {
setIsListening(true);
};
try {
recognitionRef.current.start();
} catch (e) {
console.error('[Speech] Start error:', e);
setError('Failed to start speech recognition. Please try again.');
}
}, [isSupported, onChange, value]);
const stopListening = useCallback(() => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
setIsListening(false);
}, []);
const handleMicClick = useCallback(() => {
if (isListening) {
stopListening();
} else {
startListening();
}
}, [isListening, stopListening, startListening]);
useEffect(() => {
return () => {
if (recognitionRef.current) {
recognitionRef.current.abort();
}
};
}, []);
return (
<Box
sx={{
p: 3,
p: 0,
borderRadius: 3,
background: "#ffffff",
border: "1px solid rgba(102, 126, 234, 0.15)",
border: "1px solid",
borderColor: "#e2e8f0",
height: "100%",
display: "flex",
flexDirection: "column",
boxShadow: "0 4px 20px rgba(102, 126, 234, 0.08)",
boxShadow: "0 8px 30px rgba(15, 23, 42, 0.12)",
position: "relative",
overflow: "hidden",
"&::before": {
@@ -75,183 +249,419 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
},
}}
>
<Box flex={1} display="flex" flexDirection="column">
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
<Stack direction="row" alignItems="center" spacing={1}>
{/* Header with gradient background */}
<Box flex={1} display="flex" flexDirection="column" sx={{ background: "linear-gradient(180deg, #eff6ff 0%, #f0f9ff 60%, #ffffff 100%)", px: 3, pt: 3, pb: 2, borderBottom: "1px solid #e0e7ff" }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 1.5 }}>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Box
sx={{
width: 24,
height: 24,
width: 28,
height: 28,
borderRadius: "50%",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.25)",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)",
}}
>
<Typography sx={{ color: "#fff", fontSize: "0.75rem", fontWeight: 700 }}>1</Typography>
</Box>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 1.5,
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%)",
width: 40,
height: 40,
borderRadius: 2,
background: "linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.15)",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.5)",
}}
>
<AutoAwesomeIcon sx={{ color: "#667eea", fontSize: "1rem" }} />
<LightbulbIcon sx={{ color: "#6366f1", fontSize: "1.1rem" }} />
</Box>
<Typography
variant="subtitle1"
sx={{
color: "#0f172a",
fontWeight: 700,
fontSize: "1rem",
letterSpacing: "-0.01em",
}}
>
Enter Podcast Topic or Blog URL
</Typography>
</Stack>
<Tooltip
title={
estimatedCost ? (
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Estimated Cost Breakdown:
</Typography>
<Typography variant="body2" component="div" sx={{ fontSize: "0.875rem", lineHeight: 1.6 }}>
Audio Generation: ${estimatedCost.ttsCost}<br />
Avatar Creation: ${estimatedCost.avatarCost}<br />
Video Rendering: ${estimatedCost.videoCost}<br />
Research: ${estimatedCost.researchCost}<br />
<Typography variant="body2" sx={{ fontWeight: 600, mt: 0.5, pt: 0.5, borderTop: "1px solid rgba(255,255,255,0.2)" }}>
Total: ${estimatedCost.total}
<Stack direction="row" spacing={1} alignItems="center">
{!showWebsiteInput && (
<Chip
icon={<LanguageIcon sx={{ fontSize: "0.875rem !important" }} />}
label="Your Website"
onClick={() => setShowWebsiteInput(true)}
disabled={loading}
size="small"
sx={{
background: "rgba(102, 126, 234, 0.08)",
color: "#667eea",
border: "1px solid rgba(102, 126, 234, 0.25)",
fontWeight: 600,
fontSize: "0.75rem",
height: 26,
"&:hover": {
background: "rgba(102, 126, 234, 0.15)",
transform: "scale(1.02)",
},
}}
/>
)}
<Tooltip
title={
totalCost && estimatedCost ? (
<Box>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
Estimated Cost:
</Typography>
<Typography variant="body2" component="div" sx={{ fontSize: "0.875rem", lineHeight: 1.6 }}>
Total: ${totalCost}
</Typography>
<Typography variant="caption" sx={{ fontSize: "0.75rem", opacity: 0.9, mt: 0.5, display: "block" }}>
Based on {duration} min, {speakers} speaker{speakers > 1 ? "s" : ""}, {knobs?.bitrate === "hd" ? "HD" : "standard"} quality
Based on {duration} min, {speakers} speaker{speakers > 1 ? "s" : ""}, {podcastMode} mode
</Typography>
</Typography>
</Box>
) : (
"Estimate unavailable until returned by the server."
)
</Box>
) : (
"Estimate unavailable. Pricing data not found."
)
}
arrow
placement="top"
>
<Chip
icon={<AttachMoneyIcon sx={{ fontSize: "0.875rem !important" }} />}
label={totalCost ? `Est. $${totalCost}` : "Est. Unavailable"}
size="small"
sx={{
background: totalCost ? "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)" : "rgba(100, 116, 139, 0.12)",
color: totalCost ? "#059669" : "#475569",
fontWeight: 600,
border: totalCost ? "1px solid rgba(16, 185, 129, 0.2)" : "1px solid rgba(100, 116, 139, 0.25)",
fontSize: "0.75rem",
height: 26,
cursor: "help",
}}
/>
</Tooltip>
</Stack>
</Stack>
{/* Website input row - appears when user clicks "Your Website" chip */}
<Collapse in={showWebsiteInput}>
<Box sx={{ mt: 1.5, mb: 1.5, display: "flex", alignItems: "center", gap: 1 }}>
<TextField
fullWidth
size="small"
placeholder="https://yourdomain.com (enter your website home page)"
value={websiteUrl}
onChange={(e) => setWebsiteUrl(e.target.value)}
disabled={isExtracting}
error={!!websiteError}
helperText={websiteError}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#f8fafc",
fontSize: "0.875rem",
"&.Mui-focused": {
backgroundColor: "#ffffff",
},
},
}}
/>
<Button
variant="contained"
size="small"
onClick={async () => {
if (!websiteUrl.trim()) {
setWebsiteError("Please enter a website URL");
return;
}
setWebsiteError(null);
setIsExtracting(true);
try {
const result = await podcastApi.extractUrl({ url: websiteUrl.trim() });
if (result.success) {
const extractionData = {
title: result.title || "",
text: result.text || "",
summary: result.summary || "",
highlights: result.highlights || [],
url: result.url,
image: result.image || undefined,
favicon: result.favicon || undefined,
subpages: result.subpages || [],
};
_setExtractedData(extractionData);
// Save to backend for future use
try {
await podcastApi.saveWebsiteExtraction({
title: extractionData.title,
text: extractionData.text,
summary: extractionData.summary,
highlights: extractionData.highlights,
url: extractionData.url,
subpages: extractionData.subpages,
});
} catch (saveErr) {
console.warn("[TopicUrlInput] Failed to save extraction:", saveErr);
}
setShowPreviewModal(true);
} else {
setWebsiteError(result.error || "Failed to extract content");
}
} catch (err: any) {
setWebsiteError(err?.message || "Failed to extract content");
} finally {
setIsExtracting(false);
}
}}
disabled={isExtracting || !websiteUrl.trim()}
sx={{
textTransform: "none",
fontWeight: 600,
fontSize: "0.8125rem",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
whiteSpace: "nowrap",
"&:hover": {
background: "linear-gradient(135deg, #7c8ff0 0%, #8a5cb3 100%)",
},
}}
>
{isExtracting ? <CircularProgress size={16} sx={{ color: "#fff" }} /> : "Extract"}
</Button>
</Box>
</Collapse>
<Box sx={{ position: "relative" }}>
<Tooltip
title={
isListening
? "Listening... Click the mic to stop."
: isUrl
? "We detected a URL. We'll fetch insights from this page."
: "Enter a concise idea, paste a blog URL, or click the mic to speak your topic."
}
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",
},
},
}}
>
<Chip
icon={<AttachMoneyIcon sx={{ fontSize: "0.875rem !important" }} />}
label={estimatedCost ? `Est. $${estimatedCost.total}` : "Est. Unavailable"}
<TextField
fullWidth
multiline
rows={5}
placeholder={!value ? `e.g., "${TOPIC_PLACEHOLDERS[placeholderIndex]}" or paste a URL` : ""}
inputProps={{
sx: {
"&::placeholder": { color: "#94a3b8", opacity: 1 },
color: "#1e293b",
},
}}
value={value}
onChange={(e) => onChange(e.target.value)}
size="small"
disabled={isListening}
helperText={
error
? error
: isListening
? "Listening... Speak your topic now."
: isUrl
? "URL detected. We'll analyze this page content."
: "Enter a clear, concise topic. You can also click the mic to speak."
}
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",
"& .MuiOutlinedInput-root": {
backgroundColor: isListening ? "rgba(16, 185, 129, 0.04)" : "#f8fafc",
border: isListening ? "2px solid rgba(16, 185, 129, 0.5)" : "2px solid rgba(102, 126, 234, 0.2)",
borderRadius: 2,
fontSize: "1rem",
transition: "all 0.2s ease",
"&:hover": {
backgroundColor: "#ffffff",
borderColor: isListening ? "rgba(16, 185, 129, 0.7)" : "rgba(102, 126, 234, 0.4)",
boxShadow: isListening ? "0 2px 8px rgba(16, 185, 129, 0.15)" : "0 2px 8px rgba(102, 126, 234, 0.1)",
},
"&.Mui-focused": {
backgroundColor: "#ffffff",
borderColor: isListening ? "#10b981" : isUrl ? "#10b981" : "#667eea",
borderWidth: 2,
boxShadow: isListening
? "0 0 0 4px rgba(16, 185, 129, 0.1)"
: isUrl
? "0 0 0 4px rgba(16, 185, 129, 0.1)"
: "0 0 0 4px rgba(102, 126, 234, 0.1)",
},
},
"& .MuiOutlinedInput-input": {
fontSize: "1rem",
lineHeight: 1.7,
color: "#1e293b",
fontWeight: 500,
"&::placeholder": {
color: "#64748b",
opacity: 1,
fontWeight: 400,
},
},
"& .MuiFormHelperText-root": {
color: error ? "#ef4444" : isListening ? "#059669" : isUrl ? "#059669" : "#64748b",
fontSize: "0.8125rem",
fontWeight: 500,
mt: 1,
},
}}
/>
</Tooltip>
</Stack>
<Tooltip
title={
isUrl
? "We detected a URL. We'll fetch insights from this page."
: "Enter a concise idea or paste a blog URL."
}
arrow
placement="top"
>
<TextField
fullWidth
multiline
rows={5}
placeholder={!value ? `e.g., "${TOPIC_PLACEHOLDERS[placeholderIndex]}" or paste a URL` : ""}
inputProps={{
sx: {
"&::placeholder": { color: "#94a3b8", opacity: 1 },
color: "#1e293b",
},
}}
value={value}
onChange={(e) => onChange(e.target.value)}
size="small"
helperText={
isUrl
? "URL detected. We'll analyze this page content."
: "Enter a clear, concise topic. We'll expand it into a full script after you click Analyze."
}
sx={{
"& .MuiOutlinedInput-root": {
backgroundColor: "#f8fafc",
border: "2px solid rgba(102, 126, 234, 0.2)",
borderRadius: 2,
fontSize: "1rem",
transition: "all 0.2s ease",
"&:hover": {
backgroundColor: "#ffffff",
borderColor: "rgba(102, 126, 234, 0.4)",
boxShadow: "0 2px 8px rgba(102, 126, 234, 0.1)",
},
"&.Mui-focused": {
backgroundColor: "#ffffff",
borderColor: isUrl ? "#10b981" : "#667eea",
borderWidth: 2,
boxShadow: isUrl
? "0 0 0 4px rgba(16, 185, 129, 0.1)"
: "0 0 0 4px rgba(102, 126, 234, 0.1)",
},
},
"& .MuiOutlinedInput-input": {
fontSize: "1rem",
lineHeight: 1.7,
color: "#1e293b",
fontWeight: 500,
"&::placeholder": {
color: "#64748b",
opacity: 1,
fontWeight: 400,
},
},
"& .MuiFormHelperText-root": {
color: isUrl ? "#059669" : "#64748b",
{/* Mic button with listening indicator - positioned inside the textarea bottom-right */}
{isSupported && !loading && (
<Box sx={{ position: "absolute", bottom: isListening ? 32 : 44, right: 4, zIndex: 2, display: "flex", alignItems: "center", gap: 1 }}>
{isListening && (
<Typography
sx={{
fontSize: "0.7rem",
fontWeight: 600,
color: "#059669",
backgroundColor: "rgba(16, 185, 129, 0.1)",
px: 1,
py: 0.25,
borderRadius: 1,
border: "1px solid rgba(16, 185, 129, 0.2)",
whiteSpace: "nowrap",
animation: "fadeIn 0.2s ease",
"@keyframes fadeIn": {
from: { opacity: 0, transform: "translateX(4px)" },
to: { opacity: 1, transform: "translateX(0)" },
},
}}
>
Listening...
</Typography>
)}
<IconButton
onClick={handleMicClick}
disabled={loading}
size="small"
sx={{
width: 36,
height: 36,
borderRadius: "50%",
background: isListening
? "linear-gradient(135deg, #10b981 0%, #059669 100%)"
: "linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%)",
color: isListening ? "#fff" : "#667eea",
border: isListening
? "2px solid rgba(16, 185, 129, 0.3)"
: "1px solid rgba(102, 126, 234, 0.25)",
boxShadow: isListening
? "0 0 0 4px rgba(16, 185, 129, 0.15), 0 2px 8px rgba(16, 185, 129, 0.3)"
: "0 2px 6px rgba(102, 126, 234, 0.15)",
animation: isListening ? "pulse-mic 1.5s ease-in-out infinite" : "none",
"&:hover": {
background: isListening
? "linear-gradient(135deg, #34d399 0%, #10b981 100%)"
: "linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%)",
transform: "scale(1.05)",
},
"&.Mui-disabled": {
background: "rgba(100, 116, 139, 0.08)",
color: "#94a3b8",
border: "1px solid rgba(100, 116, 139, 0.15)",
},
"@keyframes pulse-mic": {
"0%": { boxShadow: "0 0 0 4px rgba(16, 185, 129, 0.15), 0 2px 8px rgba(16, 185, 129, 0.3)" },
"50%": { boxShadow: "0 0 0 8px rgba(16, 185, 129, 0.08), 0 2px 12px rgba(16, 185, 129, 0.4)" },
"100%": { boxShadow: "0 0 0 4px rgba(16, 185, 129, 0.15), 0 2px 8px rgba(16, 185, 129, 0.3)" },
},
}}
>
{isListening ? (
<StopIcon sx={{ fontSize: "1.1rem" }} />
) : (
<MicIcon sx={{ fontSize: "1.1rem" }} />
)}
</IconButton>
</Box>
)}
</Box>
{/* Category Research Chips - News + Finance + Research Papers + Personal Website */}
{showAIDetailsButton && !isUrl && onCategoryResearchClick && (
<Box sx={{ display: "flex", justifyContent: "flex-start", mt: 1.5, gap: 1, flexWrap: "wrap" }}>
<Chip
icon={categoryResearchLoading ? <CircularProgress size={14} sx={{ color: "#667eea !important" }} /> : <NewspaperIcon sx={{ fontSize: "0.875rem !important" }} />}
label="News"
onClick={() => onCategoryResearchClick("news")}
disabled={categoryResearchLoading || loading}
size="small"
sx={{
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%)",
color: "#667eea",
border: "1px solid rgba(102, 126, 234, 0.3)",
fontWeight: 600,
fontSize: "0.8125rem",
fontWeight: 500,
mt: 1,
},
}}
/>
</Tooltip>
"&:hover": {
background: "linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%)",
transform: "scale(1.02)",
},
}}
/>
<Chip
icon={categoryResearchLoading ? <CircularProgress size={14} sx={{ color: "#10b981 !important" }} /> : <ShowChartIcon sx={{ fontSize: "0.875rem !important" }} />}
label="Finance"
onClick={() => onCategoryResearchClick("finance")}
disabled={categoryResearchLoading || loading}
size="small"
sx={{
background: "linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%)",
color: "#10b981",
border: "1px solid rgba(16, 185, 129, 0.3)",
fontWeight: 600,
fontSize: "0.8125rem",
"&:hover": {
background: "linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(5, 150, 105, 0.2) 100%)",
transform: "scale(1.02)",
},
}}
/>
<Chip
icon={categoryResearchLoading ? <CircularProgress size={14} sx={{ color: "#8b5cf6 !important" }} /> : <SchoolIcon sx={{ fontSize: "0.875rem !important" }} />}
label="Research Papers"
onClick={() => onCategoryResearchClick("research-paper")}
disabled={categoryResearchLoading || loading}
size="small"
sx={{
background: "linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(124, 58, 237, 0.1) 100%)",
color: "#8b5cf6",
border: "1px solid rgba(139, 92, 246, 0.3)",
fontWeight: 600,
fontSize: "0.8125rem",
"&:hover": {
background: "linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(124, 58, 237, 0.2) 100%)",
transform: "scale(1.02)",
},
}}
/>
<Chip
icon={categoryResearchLoading ? <CircularProgress size={14} sx={{ color: "#f59e0b !important" }} /> : <PublicIcon sx={{ fontSize: "0.875rem !important" }} />}
label="Personal Site"
onClick={() => onCategoryResearchClick("personal-site", value)}
disabled={categoryResearchLoading || loading}
size="small"
sx={{
background: "linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%)",
color: "#f59e0b",
border: "1px solid rgba(245, 158, 11, 0.3)",
fontWeight: 600,
fontSize: "0.8125rem",
"&:hover": {
background: "linear-gradient(135deg, rgba(245, 158, 11, 0.2) 0%, rgba(217, 119, 6, 0.2) 100%)",
transform: "scale(1.02)",
},
}}
/>
</Box>
)}
{/* Enhance topic with AI button + Get Trending Topics - appears when user types (and not a URL) */}
{showAIDetailsButton && !isUrl && (
@@ -340,6 +750,32 @@ export const TopicUrlInput: React.FC<TopicUrlInputProps> = ({
</Typography>
)}
</Box>
{/* Website Preview Modal */}
<WebsitePreviewModal
open={showPreviewModal}
extractedData={_extractedData}
onClose={() => {
setShowPreviewModal(false);
setShowWebsiteInput(false);
setWebsiteUrl("");
}}
onUseTextOnly={() => {
if (extractedData?.summary) {
const newValue = extractedData.title
? `${extractedData.title}: ${extractedData.summary}`
: extractedData.summary;
onChange(newValue);
}
setShowPreviewModal(false);
setShowWebsiteInput(false);
setWebsiteUrl("");
}}
onAnalyzeContent={() => {
// Phase 2: Will trigger full website analysis
console.log("[TopicUrlInput] Analyze Content clicked - Phase 2 feature");
}}
/>
</Box>
);
};
};