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&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>
  )
}