From 0732887c099da15e5d96a093b61d55cc1594f1bb Mon Sep 17 00:00:00 2001 From: ajaysi Date: Sun, 19 Apr 2026 13:21:36 +0530 Subject: [PATCH] Analyzing your idea with AI... --- .gitignore | 4 + README.md | 14 + .../ai_backlinker/README.md | 0 _session_backup/App.tsx | 672 ++++++++++++++ _session_backup/ResearchSummary.tsx | 537 +++++++++++ _session_backup/SceneEditor.tsx | 811 +++++++++++++++++ _session_backup/ScriptEditor.tsx | 818 +++++++++++++++++ _session_backup/analysis.py | 334 +++++++ _session_backup/models.py | 422 +++++++++ _session_backup/podcastApi.ts | 837 ++++++++++++++++++ _session_backup/research.py | 244 +++++ _session_backup/script.py | 183 ++++ _session_backup/types.ts | 209 +++++ _session_backup/usePodcastWorkflow.ts | 425 +++++++++ add_missing_columns.py | 184 ++++ backend/emojis.txt | 1 + .../AUDIO_ONLY_PODCAST_OPTIMIZATION.md | 530 +++++++++++ 17 files changed, 6225 insertions(+) create mode 100644 README.md create mode 100644 ToBeMigrated/lib/ai_marketing_tools/ai_backlinker/README.md create mode 100644 _session_backup/App.tsx create mode 100644 _session_backup/ResearchSummary.tsx create mode 100644 _session_backup/SceneEditor.tsx create mode 100644 _session_backup/ScriptEditor.tsx create mode 100644 _session_backup/analysis.py create mode 100644 _session_backup/models.py create mode 100644 _session_backup/podcastApi.ts create mode 100644 _session_backup/research.py create mode 100644 _session_backup/script.py create mode 100644 _session_backup/types.ts create mode 100644 _session_backup/usePodcastWorkflow.ts create mode 100644 add_missing_columns.py create mode 100644 backend/emojis.txt create mode 100644 docs/Podcast Maker/AUDIO_ONLY_PODCAST_OPTIMIZATION.md diff --git a/.gitignore b/.gitignore index d34a2410..610ce41b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ __pycache__/ *.db *.sqlite* +nul +LICENSE +CHANGELOG.md + .trae/ .trae diff --git a/README.md b/README.md new file mode 100644 index 00000000..1cc9a28b --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Render CLI + +## Installation + +- [Homebrew](https://render.com/docs/cli#homebrew-macos-linux) +- [Direct Download](https://render.com/docs/cli#direct-download) + +## Documentation + +Documentation is hosted at https://render.com/docs/cli. + +## Contributing + +To create a new command, use the `cmd/template.go` template file as a starting point. Reference the [CLI Style Guide](docs/STYLE.md) to learn more about command naming, flags, arguments, and help text conventions. diff --git a/ToBeMigrated/lib/ai_marketing_tools/ai_backlinker/README.md b/ToBeMigrated/lib/ai_marketing_tools/ai_backlinker/README.md new file mode 100644 index 00000000..e69de29b diff --git a/_session_backup/App.tsx b/_session_backup/App.tsx new file mode 100644 index 00000000..a8f10a7c --- /dev/null +++ b/_session_backup/App.tsx @@ -0,0 +1,672 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom'; +import { Box, CircularProgress, Typography } from '@mui/material'; +import { CopilotKit } from "@copilotkit/react-core"; +import { ClerkProvider, useAuth } from '@clerk/clerk-react'; +import "@copilotkit/react-ui/styles.css"; +import Wizard from './components/OnboardingWizard/Wizard'; +import MainDashboard from './components/MainDashboard/MainDashboard'; +import SEODashboard from './components/SEODashboard/SEODashboard'; +import ContentPlanningDashboard from './components/ContentPlanningDashboard/ContentPlanningDashboard'; +import FacebookWriter from './components/FacebookWriter/FacebookWriter'; +import LinkedInWriter from './components/LinkedInWriter/LinkedInWriter'; +import BlogWriter from './components/BlogWriter/BlogWriter'; +import StoryWriter from './components/StoryWriter/StoryWriter'; +import { StoryProjectList } from './components/StoryWriter/StoryProjectList'; +import YouTubeCreator from './components/YouTubeCreator/YouTubeCreator'; +import { CreateStudio, EditStudio, UpscaleStudio, ControlStudio, SocialOptimizer, AssetLibrary, ImageStudioDashboard, FaceSwapStudio, CompressionStudio, ImageProcessingStudio } from './components/ImageStudio'; +import { + VideoStudioDashboard, + CreateVideo, + AvatarVideo, + EnhanceVideo, + ExtendVideo, + EditVideo, + TransformVideo, + SocialVideo, + FaceSwap, + VideoTranslate, + VideoBackgroundRemover, + AddAudioToVideo, + LibraryVideo, +} from './components/VideoStudio'; +import { + ProductMarketingDashboard, + ProductPhotoshootStudio, + ProductAnimationStudio, + ProductVideoStudio, + ProductAvatarStudio, +} from './components/ProductMarketing'; +import PodcastDashboard from './components/PodcastMaker/PodcastDashboard'; +import PricingPage from './components/Pricing/PricingPage'; +import WixTestPage from './components/WixTestPage/WixTestPage'; +import WixCallbackPage from './components/WixCallbackPage/WixCallbackPage'; +import WordPressCallbackPage from './components/WordPressCallbackPage/WordPressCallbackPage'; +import BingCallbackPage from './components/BingCallbackPage/BingCallbackPage'; +import BingAnalyticsStorage from './components/BingAnalyticsStorage/BingAnalyticsStorage'; +import ResearchDashboard from './pages/ResearchDashboard'; +import IntentResearchTest from './pages/IntentResearchTest'; +import SchedulerDashboard from './pages/SchedulerDashboard'; +import BillingPage from './pages/BillingPage'; +import ApprovalsPage from './pages/ApprovalsPage'; +import TeamActivityPage from './pages/TeamActivityPage'; +import StripeDisputesDashboard from './pages/StripeDisputesDashboard'; +import ProtectedRoute from './components/shared/ProtectedRoute'; +import GSCAuthCallback from './components/SEODashboard/components/GSCAuthCallback'; +import Landing from './components/Landing/Landing'; +import ErrorBoundary from './components/shared/ErrorBoundary'; +import ErrorBoundaryTest from './components/shared/ErrorBoundaryTest'; +import CopilotKitDegradedBanner from './components/shared/CopilotKitDegradedBanner'; +import { OnboardingProvider } from './contexts/OnboardingContext'; +import { SubscriptionProvider, useSubscription } from './contexts/SubscriptionContext'; +import { CopilotKitHealthProvider } from './contexts/CopilotKitHealthContext'; +import { useOAuthTokenAlerts } from './hooks/useOAuthTokenAlerts'; + +import { setAuthTokenGetter, setClerkSignOut } from './api/client'; +import { setMediaAuthTokenGetter } from './utils/fetchMediaBlobUrl'; +import { setBillingAuthTokenGetter } from './services/billingService'; +import { useOnboarding } from './contexts/OnboardingContext'; +import { useState, useEffect } from 'react'; +import ConnectionErrorPage from './components/shared/ConnectionErrorPage'; +import { isPodcastOnlyDemoMode } from './utils/demoMode'; + +// interface OnboardingStatus { +// onboarding_required: boolean; +// onboarding_complete: boolean; +// current_step?: number; +// total_steps?: number; +// completion_percentage?: number; +// } + +// Conditional CopilotKit wrapper that only shows sidebar on content-planning route +const ConditionalCopilotKit: React.FC<{ children: React.ReactNode }> = ({ children }) => { + // Do not render CopilotSidebar here. Let specific pages/components control it. + return <>{children}; +}; + +// Wrapper to only enable CopilotKit checks/provider when user is authenticated +// This prevents CopilotKit from running on the Landing page +const AuthenticatedCopilotWrapper: React.FC<{ + children: React.ReactNode; + apiKey: string; +}> = ({ children, apiKey }) => { + const { isSignedIn } = useAuth(); + const location = useLocation(); + + // Exclude CopilotKit from running on: + // 1. Landing page (handled by !isSignedIn) + // 2. Onboarding pages (to prevent health check timeouts) + // 3. Podcast-only demo mode (CopilotKit not needed) + const isPodcastOnly = isPodcastOnlyDemoMode(); + const shouldExcludeCopilot = !isSignedIn || location.pathname.startsWith('/onboarding') || isPodcastOnly; + + if (shouldExcludeCopilot) { + return <>{children}; + } + + const hasKey = apiKey && apiKey.trim(); + + if (hasKey) { + // Enhanced error handler that updates health context + const handleCopilotKitError = (e: any) => { + console.error("CopilotKit Error:", e); + + // Try to get health context if available + // We'll use a custom event to notify health context since we can't access it directly here + const errorMessage = e?.error?.message || e?.message || 'CopilotKit error occurred'; + const errorType = errorMessage.toLowerCase(); + + // Differentiate between fatal and transient errors + const isFatalError = + errorType.includes('cors') || + errorType.includes('ssl') || + errorType.includes('certificate') || + errorType.includes('403') || + errorType.includes('forbidden') || + errorType.includes('ERR_CERT_COMMON_NAME_INVALID'); + + // Dispatch event for health context to listen to + window.dispatchEvent(new CustomEvent('copilotkit-error', { + detail: { + error: e, + errorMessage, + isFatal: isFatalError, + } + })); + }; + + return ( + + + + + Chat Unavailable + + + CopilotKit encountered an error. The app continues to work with manual controls. + + + } + > + + {children} + + + + ); + } + + return ( + + + {children} + + ); +}; + +// Component to handle initial routing based on subscription and onboarding status +// Flow: Subscription → Onboarding → Dashboard +const InitialRouteHandler: React.FC = () => { + const { loading, error, isOnboardingComplete, initializeOnboarding, data } = useOnboarding(); + const { subscription, loading: subscriptionLoading, checkSubscription } = useSubscription(); + const [connectionError, setConnectionError] = useState<{ + hasError: boolean; + error: Error | null; + }>({ + hasError: false, + error: null, + }); + + // Poll for OAuth token alerts and show toast notifications + // Only enabled when user is authenticated (has subscription) + useOAuthTokenAlerts({ + enabled: subscription?.active === true, + interval: 60000, // Poll every 1 minute + }); + + // Check subscription on mount (non-blocking - don't wait for it to route) + useEffect(() => { + // Delay subscription check slightly to allow auth token getter to be installed first + const timeoutId = setTimeout(async () => { + // Retry logic for initial subscription check + const maxRetries = 3; + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + await checkSubscription(); + break; // Success + } catch (err) { + console.error(`App: Subscription check attempt ${attempt + 1} failed:`, err); + + // If it's a connection error and we have retries left, wait and retry + const isConnectionError = err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError'); + + if (isConnectionError && attempt < maxRetries - 1) { + const delay = 1000 * Math.pow(2, attempt); // 1s, 2s + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + + // If final attempt or not a connection error, handle it + if (attempt === maxRetries - 1 || !isConnectionError) { + if (isConnectionError) { + setConnectionError({ + hasError: true, + error: err as Error, + }); + } + // Don't block routing on other errors + } + } + } + }, 100); // Small delay to ensure TokenInstaller has run + + return () => clearTimeout(timeoutId); + }, []); // Remove checkSubscription dependency to prevent loop + + // Initialize onboarding only after subscription is confirmed + useEffect(() => { + if (subscription && !subscriptionLoading) { + // Check if user is new (no subscription record at all) + const isNewUser = !subscription || subscription.plan === 'none'; + + console.log('InitialRouteHandler: Subscription data received:', { + plan: subscription.plan, + active: subscription.active, + isNewUser, + subscriptionLoading + }); + + if (subscription.active && !isNewUser) { + console.log('InitialRouteHandler: Subscription confirmed, initializing onboarding...'); + initializeOnboarding(); + } + } + }, [subscription, subscriptionLoading, initializeOnboarding]); + + // Handle connection error - show connection error page + if (connectionError.hasError) { + const handleRetry = () => { + setConnectionError({ + hasError: false, + error: null, + }); + // Re-trigger the subscription check using context + checkSubscription().catch((err) => { + if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) { + setConnectionError({ + hasError: true, + error: err, + }); + } + }); + }; + + const handleGoHome = () => { + window.location.href = '/'; + }; + + return ( + + ); + } + + // Loading state - only wait for onboarding init, not subscription check + // Subscription check is non-blocking and happens in background + const waitingForOnboardingInit = loading || !data; + if (loading || waitingForOnboardingInit) { + return ( + + + + {subscriptionLoading ? 'Checking subscription...' : 'Preparing your workspace...'} + + + ); + } + + // Error state + if (error) { + return ( + + + Error + + + {error} + + + ); + } + + // Decision tree for SIGNED-IN users: + // Priority: Subscription → Onboarding → Dashboard (as per user flow: Landing → Subscription → Onboarding → Dashboard) + + // 1. If subscription is still loading, show loading state + if (subscriptionLoading) { + return ( + + + + Checking subscription... + + + ); + } + + // 2. No subscription data yet - handle gracefully + // If onboarding is complete, allow access to dashboard (user already went through flow) + // If onboarding not complete, check if subscription check is still loading or failed + if (!subscription) { + if (isOnboardingComplete) { + console.log('InitialRouteHandler: Onboarding complete but no subscription data → Dashboard (allow access)'); + return ; + } + + // Onboarding not complete and no subscription data + // If subscription check is still loading, show loading state + if (subscriptionLoading) { + return ( + + + + Checking subscription... + + + ); + } + + // Subscription check completed but returned null/undefined + // This likely means no subscription - redirect to pricing + console.log('InitialRouteHandler: No subscription data after check → Pricing page'); + return ; + } + + // 3. Check subscription status first + const isNewUser = !subscription || subscription.plan === 'none'; + + // No active subscription → Show modal (SubscriptionContext handles this) + // Don't redirect immediately - let the modal show first + // User can click "Renew Subscription" button in modal to go to pricing + // Or click "Maybe Later" to dismiss (but they still can't use features) + if (isNewUser || !subscription.active) { + console.log('InitialRouteHandler: No active subscription - modal will be shown by SubscriptionContext'); + // Note: SubscriptionContext will show the modal automatically when subscription is inactive + // We still redirect to pricing for new users, but allow existing users with expired subscriptions + // to see the modal first. The modal has a "Renew Subscription" button that navigates to pricing. + // For new users (no subscription at all), redirect to pricing immediately + if (isNewUser) { + console.log('InitialRouteHandler: New user (no subscription) → Pricing page'); + return ; + } + // For existing users with inactive subscription, show modal but don't redirect immediately + // The modal will be shown by SubscriptionContext, and user can click "Renew Subscription" + // Allow access to dashboard (modal will be shown and block functionality) + console.log('InitialRouteHandler: Inactive subscription - allowing access to show modal'); + // Continue to onboarding/dashboard flow - modal will be shown by SubscriptionContext + } + + // 4. Has active subscription, check onboarding status + if (!isOnboardingComplete) { + console.log('InitialRouteHandler: Subscription active but onboarding incomplete → Onboarding'); + return ; + } + + // 5. Has subscription AND completed onboarding → Dashboard + console.log('InitialRouteHandler: All set (subscription + onboarding) → Dashboard'); + return ; +}; + +// Root route that chooses Landing (signed out) or InitialRouteHandler (signed in) +const RootRoute: React.FC = () => { + const { isSignedIn } = useAuth(); + if (isSignedIn) { + return ; + } + return ; +}; + +// Installs Clerk auth token getter into axios clients and stores user_id +// Must render under ClerkProvider +const TokenInstaller: React.FC = () => { + const { getToken, userId, isSignedIn, signOut } = useAuth(); + + // Store user_id in localStorage when user signs in + useEffect(() => { + if (isSignedIn && userId) { + console.log('TokenInstaller: Storing user_id in localStorage:', userId); + localStorage.setItem('user_id', userId); + + // Trigger event to notify SubscriptionContext that user is authenticated + window.dispatchEvent(new CustomEvent('user-authenticated', { detail: { userId } })); + } else if (!isSignedIn) { + // Clear user_id when signed out + console.log('TokenInstaller: Clearing user_id from localStorage'); + localStorage.removeItem('user_id'); + } + }, [isSignedIn, userId]); + + // Install token getter for API calls + useEffect(() => { + const tokenGetter = async () => { + try { + const template = process.env.REACT_APP_CLERK_JWT_TEMPLATE; + // If a template is provided and it's not a placeholder, request a template-specific JWT + if (template && template !== 'your_jwt_template_name_here') { + // @ts-ignore Clerk types allow options object + return await getToken({ template }); + } + return await getToken(); + } catch { + return null; + } + }; + + // Set token getter for main API client + setAuthTokenGetter(tokenGetter); + + // Set token getter for billing API client (same function) + setBillingAuthTokenGetter(tokenGetter); + + // Set token getter for media blob URL fetcher (for authenticated image/video requests) + setMediaAuthTokenGetter(tokenGetter); + }, [getToken]); + + // Install Clerk signOut function for handling expired tokens + useEffect(() => { + if (signOut) { + setClerkSignOut(async () => { + await signOut(); + }); + } + }, [signOut]); + + return null; +}; + +const App: React.FC = () => { + // React Hooks MUST be at the top before any conditionals + const [loading, setLoading] = useState(true); + + // Get CopilotKit key from localStorage or .env + const [copilotApiKey, setCopilotApiKey] = useState(() => { + const savedKey = localStorage.getItem('copilotkit_api_key'); + const envKey = process.env.REACT_APP_COPILOTKIT_API_KEY || ''; + const key = (savedKey || envKey).trim(); + + // Validate key format if present + if (key && !key.startsWith('ck_pub_')) { + console.warn('CopilotKit API key format invalid - must start with ck_pub_'); + } + + return key; + }); + + // Initialize app - loading state will be managed by InitialRouteHandler + useEffect(() => { + // Remove manual health check - connection errors are handled by ErrorBoundary + setLoading(false); + }, []); + + // Listen for CopilotKit key updates + useEffect(() => { + const handleKeyUpdate = (event: CustomEvent) => { + const newKey = event.detail?.apiKey; + if (newKey) { + console.log('App: CopilotKit key updated, reloading...'); + setCopilotApiKey(newKey); + setTimeout(() => window.location.reload(), 500); + } + }; + + window.addEventListener('copilotkit-key-updated', handleKeyUpdate as EventListener); + return () => window.removeEventListener('copilotkit-key-updated', handleKeyUpdate as EventListener); + }, []); + + // Token installer must be inside ClerkProvider; see TokenInstaller below + + if (loading) { + return ( + + + + Connecting to ALwrity... + + + ); + } + + + // Get environment variables with fallbacks + const clerkPublishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY || ''; + const clerkJSUrl = process.env.REACT_APP_CLERK_JS_URL; + + // Show error if required keys are missing + if (!clerkPublishableKey) { + return ( + + + Missing Clerk Publishable Key + + + Please add REACT_APP_CLERK_PUBLISHABLE_KEY to your .env file + + + ); + } + + // Render app with or without CopilotKit based on whether we have a key + const renderApp = () => { + return ( + + + + + + } /> + + + + } + /> + {/* Error Boundary Testing - Development Only */} + {process.env.NODE_ENV === 'development' && ( + } /> + )} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); + }; + + return ( + { + // Custom error handler - send to analytics/monitoring + console.error('Global error caught:', { error, errorInfo }); + // TODO: Send to error tracking service (Sentry, LogRocket, etc.) + }} + > + + + + {renderApp()} + + + + + ); +}; + +export default App; diff --git a/_session_backup/ResearchSummary.tsx b/_session_backup/ResearchSummary.tsx new file mode 100644 index 00000000..39176d56 --- /dev/null +++ b/_session_backup/ResearchSummary.tsx @@ -0,0 +1,537 @@ +import React, { useMemo, useCallback } from "react"; +import { Stack, Typography, Chip, Divider, Box, alpha, Paper, Tooltip } from "@mui/material"; +import { + Insights as InsightsIcon, + Search as SearchIcon, + AttachMoney as AttachMoneyIcon, + EditNote as EditNoteIcon, + Article as ArticleIcon, + AutoAwesome as AutoAwesomeIcon, + FormatQuote as FormatQuoteIcon, + Campaign as CampaignIcon, + Explore as ExploreIcon, +} from "@mui/icons-material"; +import { Research, ResearchInsight } from "../types"; +import { GlassyCard, glassyCardSx, PrimaryButton } from "../ui"; +import { FactCard } from "../FactCard"; + +interface ResearchSummaryProps { + research: Research; + canGenerateScript: boolean; + onGenerateScript: () => void; +} + +export const ResearchSummary: React.FC = ({ + research, + canGenerateScript, + onGenerateScript, +}) => { + // Simple markdown-to-HTML converter + const renderMarkdown = useCallback((text: string) => { + if (!text) return null; + return text + .split('\n') + .filter(line => line.trim() !== '') // Remove empty lines + .map((line, i) => { + // Handle bold + let processedLine = line.replace(/\*\*(.*?)\*\*/g, '$1'); + // Handle lists + if (processedLine.trim().startsWith('- ') || processedLine.trim().startsWith('* ')) { + return
  • ; + } + // Handle headers - make them smaller + if (processedLine.startsWith('### ')) { + return {processedLine.substring(4)}; + } + if (processedLine.startsWith('## ')) { + return {processedLine.substring(3)}; + } + // Paragraphs - compact spacing + return processedLine.trim() ?

    : null; + }); + }, []); + + return ( + + + + + + + Research Summary + + + {/* Research Metadata - Moved alongside title */} + + {research.searchQueries && research.searchQueries.length > 0 && ( + } + label={`${research.searchQueries.length} search${research.searchQueries.length > 1 ? "es" : ""}`} + size="small" + sx={{ + background: alpha("#667eea", 0.1), + color: "#667eea", + fontWeight: 600, + border: "1px solid rgba(102, 126, 234, 0.2)", + }} + /> + )} + {research.searchType && ( + + )} + {research.sourceCount !== undefined && ( + + )} + {research.cost !== undefined && ( + } + label={`$${research.cost.toFixed(3)}`} + size="small" + sx={{ + background: alpha("#f59e0b", 0.1), + color: "#d97706", + fontWeight: 600, + border: "1px solid rgba(245, 158, 11, 0.2)", + }} + /> + )} + + + + } + tooltip={!canGenerateScript ? "Complete research to generate script" : "Generate AI-powered script from research"} + > + Generate Script + + + + + {/* Main Summary */} + {research.summary && ( + + + + Executive Summary + + + {renderMarkdown(research.summary)} + + + )} + + {/* Deep Insights */} + {(research.keyInsights && research.keyInsights.length > 0) ? ( + + + + Deep Insights + + + {research.keyInsights.map((insight: ResearchInsight, idx: number) => ( + + + + {insight.title} + + {insight.source_indices && insight.source_indices.length > 0 && ( + + {insight.source_indices.map(sIdx => { + const sourceIdx = sIdx - 1; + const fact = research.factCards[sourceIdx]; + const sourceUrl = fact?.url; + const hasUrl = !!sourceUrl; + const hue = (sIdx * 47 + 220) % 360; + const gradientFrom = `hsl(${hue}, 70%, 55%)`; + const gradientTo = `hsl(${(hue + 30) % 360}, 80%, 65%)`; + return ( + + Source {sIdx} +
    + {sourceUrl} +
    + ) : `Source ${sIdx}`} + arrow + placement="top" + > + window.open(sourceUrl, "_blank", "noopener,noreferrer") : undefined} + sx={{ + height: 24, + minWidth: 36, + fontSize: '0.7rem', + fontWeight: 800, + fontFamily: "'Inter', 'Roboto', monospace", + letterSpacing: "0.02em", + border: "none", + background: hasUrl + ? `linear-gradient(135deg, ${gradientFrom}, ${gradientTo})` + : `linear-gradient(135deg, ${alpha(gradientFrom, 0.3)}, ${alpha(gradientTo, 0.3)})`, + color: hasUrl ? "#fff" : alpha("#fff", 0.7), + cursor: hasUrl ? "pointer" : "default", + borderRadius: "8px", + px: 0.5, + boxShadow: hasUrl + ? `0 2px 8px ${alpha(gradientFrom, 0.35)}, inset 0 1px 0 ${alpha("#fff", 0.2)}` + : "none", + transition: "all 0.2s ease", + "&:hover": hasUrl ? { + background: `linear-gradient(135deg, ${gradientTo}, ${gradientFrom})`, + boxShadow: `0 4px 14px ${alpha(gradientFrom, 0.5)}, inset 0 1px 0 ${alpha("#fff", 0.3)}`, + transform: "translateY(-1px)", + } : {}, + }} + /> + + ); + })} +
    + )} + + + {renderMarkdown(insight.content)} + + + ))} + + + ) : ( + /* Fallback if keyInsights is missing but we have summary paragraphs */ + research.summary && research.summary.length > 500 && !research.keyInsights && ( + + + + Additional Insights + + + + {/* Render parts of summary that might contain insights if structured data is missing */} + {renderMarkdown(research.summary.split('\n\n').slice(1).join('\n\n'))} + + + + ) + )} + + {/* Expert Quotes Section */} + {research.expertQuotes && research.expertQuotes.length > 0 && ( + + + + Expert Quotes ({research.expertQuotes.length}) + + + {research.expertQuotes.map((eq, idx) => ( + + + + + + “{eq.quote}” + + {eq.source_index !== undefined && (() => { + const fact = research.factCards[eq.source_index - 1]; + const sourceUrl = fact?.url; + const hasUrl = !!sourceUrl; + const hue = (eq.source_index * 47 + 270) % 360; + const gradientFrom = `hsl(${hue}, 70%, 55%)`; + const gradientTo = `hsl(${(hue + 30) % 360}, 80%, 65%)`; + return ( + + + Source {eq.source_index} +
    + {sourceUrl} +
    + ) : `Source ${eq.source_index}`} arrow placement="top"> + window.open(sourceUrl, "_blank", "noopener,noreferrer") : undefined} + sx={{ + height: 24, + fontSize: "0.7rem", + fontWeight: 800, + fontFamily: "'Inter', 'Roboto', monospace", + border: "none", + background: hasUrl + ? `linear-gradient(135deg, ${gradientFrom}, ${gradientTo})` + : `linear-gradient(135deg, ${alpha(gradientFrom, 0.3)}, ${alpha(gradientTo, 0.3)})`, + color: hasUrl ? "#fff" : alpha("#fff", 0.7), + cursor: hasUrl ? "pointer" : "default", + borderRadius: "8px", + px: 1, + boxShadow: hasUrl + ? `0 2px 8px ${alpha(gradientFrom, 0.35)}, inset 0 1px 0 ${alpha("#fff", 0.2)}` + : "none", + transition: "all 0.2s ease", + "&:hover": hasUrl ? { + background: `linear-gradient(135deg, ${gradientTo}, ${gradientFrom})`, + boxShadow: `0 4px 14px ${alpha(gradientFrom, 0.5)}, inset 0 1px 0 ${alpha("#fff", 0.3)}`, + transform: "translateY(-1px)", + } : {}, + }} + /> + +
    + ); + })()} +
    + + + ))} + + + )} + + {/* Search Queries Used */} + {research.searchQueries && research.searchQueries.length > 0 && ( + + + Search Queries Used + + + {research.searchQueries.map((query, idx) => ( + + ))} + + + )} + + + {research.factCards.length > 0 && ( + <> + + + + Research Sources & Facts ({research.factCards.length}) + + + Click to expand • Hover to see source + + + + {research.factCards.map((fact) => ( + + ))} + + + )} + + {/* Listener CTA Section */} + {research.listenerCta && research.listenerCta.length > 0 && ( + <> + + + + + Listener Call-to-Action Ideas ({research.listenerCta.length}) + + + {research.listenerCta.map((cta, idx) => ( + + + + {cta} + + + ))} + + + + )} + + {/* Mapped Angles Section */} + {research.mappedAngles && research.mappedAngles.length > 0 && ( + <> + + + + + Content Angles ({research.mappedAngles.length}) + + + {research.mappedAngles.map((angle, idx) => ( + + + + {angle.title} + + {angle.mappedFactIds && angle.mappedFactIds.length > 0 && ( + + {angle.mappedFactIds.slice(0, 4).map((fid: string) => ( + + ))} + {angle.mappedFactIds.length > 4 && ( + + )} + + )} + + + {angle.why} + + + ))} + + + + )} + +
    + ); +}; + diff --git a/_session_backup/SceneEditor.tsx b/_session_backup/SceneEditor.tsx new file mode 100644 index 00000000..3ce71089 --- /dev/null +++ b/_session_backup/SceneEditor.tsx @@ -0,0 +1,811 @@ +import React, { useState, useEffect } from "react"; +import { Stack, Box, Typography, Divider, Chip, alpha, CircularProgress, LinearProgress, IconButton, Tooltip } from "@mui/material"; +import { + EditNote as EditNoteIcon, + CheckCircle as CheckCircleIcon, + RadioButtonUnchecked as RadioButtonUncheckedIcon, + VolumeUp as VolumeUpIcon, + PlayArrow as PlayArrowIcon, + Image as ImageIcon, + Delete as DeleteIcon, +} from "@mui/icons-material"; +import { Scene, Line, Knobs } from "../types"; +import { GlassyCard, glassyCardSx, PrimaryButton } from "../ui"; +import { LineEditor } from "./LineEditor"; +import { ImageRegenerateModal, ImageGenerationSettings } from "./ImageRegenerateModal"; +import { AudioRegenerateModal, AudioGenerationSettings } from "./AudioRegenerateModal"; +import { podcastApi } from "../../../services/podcastApi"; +import { aiApiClient } from "../../../api/client"; +import { getCachedMedia, setCachedMedia } from "../../../utils/mediaCache"; + +interface SceneEditorProps { + scene: Scene; + onUpdateScene: (s: Scene) => void; + onApprove: (id: string) => Promise; + onDelete: (sceneId: string) => void; + knobs: Knobs; + approvingSceneId?: string | null; + generatingAudioId?: string | null; + onAudioGenerationStart?: (sceneId: string) => void; + onAudioGenerated?: (sceneId: string, audioUrl: string) => void; + idea?: string; // Podcast idea for image generation context + avatarUrl?: string | null; // Base avatar URL for consistent scene image generation + totalScenes?: number; // Total number of scenes in the script +} + +export const SceneEditor: React.FC = ({ + scene, + onUpdateScene, + onApprove, + onDelete, + knobs, + approvingSceneId, + generatingAudioId, + onAudioGenerationStart, + onAudioGenerated, + idea, + avatarUrl, + totalScenes, +}) => { + const [localGenerating, setLocalGenerating] = useState(false); + const [generatingImage, setGeneratingImage] = useState(false); + const [imageGenerationStatus, setImageGenerationStatus] = useState(""); + const [imageGenerationProgress, setImageGenerationProgress] = useState(0); + const [audioBlobUrl, setAudioBlobUrl] = useState(null); + const [imageBlobUrl, setImageBlobUrl] = useState(null); + const [imageLoading, setImageLoading] = useState(false); + const [showRegenerateModal, setShowRegenerateModal] = useState(false); + const [showAudioModal, setShowAudioModal] = useState(false); + const [audioSettings, setAudioSettings] = useState({ + voiceId: "Wise_Woman", + speed: 1.0, + volume: 1.0, + pitch: 0.0, + emotion: scene.emotion || "neutral", + englishNormalization: true, + sampleRate: 24000, + bitrate: 64000, + channel: "1", + format: "mp3", + languageBoost: "auto", + }); + + // Load audio as blob when audioUrl is available + useEffect(() => { + if (!scene.audioUrl) { + // Clean up blob URL if audioUrl is removed + setAudioBlobUrl((currentBlobUrl) => { + if (currentBlobUrl) { + URL.revokeObjectURL(currentBlobUrl); + } + return null; + }); + return; + } + + let isMounted = true; + const currentAudioUrl = scene.audioUrl; // Capture current value + + const loadAudioBlob = async () => { + try { + // Normalize path + let audioPath = currentAudioUrl.startsWith('/') ? currentAudioUrl : `/${currentAudioUrl}`; + + // Convert /api/story/audio/ to /api/podcast/audio/ if needed + if (audioPath.includes('/api/story/audio/')) { + const filename = audioPath.split('/api/story/audio/').pop() || ''; + audioPath = `/api/podcast/audio/${filename}`; + } + + // Ensure it's a podcast audio endpoint + if (!audioPath.includes('/api/podcast/audio/')) { + const filename = audioPath.split('/').pop() || currentAudioUrl; + audioPath = `/api/podcast/audio/${filename}`; + } + + // Remove query parameters if present + audioPath = audioPath.split('?')[0]; + + const response = await aiApiClient.get(audioPath, { + responseType: 'blob', + }); + + if (!isMounted) { + // Component unmounted or audioUrl changed, don't set blob URL + return; + } + + // Double-check that audioUrl hasn't changed + if (scene.audioUrl !== currentAudioUrl) { + return; + } + + const blob = response.data; + const blobUrl = URL.createObjectURL(blob); + + setAudioBlobUrl((prevBlobUrl) => { + // Clean up previous blob URL if exists + if (prevBlobUrl && prevBlobUrl !== blobUrl) { + URL.revokeObjectURL(prevBlobUrl); + } + return blobUrl; + }); + } catch (error) { + console.error(`Failed to load audio blob for scene ${scene.id}:`, error); + // Don't set blob URL on error - will show error state + } + }; + + loadAudioBlob(); + + // Cleanup: only mark as unmounted, don't revoke blob URL here + // The blob URL will be cleaned up when audioUrl changes (new effect) or component unmounts + return () => { + isMounted = false; + }; + }, [scene.audioUrl, scene.id]); + + // Load image as blob when imageUrl is available + useEffect(() => { + if (!scene.imageUrl) { + // Clean up blob URL if imageUrl is removed + setImageBlobUrl((currentBlobUrl) => { + if (currentBlobUrl && currentBlobUrl.startsWith('blob:')) { + URL.revokeObjectURL(currentBlobUrl); + } + return null; + }); + return; + } + + // Check cache first with scene context + const cachedUrl = getCachedMedia(scene.imageUrl, scene.id); + if (cachedUrl) { + console.log('[SceneEditor] Using cached image:', scene.imageUrl, `(scene: ${scene.id})`); + setImageBlobUrl(cachedUrl); + setImageLoading(false); + return; + } + + let isMounted = true; + const currentImageUrl = scene.imageUrl; // Capture current value + + const loadImageBlob = async () => { + try { + setImageLoading(true); + + // Check cache again in case it was loaded while we were waiting + const cachedUrl = getCachedMedia(currentImageUrl, scene.id); + if (cachedUrl) { + if (isMounted) { + setImageBlobUrl(cachedUrl); + setImageLoading(false); + } + return; + } + + console.log('[SceneEditor] Loading image blob for:', currentImageUrl); + + // Normalize path + let imagePath = currentImageUrl.startsWith('/') ? currentImageUrl : `/${currentImageUrl}`; + + // Convert /api/story/images/ to /api/podcast/images/ if needed + if (imagePath.includes('/api/story/images/')) { + const filename = imagePath.split('/api/story/images/').pop() || ''; + imagePath = `/api/podcast/images/${filename}`; + } + + // Ensure it's a podcast image endpoint + if (!imagePath.includes('/api/podcast/images/')) { + const filename = imagePath.split('/').pop() || currentImageUrl; + imagePath = `/api/podcast/images/${filename}`; + } + + // Remove query parameters if present + imagePath = imagePath.split('?')[0]; + + const response = await aiApiClient.get(imagePath, { + responseType: 'blob', + }); + + if (!isMounted) { + return; + } + + // Double-check that imageUrl hasn't changed + if (scene.imageUrl !== currentImageUrl) { + return; + } + + const blob = response.data; + const blobUrl = URL.createObjectURL(blob); + + // Cache the blob URL with scene context + setCachedMedia(currentImageUrl, blobUrl, 'image', blob.size, scene.id); + + setImageBlobUrl((prevBlobUrl) => { + // Clean up previous blob URL if exists + if (prevBlobUrl && prevBlobUrl !== blobUrl && prevBlobUrl.startsWith('blob:')) { + URL.revokeObjectURL(prevBlobUrl); + } + return blobUrl; + }); + console.log('[SceneEditor] Image blob loaded and cached successfully:', currentImageUrl); + } catch (error) { + console.error('[SceneEditor] Failed to load image blob:', error); + if (isMounted) { + // Try adding query token as fallback + try { + const token = localStorage.getItem('clerk_dashboard_token') || ''; + if (token) { + const urlWithToken = `${currentImageUrl}?token=${encodeURIComponent(token)}`; + setImageBlobUrl(urlWithToken); + setCachedMedia(currentImageUrl, urlWithToken, 'image', undefined, scene.id); + } + } catch (fallbackError) { + console.error('[SceneEditor] Fallback image loading failed:', fallbackError); + } + } + } finally { + if (isMounted) { + setImageLoading(false); + } + } + }; + + loadImageBlob(); + + return () => { + isMounted = false; + // Don't cleanup blob URL here - let the cache handle it + }; + }, [scene.imageUrl]); + + const updateLine = (updatedLine: Line) => { + const updated = { ...scene, lines: scene.lines.map((l) => (l.id === updatedLine.id ? updatedLine : l)) }; + onUpdateScene(updated); + }; + + const approving = approvingSceneId === scene.id; + const generating = generatingAudioId === scene.id || localGenerating; + const hasAudio = Boolean(scene.audioUrl && audioBlobUrl); + const hasImage = Boolean(scene.imageUrl); + + const handleApproveAndGenerate = async (settings?: AudioGenerationSettings) => { + const wasAlreadyApproved = scene.approved; + const sceneId = scene.id; + + try { + // Set generating state + setLocalGenerating(true); + if (onAudioGenerationStart) { + onAudioGenerationStart(sceneId); + } + + // If scene is not approved yet, approve it first + // This will update the parent script state + if (!scene.approved) { + await onApprove(sceneId); + // The parent's approveScene already updated the script state + // We need to wait for React to propagate the updated scene prop + // For now, we'll update it locally too to ensure UI updates immediately + onUpdateScene({ ...scene, approved: true }); + } + + // Use the current scene (which should now be approved) + // If scene prop hasn't updated yet, use the local update we just made + const currentScene = { ...scene, approved: true }; + + // Generate audio + const effectiveSettings = settings || audioSettings; + const result = await podcastApi.renderSceneAudio({ + scene: currentScene, + voiceId: effectiveSettings.voiceId || "Wise_Woman", + emotion: effectiveSettings.emotion || scene.emotion || knobs.voice_emotion || "neutral", + speed: effectiveSettings.speed ?? knobs.voice_speed ?? 1.0, + volume: effectiveSettings.volume ?? 1.0, + pitch: effectiveSettings.pitch ?? 0.0, + englishNormalization: effectiveSettings.englishNormalization ?? true, + sampleRate: effectiveSettings.sampleRate, + bitrate: effectiveSettings.bitrate, + channel: effectiveSettings.channel, + format: effectiveSettings.format, + languageBoost: effectiveSettings.languageBoost, + }); + + // Update scene with audio URL and ensure approved state + // This will sync with parent script state + const updatedScene = { ...currentScene, audioUrl: result.audioUrl, approved: true }; + onUpdateScene(updatedScene); + + if (onAudioGenerated) { + onAudioGenerated(sceneId, result.audioUrl); + } + } catch (error) { + console.error("Failed to approve and generate audio:", error); + // On error, revert approval only if we just approved it in this call + if (!wasAlreadyApproved) { + onUpdateScene({ ...scene, approved: false, audioUrl: undefined }); + } + throw error; + } finally { + setLocalGenerating(false); + } + }; + + const handleGenerateImage = async (settings?: ImageGenerationSettings) => { + const sceneId = scene.id; + const startTime = Date.now(); + let progressInterval: NodeJS.Timeout | null = null; + + try { + setGeneratingImage(true); + setShowRegenerateModal(false); + setImageGenerationStatus("Submitting image generation request..."); + setImageGenerationProgress(10); + + // Build scene content from lines for context + const sceneContent = scene.lines.map((line) => line.text).join(" "); + + // Log avatar URL for debugging + console.log("[SceneEditor] Generating image with avatarUrl:", avatarUrl); + console.log("[SceneEditor] Custom settings:", settings); + + // Simulate progress updates during API call + progressInterval = setInterval(() => { + const elapsed = Date.now() - startTime; + const seconds = Math.floor(elapsed / 1000); + + // Update status based on elapsed time + if (seconds < 5) { + setImageGenerationStatus("Submitting request to AI service..."); + setImageGenerationProgress(15); + } else if (seconds < 15) { + setImageGenerationStatus("AI is generating your image..."); + setImageGenerationProgress(30); + } else if (seconds < 30) { + setImageGenerationStatus("Creating character-consistent scene image..."); + setImageGenerationProgress(50); + } else if (seconds < 60) { + setImageGenerationStatus("Rendering image details..."); + setImageGenerationProgress(70); + } else { + setImageGenerationStatus(`Processing... (${seconds}s elapsed)`); + setImageGenerationProgress(Math.min(90, 50 + (seconds - 30) / 2)); + } + }, 1000); + + const result = await podcastApi.generateSceneImage({ + sceneId: scene.id, + sceneTitle: scene.title, + sceneContent: sceneContent, + baseAvatarUrl: avatarUrl || undefined, // Pass base avatar URL for character consistency + idea: idea, + width: 1024, + height: 1024, + // Pass custom settings if provided + customPrompt: settings?.prompt, + style: settings?.style, + renderingSpeed: settings?.renderingSpeed, + aspectRatio: settings?.aspectRatio, + }); + + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + } + + setImageGenerationStatus("Finalizing image..."); + setImageGenerationProgress(95); + + // Update scene with image URL + const updatedScene = { ...scene, imageUrl: result.image_url }; + onUpdateScene(updatedScene); + + const elapsed = Math.floor((Date.now() - startTime) / 1000); + setImageGenerationStatus(`Image generated successfully in ${elapsed}s`); + setImageGenerationProgress(100); + + // Clear status after a moment + setTimeout(() => { + setImageGenerationStatus(""); + setImageGenerationProgress(0); + }, 2000); + } catch (error: any) { + // Clear interval on error + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + } + + console.error("Failed to generate image:", error); + // Extract error message from response if available + const errorMessage = error?.response?.data?.detail?.message + || error?.response?.data?.detail?.error + || error?.response?.data?.detail + || error?.message + || "Failed to generate image. Please try again."; + console.error("Error details:", { + status: error?.response?.status, + statusText: error?.response?.statusText, + data: error?.response?.data, + message: errorMessage, + }); + + setImageGenerationStatus(`Error: ${errorMessage}`); + setImageGenerationProgress(0); + + // Show user-friendly error message + alert(`Image generation failed: ${errorMessage}`); + throw error; + } finally { + // Ensure interval is cleared + if (progressInterval) { + clearInterval(progressInterval); + } + setGeneratingImage(false); + } + }; + + const handleRegenerateClick = () => { + setShowRegenerateModal(true); + }; + + const handleAudioRegenerateClick = () => { + if (hasAudio) { + setShowAudioModal(true); + } else { + handleApproveAndGenerate(audioSettings); + } + }; + + const handleAudioRegenerate = (settings: AudioGenerationSettings) => { + setAudioSettings(settings); + setShowAudioModal(false); + handleApproveAndGenerate(settings); + }; + + return ( + + + + + + + {scene.title} + + + : } + label={scene.approved ? "Approved" : "Pending Approval"} + size="small" + color={scene.approved ? "success" : "warning"} + sx={{ + background: scene.approved + ? "linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%)" + : "linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(217, 119, 6, 0.12) 100%)", + color: scene.approved ? "#059669" : "#d97706", + border: scene.approved + ? "1px solid rgba(16, 185, 129, 0.25)" + : "1px solid rgba(245, 158, 11, 0.25)", + fontWeight: 600, + fontSize: "0.75rem", + height: 26, + boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)", + }} + /> + + Duration: {scene.duration}s + + + + + + ) : generating ? ( + + ) : ( + + ) + } + tooltip={ + hasAudio && !generating + ? "Regenerate audio for this scene with custom settings" + : generating + ? "Generating audio..." + : scene.approved + ? "Generate audio for this scene" + : "Approve scene and generate audio" + } + sx={{ + minWidth: 200, + }} + > + {hasAudio && !generating + ? "Regenerate Audio" + : generating + ? "Generating Audio..." + : scene.approved + ? "Generate Audio" + : "Approve & Generate Audio"} + + handleGenerateImage()} + disabled={generatingImage} + loading={generatingImage} + startIcon={ + hasImage && !generatingImage ? ( + + ) : generatingImage ? ( + + ) : ( + + ) + } + tooltip={ + hasImage + ? "Regenerate image for this scene" + : generatingImage + ? "Generating image..." + : "Generate image for video (optional)" + } + sx={{ + minWidth: 180, + background: hasImage + ? "linear-gradient(135deg, #10b981 0%, #059669 100%)" + : "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + "&:hover": { + background: hasImage + ? "linear-gradient(135deg, #059669 0%, #047857 100%)" + : "linear-gradient(135deg, #764ba2 0%, #667eea 100%)", + }, + }} + > + {hasImage && !generatingImage + ? "Regenerate Image" + : generatingImage + ? "Generating Image..." + : "Generate Image"} + + + + onDelete(scene.id)} + disabled={approving || generating || (totalScenes !== undefined && totalScenes <= 1)} + sx={{ + color: "#ef4444", + backgroundColor: "rgba(239, 68, 68, 0.1)", + border: "1px solid rgba(239, 68, 68, 0.2)", + borderRadius: 2, + padding: 1.5, + "&:hover": { + backgroundColor: "rgba(239, 68, 68, 0.15)", + borderColor: "rgba(239, 68, 68, 0.3)", + }, + "&:disabled": { + backgroundColor: "rgba(156, 163, 175, 0.1)", + borderColor: "rgba(156, 163, 175, 0.2)", + color: "#9ca3af", + }, + }} + > + + + + + + + + + + {scene.lines.map((line) => ( + + ))} + + + {scene.audioUrl && ( + <> + + + + + + {hasAudio ? "Audio Generated" : "Loading Audio..."} + + + {hasAudio && audioBlobUrl ? ( + + ) : ( + + + + )} + + + )} + + {/* Image Generation Progress - Show when generating */} + {generatingImage && ( + <> + + + + + + Generating Image... + + + + {/* Progress Bar */} + + + + {imageGenerationProgress}% + + + + {/* Status Message */} + {imageGenerationStatus && ( + + {imageGenerationStatus} + + )} + + {/* Spinner */} + + + + + + )} + + {/* Generated Image Display - Show when image exists and not generating */} + {scene.imageUrl && !generatingImage && ( + <> + + + + + + {imageBlobUrl && !imageLoading ? "Image Generated" : "Loading Image..."} + + + {imageBlobUrl && !imageLoading ? ( + + { + console.error('[SceneEditor] Image failed to load:', { + src: e.currentTarget.src, + imageUrl: scene.imageUrl, + imageBlobUrl, + }); + }} + onLoad={() => { + console.log('[SceneEditor] Image loaded successfully'); + }} + /> + + ) : ( + + + + )} + + + )} + + + {/* Image Regeneration Modal */} + setShowRegenerateModal(false)} + onRegenerate={handleGenerateImage} + initialPrompt={(() => { + const promptParts = [ + `Scene: ${scene.title}`, + "Professional podcast recording studio", + "Modern microphone setup", + "Clean background, professional lighting", + "16:9 aspect ratio, video-optimized composition" + ]; + if (idea) { + promptParts.push(`Topic: ${idea.substring(0, 60)}`); + } + return promptParts.join(", "); + })()} + initialStyle="Realistic" + initialRenderingSpeed="Quality" + initialAspectRatio="16:9" + isGenerating={generatingImage} + /> + + setShowAudioModal(false)} + onRegenerate={handleAudioRegenerate} + initialSettings={audioSettings} + isGenerating={generating} + /> + + ); +}; + diff --git a/_session_backup/ScriptEditor.tsx b/_session_backup/ScriptEditor.tsx new file mode 100644 index 00000000..98d4cd05 --- /dev/null +++ b/_session_backup/ScriptEditor.tsx @@ -0,0 +1,818 @@ +import React, { useEffect, useState, useCallback } from "react"; +import { Box, Stack, Typography, Alert, Paper, LinearProgress, CircularProgress, alpha, Collapse, IconButton, Divider } from "@mui/material"; +import { EditNote as EditNoteIcon, CheckCircle as CheckCircleIcon, PlayArrow as PlayArrowIcon, ArrowBack as ArrowBackIcon, Info as InfoIcon, ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, Download as DownloadIcon, Refresh as RefreshIcon } from "@mui/icons-material"; +import { Script, Knobs, Scene } from "../types"; +import { BlogResearchResponse } from "../../../services/blogWriterApi"; +import { podcastApi } from "../../../services/podcastApi"; +import { GlassyCard, PrimaryButton, SecondaryButton } from "../ui"; +import { SceneEditor } from "./SceneEditor"; +import { InlineAudioPlayer } from "../InlineAudioPlayer"; +import { aiApiClient } from "../../../api/client"; + +interface ScriptEditorProps { + projectId: string; + idea: string; + research: any; // Research type + rawResearch: BlogResearchResponse | null; + knobs: Knobs; + speakers: number; + durationMinutes: number; + script: Script | null; + onScriptChange: (script: Script) => void; + onBackToResearch: () => void; + onProceedToRendering: (script: Script) => void; + onError: (message: string) => void; + avatarUrl?: string | null; // Base avatar URL for consistent scene image generation + analysis?: any; + outline?: any; +} + +export const ScriptEditor: React.FC = ({ + projectId, + idea, + research, + rawResearch, + knobs, + speakers, + durationMinutes, + script: initialScript, + onScriptChange, + onBackToResearch, + onProceedToRendering, + onError, + avatarUrl, + analysis, + outline, +}) => { + const [script, setScript] = useState