SPB Git

Repository Public

page.tsx 6.1 KB 65 lines tsx

1import { randomUUID } from "node:crypto";2import { Notice, PageHead, Panel } from "@/components/studio/ui";3import { requireRole } from "@/lib/auth";4import { getPolicy, observedVideoRate } from "@/lib/budget";5import { env } from "@/lib/env";6import { SERIES } from "@/lib/taxonomy";7import { estimateImage, estimateVideo } from "@/providers/pricing";8import { usd } from "@/components/studio/ui";9import { createProject } from "../../../actions";1011export const metadata = { title: "Concept Lab" };12export default async function ConceptLab({ searchParams }: { searchParams: Promise<{ err?: string; series?: string }> }) {13  const sp = await searchParams;14  await requireRole("editor");15  const policy = await getPolicy();16  const res = env.videoResolution;17  const est = async (model: string) => { const v = estimateVideo(model, res, 6, await observedVideoRate(model, res)); const i = estimateImage(env.imageModel).usd ?? 0; return { perShot: v.usd === null ? null : v.usd + i, basis: v.basis }; };18  const [master, draft] = await Promise.all([est(env.videoModel), est(env.videoDraftModel)]);19  return (20    <>21      <PageHead title="Concept Lab" sub="Describe what the film should be about. The Creative Director returns four different pitches, scored for originality, feasibility, visual potential, clarity and artifact risk. Pitches cost a few cents; nothing expensive starts without the chosen autonomy mode allowing it." />22      <Notice err={sp.err} />23      <form action={createProject} className="grid gap-5 lg:grid-cols-[1.4fr_1fr]">24        <input type="hidden" name="nonce" value={randomUUID()} />25        <Panel title="Creative brief">26          <div className="space-y-4">27            <div><label htmlFor="theme" className="label">Theme or idea *</label><textarea id="theme" name="theme" rows={3} required minLength={3} maxLength={600} className="field mt-1" placeholder="e.g. Why a spinning coin seems to hover just before it falls" /></div>28            <div><label htmlFor="objective" className="label">Objective — what should the viewer leave with?</label><input id="objective" name="objective" maxLength={400} className="field mt-1" /></div>29            <div className="grid gap-4 sm:grid-cols-2">30              <div><label htmlFor="audience" className="label">Audience</label><input id="audience" name="audience" maxLength={200} className="field mt-1" placeholder="curious general audience" /></div>31              <div><label htmlFor="mood" className="label">Mood</label><input id="mood" name="mood" maxLength={120} className="field mt-1" placeholder="quiet, precise, a little funny" /></div>32              <div><label htmlFor="genre" className="label">Genre</label><input id="genre" name="genre" maxLength={120} className="field mt-1" /></div>33              <div><label htmlFor="seriesSlug" className="label">Series</label><select id="seriesSlug" name="seriesSlug" className="field mt-1" defaultValue={sp.series ?? ""}><option value="">Standalone film</option>{SERIES.map((s) => <option key={s.slug} value={s.slug}>{s.titleEn} ({s.kind})</option>)}</select></div>34            </div>35            <div><label htmlFor="constraints" className="label">Constraints</label><textarea id="constraints" name="constraints" rows={2} maxLength={600} className="field mt-1" placeholder="No people. One location. Must end on the object." /></div>36          </div>37        </Panel>38        <div className="space-y-5">39          <Panel title="Production">40            <div className="space-y-4">41              <div className="grid grid-cols-2 gap-4">42                <div><label htmlFor="language" className="label">Spoken language</label><select id="language" name="language" className="field mt-1"><option value="en">English</option><option value="fr">French</option><option value="bilingual">Bilingual (FR + EN)</option></select></div>43                <div><label htmlFor="shots" className="label">Shots</label><select id="shots" name="shots" className="field mt-1" defaultValue="5">{[3, 4, 5, 6].map((n) => <option key={n} value={n}>{n}</option>)}</select></div>44              </div>45              <fieldset><legend className="label">Video quality</legend>46                <label className="mt-2 flex items-start gap-3 text-sm"><input type="radio" name="quality" value="master" defaultChecked className="mt-1" /><span>Master — <span className="mono">{env.videoModel}</span><br /><span className="text-xs text-[var(--color-faint)]">≈ {usd(master.perShot)} per 6-second shot ({master.basis} price)</span></span></label>47                <label className="mt-2 flex items-start gap-3 text-sm"><input type="radio" name="quality" value="draft" className="mt-1" /><span>Draft — <span className="mono">{env.videoDraftModel}</span><br /><span className="text-xs text-[var(--color-faint)]">≈ {usd(draft.perShot)} per 6-second shot ({draft.basis} price)</span></span></label>48              </fieldset>49              <fieldset><legend className="label">Autonomy</legend>50                {([["manual", "Manual", "You start every stage yourself."], ["assisted", "Assisted", "Script, storyboard, keyframes and voice run on their own; the pipeline pauses before paid video generation."], ["autonomous", "Autonomous with budget", "Runs through to QA inside the ceilings. Release still needs your approval unless auto-publish is enabled in the policy."]] as const).map(([v, label, body]) => (51                  <label key={v} className="mt-2 flex items-start gap-3 text-sm"><input type="radio" name="mode" value={v} defaultChecked={v === policy.default_mode} className="mt-1" /><span>{label}<br /><span className="text-xs text-[var(--color-faint)]">{body}</span></span></label>52                ))}53              </fieldset>54            </div>55          </Panel>56          <Panel title="Ceilings that apply">57            <p className="text-sm text-[var(--color-muted)]">Per shot {usd(policy.per_shot_usd)} · per film {usd(policy.per_film_usd)} · per day {usd(policy.per_day_usd)} · per month {usd(policy.per_month_usd)}.</p>58            <button className="btn btn-primary mt-4 w-full" disabled={policy.kill_switch}>{policy.kill_switch ? "Kill switch is on" : "Generate pitches"}</button>59          </Panel>60        </div>61      </form>62    </>63  );64}65
Files main
Menu

Theme