reel-studio-2 / web /src /steps /ScriptStep.jsx
xvepkj's picture
Add automatic edit populate
7f29038
Raw
History Blame
8.43 kB
import { useEffect, useState } from 'react'
import { api } from '../api'
import { useJob, ProgressBox, ErrorBox } from '../hooks'
import PromptPanel from '../components/PromptPanel'
export default function ScriptStep({ reelData, refreshReel, reelId, projectId, setStep }) {
const reel = reelData?.reel
const script = reelData?.script
const base = `/api/projects/${projectId}/reels/${reelId}`
const [local, setLocal] = useState(script)
const [dirty, setDirty] = useState(false)
const [topic, setTopic] = useState(reel?.topic || '')
const [targetSec, setTargetSec] = useState(0) // 0 = auto (default 30–45s)
const [prompt, setPrompt] = useState(null)
const [scenePrompts, setScenePrompts] = useState({}) // {sceneNo: prompt override}
const [error, setError] = useState('')
const genJob = useJob()
const sceneJob = useJob()
useEffect(() => { setLocal(reelData?.script); setDirty(false) }, [reelData?.script])
useEffect(() => { setTopic(reel?.topic || '') }, [reel?.topic])
if (!reelId) {
return (
<div>
<h2>Script</h2>
<p className="subtitle">Select or create a reel first (left sidebar), or pick a topic on the Topics step.</p>
</div>
)
}
const mutate = (fn) => { const next = structuredClone(local); fn(next); setLocal(next); setDirty(true) }
const save = async () => {
try {
await api.patch(`${base}/script`, local)
await refreshReel()
setError('')
} catch (e) { setError(e.message) }
}
const generate = async () => {
if (!topic.trim()) { setError('Enter a topic for this reel first.'); return }
try {
await genJob.start(`${base}/script/generate`, { topic: topic.trim(), prompt, target_sec: targetSec || null })
await refreshReel()
} catch { /* shown */ }
}
const regenerateScene = async (sceneNo) => {
try {
await sceneJob.start(`${base}/script/regenerate-scene`, { scene: sceneNo, prompt: scenePrompts[sceneNo] ?? null })
await refreshReel()
} catch { /* shown */ }
}
const goMedia = async () => { if (dirty) await save(); setStep(4) }
// --- No script yet: topic + generate (with editable prompt) ---
if (!local) {
return (
<div>
<h2>Script</h2>
<p className="subtitle">Write the scene-structured script for this reel, in your references&apos; style.</p>
<ErrorBox error={error || genJob.error} onRetry={generate} />
<div className="card">
<label className="field"><span className="lbl">Topic for this reel</span>
<input type="text" value={topic} onChange={(e) => setTopic(e.target.value)}
placeholder="e.g. Why the anglerfish glows" />
</label>
<label className="field" style={{ maxWidth: 260 }}>
<span className="lbl">Target length (the narration drives the reel length)</span>
<select value={targetSec} onChange={(e) => setTargetSec(Number(e.target.value))}>
<option value={0}>Auto (30–45s)</option>
<option value={15}>~15s (quick)</option>
<option value={30}>~30s</option>
<option value={45}>~45s</option>
<option value={60}>~60s</option>
<option value={90}>~90s</option>
<option value={120}>~2 min</option>
</select>
</label>
<PromptPanel
label="scriptwriting"
value={prompt}
setValue={setPrompt}
disabled={genJob.running}
fetchPrompt={async () => (await api.post(`${base}/script/prompt`, { topic: topic.trim() })).prompt}
/>
<button className="btn" onClick={generate} disabled={genJob.running || !topic.trim()}>
{genJob.running ? 'Writing…' : 'Generate script'}
</button>
{genJob.running && <ProgressBox progress={['Writing the script in your referencesstyle…']} />}
</div>
</div>
)
}
// --- Script exists: editable scenes ---
return (
<div>
<h2>Script</h2>
<p className="subtitle">
Topic: <strong>{local.topic || local.title}</strong>
{local.clone_source && <> · cloned structure from <a href={local.clone_source} target="_blank" rel="noreferrer">source</a> (fresh wording)</>}
</p>
<ErrorBox error={error || genJob.error || sceneJob.error} />
{(genJob.running || sceneJob.running) && <ProgressBox progress={['Rewriting with the style profile…']} />}
<div className="card">
<label className="field"><span className="lbl">Title</span>
<input type="text" value={local.title} onChange={(e) => mutate((s) => { s.title = e.target.value })} />
</label>
<span className="lbl" style={{ color: 'var(--muted)', fontSize: 13 }}>Hook — pick one of 3 (spoken in the first 2 seconds)</span>
<div className="spacer" />
{local.hook_options.map((h, i) => (
<button key={i}
className={`hook-option ${local.selected_hook === i ? 'selected' : ''}`}
onClick={() => mutate((s) => { s.selected_hook = i })}>
<span className="tag" style={{ marginRight: 8 }}>{i + 1}</span>{h}
</button>
))}
</div>
{local.scenes.map((scene, i) => (
<div key={i} className="card scene-card">
<div className="row" style={{ marginBottom: 8 }}>
<strong>Scene {scene.scene}</strong>
<span className="tag">{scene.duration_sec}s</span>
<div className="grow" />
<button className="btn small secondary" disabled={sceneJob.running}
onClick={() => regenerateScene(scene.scene)}>↻ Regenerate scene</button>
</div>
<label className="field"><span className="lbl">Narration</span>
<textarea value={scene.narration}
onChange={(e) => mutate((s) => { s.scenes[i].narration = e.target.value })} />
</label>
<div className="row">
<label className="field grow"><span className="lbl">Visual query (stock footage search)</span>
<input type="text" value={scene.visual_query}
onChange={(e) => mutate((s) => { s.scenes[i].visual_query = e.target.value })} />
</label>
<label className="field grow"><span className="lbl">SFX query (optional)</span>
<input type="text" value={scene.sfx_query || ''}
onChange={(e) => mutate((s) => { s.scenes[i].sfx_query = e.target.value || null })} />
</label>
</div>
<label className="field"><span className="lbl">📌 Real event info — for finding actual footage (optional)</span>
<input type="text" value={scene.event_info || ''}
placeholder="e.g. Ronaldo bicycle kick — Juventus vs Real Madrid, UCL QF, 3 Apr 2018, Allianz Stadium"
onChange={(e) => mutate((s) => { s.scenes[i].event_info = e.target.value })} />
</label>
<PromptPanel label="scene-rewrite" disabled={sceneJob.running}
value={scenePrompts[scene.scene] ?? null}
setValue={(v) => setScenePrompts((p) => ({ ...p, [scene.scene]: v }))}
fetchPrompt={async () => (await api.post(`${base}/script/scene-prompt`, { scene: scene.scene })).prompt} />
</div>
))}
<div className="card">
<label className="field"><span className="lbl">Call to action</span>
<input type="text" value={local.cta} onChange={(e) => mutate((s) => { s.cta = e.target.value })} />
</label>
</div>
{/* Regenerate whole script — with editable prompt */}
<div className="card">
<PromptPanel
label="scriptwriting"
value={prompt}
setValue={setPrompt}
disabled={genJob.running}
fetchPrompt={async () => (await api.post(`${base}/script/prompt`, { topic: local.topic || local.title })).prompt}
/>
<button className="btn secondary" onClick={() => { setTopic(local.topic || local.title); generate() }} disabled={genJob.running}>
↻ Regenerate whole script
</button>
</div>
<div className="row">
{dirty && <button className="btn secondary" onClick={save}>Save edits</button>}
<div className="grow" />
<button className="btn" onClick={goMedia}>Continue to Media →</button>
</div>
</div>
)
}