import React, { useEffect, useState, useCallback } from "react"; import { ArrowLeft, X, ChevronDown } from "lucide-react"; import { useStudioStore } from "./studio/stores/studioStore"; import { CreatorStudioEditor } from "./CreatorStudioEditor"; import { detectArchitecture, getArchitectureLabel, getModelSettings } from "./modelPresets"; /** * CreatorStudioHost - Handles both wizard (new) and editor (existing) modes * * - If projectId is provided: opens the editor for that project * - If no projectId: shows the New Project wizard * - After wizard creates a project: switches to editor mode */ export function CreatorStudioHost({ backendUrl, apiKey, projectId: initialProjectId, onExit, }) { const authKey = (apiKey || "").trim(); // Mode: "wizard" for creating new, "editor" for existing project const [mode, setMode] = useState(initialProjectId ? "editor" : "wizard"); const [currentProjectId, setCurrentProjectId] = useState(initialProjectId); const [isNewlyCreated, setIsNewlyCreated] = useState(false); const [projectSettings, setProjectSettings] = useState({ targetSceneCount: 8, sceneDuration: 5, llmModel: "", imageModel: "", videoModel: "", enableVideoGeneration: false, imageWidth: undefined, imageHeight: undefined, }); // Bootstrap connection info for API calls useEffect(() => { const store = useStudioStore.getState(); if (store.setConnection) { store.setConnection(backendUrl, authKey); } }, [backendUrl, authKey]); // If we have a project ID, show the editor if (mode === "editor" && currentProjectId) { return (); } // Otherwise, show the wizard return ( { // Switch to editor mode with the new project setCurrentProjectId(projectId); setIsNewlyCreated(true); // Flag for auto-generating first scene // Ensure imageWidth/imageHeight are always present (may be undefined) setProjectSettings({ ...settings, imageWidth: settings.imageWidth, imageHeight: settings.imageHeight, }); setMode("editor"); }}/>); } function CreatorStudioWizard({ backendUrl, apiKey, onExit, onProjectCreated, }) { const authKey = (apiKey || "").trim(); // Wizard state - 6 steps: Project Type (0), Details (1), Visuals (2), Checks (3), Review (4), Outline (5) const [step, setStep] = useState(0); // Project type selection (Step 0) const [projectType, setProjectType] = useState("video"); // Core fields const [title, setTitle] = useState(""); const [logline, setLogline] = useState(""); const [platformPreset, setPlatformPreset] = useState("youtube_16_9"); const [contentRating, setContentRating] = useState("sfw"); const [allowMature, setAllowMature] = useState(false); const [localOnly, setLocalOnly] = useState(true); // Wizard fields const [goal, setGoal] = useState("Educate"); const [tones, setTones] = useState(["Documentary", "Calm"]); const [visualStyle, setVisualStyle] = useState("Cinematic"); const [lockIdentity, setLockIdentity] = useState(true); // Episode/scene configuration const [targetSceneCount, setTargetSceneCount] = useState(8); const [sceneDuration, setSceneDuration] = useState(5); // LLM Model selection const [availableLLMModels, setAvailableLLMModels] = useState([]); const [selectedLLMModel, setSelectedLLMModel] = useState(""); const [loadingModels, setLoadingModels] = useState(false); // Image Model selection (ComfyUI checkpoints) const [availableImageModels, setAvailableImageModels] = useState([]); const [selectedImageModel, setSelectedImageModel] = useState(""); const [loadingImageModels, setLoadingImageModels] = useState(false); // Video Model selection (ComfyUI video models) const [availableVideoModels, setAvailableVideoModels] = useState([]); const [selectedVideoModel, setSelectedVideoModel] = useState(""); const [loadingVideoModels, setLoadingVideoModels] = useState(false); // Video generation toggle - auto-enabled for video/video_series projects const [enableVideoGeneration, setEnableVideoGeneration] = useState(true); // Mature content settings (only visible when contentRating === "mature") const [matureCategory, setMatureCategory] = useState("fan_service"); const [intensityLevel, setIntensityLevel] = useState(0.3); // 0-1 scale: 0=tasteful, 1=bold // Mature consent modal const [showMatureModal, setShowMatureModal] = useState(false); const [matureConsentChecked, setMatureConsentChecked] = useState(false); // UI state const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Outline generation state (Step 5) const [generatingOutline, setGeneratingOutline] = useState(false); const [generatedOutline, setGeneratedOutline] = useState(null); const [tempProjectId, setTempProjectId] = useState(null); // Fetch available LLM models const fetchLLMModels = useCallback(async () => { setLoadingModels(true); try { const url = `${backendUrl.replace(/\/+$/, "")}/models?provider=ollama`; const res = await fetch(url, { headers: authKey ? { "x-api-key": authKey } : {}, }); if (res.ok) { const data = await res.json(); if (data.models) { // Backend returns models as strings (e.g., ["llama3:8b", "mistral:latest"]) const models = data.models.map((m) => { // Handle both string format and object format if (typeof m === 'string') { return { id: m, name: m }; } return { id: m.id, name: m.name || m.id }; }); setAvailableLLMModels(models); // Auto-select first model if none selected if (!selectedLLMModel && models.length > 0) { // Prefer llama3:8b if available, otherwise first model const preferred = models.find((m) => m.id.includes("llama3")) || models[0]; setSelectedLLMModel(preferred.id); } } } } catch (e) { console.log("[Wizard] Failed to fetch LLM models:", e); } finally { setLoadingModels(false); } }, [backendUrl, authKey, selectedLLMModel]); // Fetch available image models from ComfyUI const fetchImageModels = useCallback(async () => { setLoadingImageModels(true); try { const url = `${backendUrl.replace(/\/+$/, "")}/models?provider=comfyui&model_type=image`; const res = await fetch(url, { headers: authKey ? { "x-api-key": authKey } : {}, }); if (res.ok) { const data = await res.json(); if (data.models) { const models = data.models.map((m) => ({ id: m, name: m, })); setAvailableImageModels(models); // Auto-select first model if none selected if (!selectedImageModel && models.length > 0) { // Prefer dreamshaper for SD1.5 (good quality, safe) const preferred = models.find((m) => m.id.toLowerCase().includes("dreamshaper")) || models[0]; setSelectedImageModel(preferred.id); } } } } catch (e) { console.log("[Wizard] Failed to fetch image models:", e); } finally { setLoadingImageModels(false); } }, [backendUrl, authKey, selectedImageModel]); // Fetch available video models from ComfyUI const fetchVideoModels = useCallback(async () => { setLoadingVideoModels(true); try { const url = `${backendUrl.replace(/\/+$/, "")}/models?provider=comfyui&model_type=video`; const res = await fetch(url, { headers: authKey ? { "x-api-key": authKey } : {}, }); if (res.ok) { const data = await res.json(); if (data.models) { const models = data.models.map((m) => ({ id: m, name: m, })); setAvailableVideoModels(models); // Auto-select first model if none selected if (!selectedVideoModel && models.length > 0) { setSelectedVideoModel(models[0].id); } } } } catch (e) { console.log("[Wizard] Failed to fetch video models:", e); } finally { setLoadingVideoModels(false); } }, [backendUrl, authKey, selectedVideoModel]); // Fetch models on mount useEffect(() => { fetchLLMModels(); fetchImageModels(); fetchVideoModels(); }, [fetchLLMModels, fetchImageModels, fetchVideoModels]); // Build tags for backend const tagsForBackend = React.useMemo(() => { const t = []; // Project type (determines generation workflow) t.push(`projectType:${projectType}`); // Also add mode:video or mode:slideshow for easy filtering and editor detection t.push(`mode:${projectType === "slideshow" ? "slideshow" : "video"}`); if (goal) t.push(`goal:${goal.toLowerCase()}`); if (visualStyle) t.push(`visual:${visualStyle.toLowerCase().replaceAll(" ", "_")}`); if (tones.length) t.push(...tones.map((x) => `tone:${x.toLowerCase().replaceAll(" ", "_")}`)); if (lockIdentity) t.push("lock:identity"); // Include episode configuration t.push(`scenes:${targetSceneCount}`); t.push(`duration:${sceneDuration}`); // Include selected models if (selectedLLMModel) t.push(`llm:${selectedLLMModel}`); if (selectedImageModel) t.push(`imageModel:${selectedImageModel}`); // Video generation settings t.push(`videoGeneration:${enableVideoGeneration ? 'enabled' : 'disabled'}`); if (enableVideoGeneration && selectedVideoModel) t.push(`videoModel:${selectedVideoModel}`); // Mature content settings (only when mature mode is enabled) if (contentRating === "mature") { t.push(`mature:enabled`); t.push(`matureCategory:${matureCategory}`); t.push(`intensity:${intensityLevel.toFixed(2)}`); } return Array.from(new Set(t)); }, [projectType, goal, visualStyle, tones, lockIdentity, targetSceneCount, sceneDuration, selectedLLMModel, selectedImageModel, enableVideoGeneration, selectedVideoModel, contentRating, matureCategory, intensityLevel]); // Helper: Convert platform preset to aspect ratio for resolution lookup const platformToAspectRatio = React.useCallback((platform) => { if (platform === "shorts_9_16") return "9:16"; return "16:9"; // youtube_16_9 and slides_16_9 both use 16:9 }, []); // Computed resolution based on selected image model and platform preset const computedResolution = React.useMemo(() => { const aspectRatio = platformToAspectRatio(platformPreset); const settings = getModelSettings(selectedImageModel || "", aspectRatio, "med"); return { width: settings.width, height: settings.height, aspectRatio, aspectLabel: aspectRatio === "16:9" ? "landscape" : aspectRatio === "9:16" ? "vertical" : aspectRatio, archLabel: getArchitectureLabel(settings.architecture), architecture: settings.architecture, }; }, [platformPreset, selectedImageModel, platformToAspectRatio]); const canProceedStep1 = title.trim().length > 0; const canCreate = title.trim().length > 0 && !loading && generatedOutline; function goTo(next) { setError(null); setStep(next); } function canNav(target) { // Can navigate to previous steps or the next step // But step 5 (Outline) requires going through step 4 first if (target === 5) return step === 5 || (step === 4 && generatedOutline); return target < step || target === step + 1; } // Auto-configure based on project type function handleProjectTypeSelect(type) { setProjectType(type); // Auto-set platform preset based on project type if (type === "slideshow") { setPlatformPreset("slides_16_9"); // Slideshows use images only (Ken Burns effect) setEnableVideoGeneration(false); } else if (type === "video_series") { setPlatformPreset("youtube_16_9"); // Video series uses AI video generation setEnableVideoGeneration(true); } else { // "video" - default to YouTube with video generation setPlatformPreset("youtube_16_9"); setEnableVideoGeneration(true); } } function toggleTone(t) { setTones((prev) => { const has = prev.includes(t); if (has) return prev.filter((x) => x !== t); return [...prev, t]; }); } function requestMature() { setShowMatureModal(true); setMatureConsentChecked(false); } function confirmMatureEnabled() { if (!matureConsentChecked) return; setShowMatureModal(false); setContentRating("mature"); setAllowMature(true); setLocalOnly(true); } function cancelMature() { setShowMatureModal(false); setMatureConsentChecked(false); } // Create project and generate outline (transition from Step 4 to Step 5) async function handleGenerateOutline() { if (!title.trim()) { setError("Project name is required."); setStep(1); return; } setGeneratingOutline(true); setError(null); setGeneratedOutline(null); try { // Step 1: Create the project first (if not already created) let projectId = tempProjectId; if (!projectId) { const url = `${backendUrl.replace(/\/+$/, "")}/studio/videos`; const payload = { title: title.trim(), logline: logline.trim(), tags: tagsForBackend, platformPreset, targetDurationSec: targetSceneCount * sceneDuration, contentRating, policyMode: contentRating === "mature" ? "restricted" : "youtube_safe", providerPolicy: { allowMature: contentRating === "mature" ? !!allowMature : false, allowedProviders: ["ollama"], localOnly: contentRating === "mature" ? !!localOnly : true, }, }; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...(authKey ? { "x-api-key": authKey } : {}), }, body: JSON.stringify(payload), }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`HTTP ${res.status}${text ? `: ${text}` : ""}`); } const data = await res.json(); projectId = data.video?.id; if (!projectId) { throw new Error("No project ID returned from server"); } setTempProjectId(projectId); } // Step 2: Generate the outline const outlineUrl = `${backendUrl.replace(/\/+$/, "")}/studio/videos/${projectId}/generate-outline`; const outlinePayload = { target_scenes: targetSceneCount, scene_duration: sceneDuration, ollama_model: selectedLLMModel || undefined, }; const outlineRes = await fetch(outlineUrl, { method: "POST", headers: { "Content-Type": "application/json", ...(authKey ? { "x-api-key": authKey } : {}), }, body: JSON.stringify(outlinePayload), }); if (!outlineRes.ok) { const text = await outlineRes.text().catch(() => ""); throw new Error(`Failed to generate outline: HTTP ${outlineRes.status}${text ? `: ${text}` : ""}`); } const outlineData = await outlineRes.json(); if (!outlineData.ok || !outlineData.outline) { throw new Error("Outline generation failed - no outline returned"); } setGeneratedOutline(outlineData.outline); setStep(5); // Move to outline review step } catch (e) { setError(e.message || String(e)); } finally { setGeneratingOutline(false); } } async function handleCreate() { // Project should already be created during outline generation if (!tempProjectId) { setError("Project not created yet. Please generate outline first."); return; } if (!generatedOutline) { setError("No outline generated. Please generate outline first."); return; } setLoading(true); setError(null); try { // Project already exists with outline - just open it in the editor onProjectCreated(tempProjectId, { targetSceneCount, sceneDuration, llmModel: selectedLLMModel, imageModel: selectedImageModel, videoModel: selectedVideoModel, enableVideoGeneration, imageWidth: computedResolution.width, imageHeight: computedResolution.height, }); } catch (e) { setError(e.message || String(e)); } finally { setLoading(false); } } return (
{/* Header with Exit Button */}
Creator Studio
{/* Spacer for centering */}
{/* Mature Consent Modal */} {showMatureModal && (
Enable Mature Mode?
This enables adult/mature generation for this project. Use only where legal and compliant with platform policies.
This allows:
  • Adult/NSFW image generation
  • Mature story themes
  • Access to mature presets (when enabled server-side)
)} {/* Main Wizard Content */}
{/* Wizard Header */}
New Project
{/* Horizontal Stepper */}
canNav(0) && goTo(0)} totalSteps={6}/> canNav(1) && goTo(1)} totalSteps={6}/> canNav(2) && goTo(2)} totalSteps={6}/> canNav(3) && goTo(3)} totalSteps={6}/> canNav(4) && goTo(4)} totalSteps={6}/> canNav(5) && goTo(5)} totalSteps={6}/>
{/* Content Area */}
{/* STEP 0: Project Type */} {step === 0 && (

Choose Your Project Type

Select the type of content you want to create. This determines how your scenes will be generated and played back.

{/* Video Project */} handleProjectTypeSelect("video")}/> {/* Slideshow */} handleProjectTypeSelect("slideshow")}/> {/* Video Series */} handleProjectTypeSelect("video_series")}/>
All project types include:
AI Narration (TTS) TV Mode Playback Scene-by-Scene Editing Export to Video
)} {/* STEP 1: Details */} {step === 1 && (

Details

Give your project a title and choose the format.

setTitle(e.target.value)}/>

Describe what topic or subject this video will cover. This helps the AI generate better content.