diff --git a/.gitignore b/.gitignore index 610ce41b..f8c30e28 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ nul LICENSE CHANGELOG.md +.planning +.planning/ + + .trae/ .trae diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md deleted file mode 100644 index 784ff160..00000000 --- a/.planning/PROJECT.md +++ /dev/null @@ -1,46 +0,0 @@ -# ALwrity Project - -## What This Is -ALwrity is an AI-powered content creation platform that helps users generate various types of content including podcasts, videos, blogs, and social media content. The platform features a React frontend and a FastAPI backend with onboarding workflows, API key management, and content generation capabilities. - -## Core Value -To provide an all-in-one AI content creation suite that simplifies the content production process for creators, marketers, and businesses. - -## Current Focus -Based on recent git commits, the team has been working on: -- Podcast production features (voice cloning, avatar generation, B-roll integration) -- Onboarding flow improvements -- Backend stability and debugging -- Frontend UI/UX enhancements - -## Requirements - -### Validated -- User authentication (Clerk) -- API key management for AI providers -- Basic podcast generation workflow -- File storage and media handling - -### Active -- Podcast script generation and editing -- Voice cloning and avatar creation -- B-roll scene rendering and integration -- Onboarding flow completion tracking -- API endpoint stability and debugging - -### Out of Scope -- Mobile applications (currently web-only) -- Enterprise team collaboration features -- Advanced analytics dashboard - -## Key Decisions -- Using FastAPI for backend performance -- React with Material-UI for frontend consistency -- Modular API design for extensibility -- Database-first approach for persistence - -## Constraints -- Must maintain backward compatibility with existing API -- Deployment targets include both development and production environments -- Must support multiple AI providers (OpenAI, HuggingFace, etc.) -- Budget-conscious resource usage for AI API calls \ No newline at end of file diff --git a/Procfile b/Procfile index 8b665e1f..a345bbb4 100644 --- a/Procfile +++ b/Procfile @@ -1,13 +1 @@ -web: cd backend && ALWRITY_ENABLED_FEATURES=podcast python -c " -import os -import sys -# Ensure podcast mode -os.environ.setdefault('ALWRITY_ENABLED_FEATURES', 'podcast') -# Set HOST/PORT for Render -port = os.getenv('PORT', '10000') -host = os.getenv('HOST', '0.0.0.0') -print(f'[STARTUP] Starting uvicorn on {host}:{port}', flush=True) -sys.stdout.flush() -import uvicorn -uvicorn.run('app:app', host=host, port=int(port), reload=False) -" +web: cd backend && python start_alwrity_backend.py --production diff --git a/README.md b/README.md deleted file mode 100644 index 1cc9a28b..00000000 --- a/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# 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/_session_backup/App.tsx b/_session_backup/App.tsx deleted file mode 100644 index a8f10a7c..00000000 --- a/_session_backup/App.tsx +++ /dev/null @@ -1,672 +0,0 @@ -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 deleted file mode 100644 index 39176d56..00000000 --- a/_session_backup/ResearchSummary.tsx +++ /dev/null @@ -1,537 +0,0 @@ -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 deleted file mode 100644 index 3ce71089..00000000 --- a/_session_backup/SceneEditor.tsx +++ /dev/null @@ -1,811 +0,0 @@ -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 deleted file mode 100644 index 98d4cd05..00000000 --- a/_session_backup/ScriptEditor.tsx +++ /dev/null @@ -1,818 +0,0 @@ -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