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