import { useEffect, useState } from "react"; import { addScene, type Project, type Scene } from "../api/client"; import { formatAiEditCreditsDisplay } from "../lib/formatAiEditCredits"; /** Adding a scene costs this many AI-edit credits (mirrors the backend). */ export const ADD_SCENE_CREDIT_COST = 5; export interface AddSceneModalProps { open: boolean; onClose: () => void; project: Project; /** Total AI-edit budget available (allowance + purchased pool). */ creditsRemaining: number; /** * Whether the owner has enough AI-edit budget for one add-scene action. */ canAfford: boolean; /** * True when the viewer is a collaborator on a shared project (spending the * OWNER's credit pool). A collaborator can't fix an exhausted pool by upgrading * their own plan. */ isCollaborator?: boolean; /** The scene the new one will be inserted AFTER; null = append at the end. */ anchorScene?: Scene | null; /** Called after the add-scene job is enqueued (parent starts polling). */ onAdded: (position?: number) => void; /** Navigate to the billing page from the inline "Upgrade now" link (free viewers). */ onUpgradeNow: () => void; /** Surface an API failure (e.g. the 403 credit / 409 busy error) in the global "Oops" modal. */ onError: (err: unknown) => void; } /** * Modal to generate and insert a new AI scene. The user describes the scene; it's * inserted right after `anchorScene` (or appended when none). On submit the backend * ENQUEUES a background generation job and this closes immediately — the parent shows * a placeholder row and polls for completion. */ export default function AddSceneModal({ open, onClose, project, creditsRemaining, canAfford, isCollaborator = false, anchorScene, onAdded, onError, onUpgradeNow, }: AddSceneModalProps) { const [prompt, setPrompt] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Insert position: right after the anchor row, else append (undefined). const insertPosition = anchorScene ? anchorScene.order + 1 : undefined; // Reset the form whenever the modal (re)opens. useEffect(() => { if (open) { setPrompt(""); setError(null); setLoading(false); } }, [open]); if (!open) return null; const handleClose = () => { if (loading) return; onClose(); }; const handleSubmit = async () => { const trimmed = prompt.trim(); if (!trimmed) { setError("Please describe the scene you want to add."); return; } if (!canAfford) return; setLoading(true); setError(null); try { await addScene(project.id, trimmed, insertPosition); // Job enqueued — hand the target position to the parent (for the placeholder) // and close. Generation continues in the background. onAdded(insertPosition); onClose(); } catch (err: unknown) { // Surface API failures (403 credits / 409 busy) in the global "Oops" modal. // Close first so the error modal isn't hidden behind this dialog. onClose(); onError(err); } finally { setLoading(false); } }; return (
e.stopPropagation()} role="dialog" aria-labelledby="add-scene-title" >

Add a scene

Describe the scene — it's generated from your prompt and the rest of the video.

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

{error && (

{error}

)}