import { useEffect, useMemo, useState } from "react"; import type { Project, Scene } from "../api/client"; import { generateSceneImage } from "../api/client"; import { formatAiEditCreditsDisplay } from "../lib/formatAiEditCredits"; // AI image generation costs this many AI-edit credits per image (all plans). // Kept in sync with the backend GENERATE_IMAGE_CREDIT_COST. export const AI_IMAGE_CREDIT_COST = 3; export interface GenerateSceneImageModalProps { open: boolean; onClose: () => void; scene: Scene; project: Project; /** * Whether the owner has enough AI-edit budget for one generation (monthly * allowance + purchased pool). */ canGenerate: boolean; /** Credits one generation costs (shown in the modal for every plan). */ creditCost: number; /** * True when the viewer is a collaborator on a shared project (spending the OWNER's * credit pool). They can't fix an exhausted pool by upgrading their own plan. */ isCollaborator?: boolean; /** Total AI-edit budget available (allowance + purchased pool). */ creditsRemaining: number; /** True when the blocker is the OWNER's exhausted access, not the viewer's own. */ ownerBlocked?: boolean; onUpgrade: () => void; /** Navigate to the billing page from the inline "Upgrade now" link. */ onUpgradeNow: () => void; /** Shown instead of the self-upgrade prompt when `ownerBlocked`. */ onOwnerBlocked?: () => void; onImageReady: (imageBase64: string, refinedPrompt: string) => void; onGenerateStart?: () => void; onGenerateError?: (message: string) => void; } export default function GenerateSceneImageModal({ open, onClose, scene, project, canGenerate, creditCost, isCollaborator = false, creditsRemaining, ownerBlocked, onUpgrade, onUpgradeNow, onOwnerBlocked, onImageReady, onGenerateStart, onGenerateError, }: GenerateSceneImageModalProps) { const [imageDescription, setImageDescription] = useState(""); const [error, setError] = useState(null); const [generating, setGenerating] = useState(false); const placeholderHint = useMemo(() => { const v = (scene.visual_description || "").trim(); if (!v) return "Describe the image you want (subject, style, mood, colors)…"; const first = v.split("\n").find((line) => line.trim())?.trim() ?? v; return first.length > 120 ? `${first.slice(0, 120)}…` : first; }, [scene.visual_description]); useEffect(() => { if (!open) return; setImageDescription(""); setError(null); setGenerating(false); }, [open, scene.id]); if (!open) return null; // A collaborator blocked by the owner's free plan can't act on an upgrade prompt. const promptForAccess = () => { if (ownerBlocked) onOwnerBlocked?.(); else onUpgrade(); }; const handleGenerate = async () => { const desc = imageDescription.trim(); if (desc.length < 3) { setError("Image description must be at least 3 characters."); return; } if (!canGenerate) { promptForAccess(); return; } // Keep the modal open and show a generating state so the user sees progress and // any error, instead of the modal vanishing with no feedback. setError(null); setGenerating(true); onGenerateStart?.(); try { const res = await generateSceneImage(project.id, scene.id, { image_description: desc, }); // Hand the result to the parent (which shows the keep/discard preview) and close. onImageReady(res.data.image_base64, res.data.refined_prompt); onClose(); } catch (err: unknown) { const status = err && typeof err === "object" && "response" in err ? (err as { response?: { status?: number } }).response?.status : 0; if (status === 403) { promptForAccess(); return; } const msg = err && typeof err === "object" && "response" in err ? (err as { response?: { data?: { detail?: string } } }).response?.data ?.detail : "Image generation failed"; setError(String(msg || "Image generation failed")); onGenerateError?.(String(msg || "Image generation failed")); } finally { setGenerating(false); } }; return (
{ if (!generating) onClose(); }} aria-hidden />
e.stopPropagation()} role="dialog" aria-labelledby="generate-scene-image-title" >

Generate image with AI

Describe the image you want. Your description is the main input.

Costs{" "} {creditCost} AI edit credits {isCollaborator ? ( <> {" "} from the project owner's balance ( {formatAiEditCreditsDisplay(creditsRemaining)} remaining). ) : ( <> {" "} · {formatAiEditCreditsDisplay(creditsRemaining)} remaining {!canGenerate && ( <> {" "} ·{" "} )} )}

{generating ? (

Generating image…

This can take up to a minute. Please keep this open.

) : (