Spaces:
Paused
Paused
File size: 8,433 Bytes
3d2098f 854af46 3d2098f 7f29038 3d2098f 854af46 3d2098f 7f29038 3d2098f 854af46 3d2098f 7f29038 3d2098f 2980fd0 7f29038 3d2098f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | 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' 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 references’ style…']} />}
</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>
)
}
|