auto-save 2026-05-25 10:27 (~2)

This commit is contained in:
2026-05-25 10:27:52 +08:00
parent 04d80c133a
commit 976b318432
2 changed files with 200 additions and 365 deletions

View File

@@ -1,11 +1,5 @@
{ {
"entries": [ "entries": [
{
"files_changed": 2,
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 2 项未提交变更 · 最近提交auto-save 2026-05-19 20:21 (~4)",
"ts": "2026-05-19T12:24:39Z",
"type": "session-heartbeat"
},
{ {
"files_changed": 2, "files_changed": 2,
"hash": "00872db", "hash": "00872db",
@@ -3199,6 +3193,13 @@
"type": "session-end", "type": "session-end",
"message": "Codex 会话结束 · 持续 0 秒 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交fix: normalize media prompts and patent views", "message": "Codex 会话结束 · 持续 0 秒 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交fix: normalize media prompts and patent views",
"files_changed": 1 "files_changed": 1
},
{
"ts": "2026-05-25T10:16:59+08:00",
"type": "commit",
"message": "auto-save 2026-05-25 10:16 (~2)",
"hash": "04d80c1",
"files_changed": 2
} }
] ]
} }

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { import {
ArrowRight,
ArrowUp, ArrowUp,
Clapperboard, Clapperboard,
Copy, Copy,
@@ -15,7 +14,6 @@ import {
Menu, Menu,
Plus, Plus,
RefreshCw, RefreshCw,
Search,
Sparkles, Sparkles,
Upload, Upload,
Wand2, Wand2,
@@ -47,86 +45,57 @@ type BusyTask = CreationMode | "job" | null
type ModeConfig = { type ModeConfig = {
id: CreationMode id: CreationMode
label: string label: string
short: string
icon: LucideIcon icon: LucideIcon
accent: string
active: string
placeholder: string placeholder: string
} }
type InspirationCard = { type InspirationCard = {
title: string title: string
tag: string
mode: CreationMode mode: CreationMode
prompt: string prompt: string
tone: string
} }
const OUTPUT_MODES: ModeConfig[] = [ const OUTPUT_MODES: ModeConfig[] = [
{ {
id: "video", id: "video",
label: "视频", label: "视频",
short: "竖屏短片 / 产品动态",
icon: Clapperboard, icon: Clapperboard,
accent: "text-cyan-200", placeholder: "Seedance 2.0 全能参考,视频创意无限可能",
active: "border-cyan-300/60 bg-cyan-300/12 text-cyan-50",
placeholder: "做一条 15 秒 TikTok 竖屏视频,办公室午休场景,人物戴上 SKG 颈部按摩仪,镜头干净,突出日常放松和产品佩戴清楚。",
}, },
{ {
id: "image", id: "image",
label: "图片", label: "图片",
short: "营销图 / 首帧 / 产品场景",
icon: ImageIcon, icon: ImageIcon,
accent: "text-emerald-200", placeholder: "生成一张 9:16 信息流营销图SKG 颈部按摩仪佩戴清楚,真实办公室午休场景。",
active: "border-emerald-300/60 bg-emerald-300/12 text-emerald-50",
placeholder: "生成一张 9:16 信息流营销图SKG 颈部按摩仪佩戴清楚,真实办公室午休场景,画面干净,有高级感。",
}, },
{ {
id: "copy", id: "copy",
label: "图文", label: "图文",
short: "标题 / 脚本 / caption",
icon: FileText, icon: FileText,
accent: "text-orange-200", placeholder: "写一组 SKG 颈部按摩仪营销图文方案,包含 hook、脚本、caption 和生成提示词。",
active: "border-orange-300/65 bg-orange-300/12 text-orange-50",
placeholder: "写一组 SKG 颈部按摩仪的营销图文方案,面向久坐办公人群,语气真实直接,有前三秒 hook 和可用于生图/生视频的提示词。",
}, },
] ]
const PROMPT_PRESETS: InspirationCard[] = [ const PROMPT_PRESETS: InspirationCard[] = [
{ {
title: "办公室午休", title: "办公室午休",
tag: "短视频",
mode: "video", mode: "video",
prompt: "做一条 15 秒 TikTok 竖屏视频,办公室午休场景,人物放下电脑后戴上 SKG 颈部按摩仪,镜头缓慢推进,突出日常放松。", prompt: "做一条 15 秒 TikTok 竖屏视频,办公室午休场景,人物放下电脑后戴上 SKG 颈部按摩仪,镜头缓慢推进,突出日常放松。",
tone: "from-cyan-500/22 via-slate-900 to-slate-950",
}, },
{ {
title: "下班回家放松", title: "下班回家放松",
tag: "短视频",
mode: "video", mode: "video",
prompt: "做一条 12 秒竖屏短片,年轻上班族下班回家后放松肩颈,先表现疲惫,再自然戴上 SKG 产品,动作可信。", prompt: "做一条 12 秒竖屏短片,年轻上班族下班回家后放松肩颈,先表现疲惫,再自然戴上 SKG 产品,动作可信。",
tone: "from-orange-500/22 via-slate-900 to-slate-950",
}, },
{ {
title: "白底功能图", title: "白底产品功能图",
tag: "图片",
mode: "image", mode: "image",
prompt: "生成一张白底产品功能图,高级电商质感,突出 SKG 颈部按摩仪外形、佩戴方式和日常使用,产品结构不能变形。", prompt: "生成一张白底产品功能图,高级电商质感,突出 SKG 颈部按摩仪外形、佩戴方式和日常使用,产品结构不能变形。",
tone: "from-emerald-500/22 via-slate-900 to-slate-950",
}, },
{ {
title: "前三秒 Hook", title: "前三秒 Hook",
tag: "图文",
mode: "copy", mode: "copy",
prompt: "写 3 套 SKG 颈部按摩仪信息流营销图文方案,每套包含前三秒 hook、中文脚本、caption、图片提示词和视频提示词。", prompt: "写 3 套 SKG 颈部按摩仪信息流营销图文方案,每套包含前三秒 hook、中文脚本、caption、图片提示词和视频提示词。",
tone: "from-fuchsia-500/22 via-slate-900 to-slate-950",
},
{
title: "真实生活方式",
tag: "图片",
mode: "image",
prompt: "生成一张真实生活方式营销图,人物在家中沙发放松,佩戴 SKG 颈部按摩仪,光线自然,产品清晰可见。",
tone: "from-blue-500/20 via-slate-900 to-slate-950",
}, },
] ]
@@ -195,7 +164,6 @@ export default function Home() {
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const activeMode = OUTPUT_MODES.find((item) => item.id === mode) ?? OUTPUT_MODES[0] const activeMode = OUTPUT_MODES.find((item) => item.id === mode) ?? OUTPUT_MODES[0]
const ActiveIcon = activeMode.icon
const images = useMemo(() => allGeneratedImages(job), [job]) const images = useMemo(() => allGeneratedImages(job), [job])
const latestImage = latestGeneratedImage(job) const latestImage = latestGeneratedImage(job)
const latestVideo = latestGeneratedVideo(job) const latestVideo = latestGeneratedVideo(job)
@@ -401,28 +369,29 @@ export default function Home() {
return ( return (
<main className="min-h-screen bg-[#090a0f] text-white"> <main className="min-h-screen bg-[#090a0f] text-white">
<Toaster richColors position="top-center" /> <Toaster richColors position="top-center" />
<div className="grid min-h-screen grid-cols-[78px_minmax(0,1fr)]"> <div className="grid min-h-screen" style={{ gridTemplateColumns: "42px 132px minmax(0, 1fr)" }}>
<aside className="flex min-h-screen flex-col items-center border-r border-white/8 bg-[#0b0c11] px-3 py-6"> <aside className="flex min-h-screen flex-col items-center border-r border-white/6 bg-[#090a0f] py-6">
<div className="mb-24 flex h-9 w-9 items-center justify-center rounded-xl bg-cyan-400/15 text-cyan-200 ring-1 ring-cyan-200/20"> <div className="mb-[230px] flex h-7 w-7 items-center justify-center rounded-lg bg-cyan-400/12 text-cyan-200">
<Sparkles className="h-5 w-5" /> <Sparkles className="h-4 w-4" />
</div> </div>
<nav className="grid gap-7 text-[11px] text-white/58"> <nav className="grid gap-6 text-[10px] text-white/64">
<a href="#inspiration" className="group grid justify-items-center gap-1.5 transition hover:text-white"> <button type="button" className="group grid justify-items-center gap-1.5 transition hover:text-white">
<Sparkles className="h-5 w-5 text-white/80 group-hover:text-cyan-200" /> <Sparkles className="h-4 w-4 text-white/84 group-hover:text-cyan-200" />
</a> </button>
<a href="#generate" className="group grid justify-items-center gap-1.5 text-white transition"> <button type="button" className="group grid justify-items-center gap-1.5 text-white transition">
<Wand2 className="h-5 w-5 text-cyan-200" /> <Wand2 className="h-4 w-4 text-cyan-200" />
</a> </button>
<a href="#assets" className="group grid justify-items-center gap-1.5 transition hover:text-white"> <button type="button" onClick={refreshJobs} className="group grid justify-items-center gap-1.5 transition hover:text-white">
<Folder className="h-5 w-5 text-white/80 group-hover:text-cyan-200" /> <Folder className="h-4 w-4 text-white/84 group-hover:text-cyan-200" />
</a> </button>
</nav> </nav>
<div className="mt-auto grid justify-items-center gap-6 text-white/42"> <div className="mt-auto grid justify-items-center gap-5 text-white/42">
<a href="/agent/" className="grid justify-items-center gap-1 text-[10px] transition hover:text-white" title="高级复刻"> <div className="h-5 w-5 rounded-full bg-gradient-to-br from-slate-500 to-slate-800 ring-1 ring-white/12" />
<Layers3 className="h-4 w-4" /> <a href="/agent/" className="text-[10px] transition hover:text-white" title="高级复刻">
<Layers3 className="mx-auto mb-1 h-3.5 w-3.5" />
Agent Agent
</a> </a>
<button type="button" className="rounded-lg p-2 transition hover:bg-white/8 hover:text-white" aria-label="菜单"> <button type="button" className="rounded-lg p-2 transition hover:bg-white/8 hover:text-white" aria-label="菜单">
@@ -431,40 +400,81 @@ export default function Home() {
</div> </div>
</aside> </aside>
<section className="min-h-screen overflow-y-auto"> <aside className="hidden min-h-screen border-r border-white/6 bg-[#15171e] px-2 py-5 md:block">
<header className="flex items-center justify-between px-10 py-6"> <div className="mb-4 flex items-center justify-between px-1 text-xs font-semibold text-white/86">
<div className="flex items-center gap-3 text-sm text-white/52"> <span></span>
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-white text-xs font-black text-black">SKG</span> <button type="button" className="rounded-md p-1 text-white/36 transition hover:bg-white/8 hover:text-white" aria-label="收起侧栏">
<span></span> <Menu className="h-3.5 w-3.5" />
</div> </button>
<div className="flex items-center gap-2"> </div>
<div className="grid gap-2">
<button
type="button"
onClick={() => {
setJob(null)
setPrompt("")
setCopyVariants([])
setReferenceFile(null)
setError("")
}}
className="flex h-8 items-center gap-2 rounded-md bg-white/8 px-2 text-left text-xs font-semibold text-white/86 transition hover:bg-white/12"
>
<Wand2 className="h-3.5 w-3.5 text-white/58" />
</button>
<button
type="button"
onClick={() => setPrompt(activeMode.placeholder)}
className="flex h-8 items-center gap-2 rounded-md px-2 text-left text-xs font-semibold text-white/58 transition hover:bg-white/8 hover:text-white"
>
<FileText className="h-3.5 w-3.5 text-white/40" />
</button>
{job ? (
<a <a
href="#assets" href={`/detail/?job=${job.id}`}
className="inline-flex h-9 items-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-4 text-sm font-semibold text-white/78 transition hover:border-white/20 hover:bg-white/[0.07]" className="mt-2 flex min-h-9 items-center gap-2 rounded-md border border-cyan-200/16 bg-cyan-300/8 px-2 text-left text-xs font-semibold text-cyan-100 transition hover:border-cyan-200/30"
> >
</a>
<a
href="/agent/"
className="inline-flex h-9 items-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-4 text-sm font-semibold text-white/78 transition hover:border-white/20 hover:bg-white/[0.07]"
>
<ExternalLink className="h-3.5 w-3.5" /> <ExternalLink className="h-3.5 w-3.5" />
</a> </a>
) : null}
</div>
<div className="mt-8 border-t border-white/6 pt-3">
<div className="mb-2 px-1 text-[11px] font-semibold text-white/34"></div>
<div className="grid gap-1.5">
{recentJobs.slice(0, 5).map((item) => (
<button
key={item.id}
type="button"
onClick={() => loadJob(item.id)}
className={cx(
"min-w-0 rounded-md px-2 py-2 text-left transition hover:bg-white/8",
job?.id === item.id ? "bg-white/8 text-white" : "text-white/42",
)}
>
<span className="block truncate text-[11px] font-semibold">{jobTitle(item)}</span>
<span className="mt-0.5 block truncate text-[10px] text-white/30">{statusLabel(item.status)} · {item.video_count} </span>
</button>
))}
</div> </div>
</header> </div>
</aside>
<div className="mx-auto grid w-full max-w-[1280px] gap-10 px-8 pb-12 pt-4"> <section className="relative min-h-screen overflow-hidden bg-[#0b0c10]">
<section id="generate" className="grid justify-items-center gap-8"> <div className="absolute inset-x-0 top-0 h-28 bg-[radial-gradient(circle_at_50%_0%,rgba(17,211,239,0.08),transparent_58%)]" />
<div className="text-center"> <div className="relative flex min-h-screen items-center justify-center px-5 py-16">
<h1 className="text-[28px] font-semibold leading-tight tracking-normal text-white sm:text-[34px]"> <section className="mb-20 grid w-full max-w-[520px] -translate-y-12 justify-items-center gap-5">
<span className="text-cyan-300">SKG </span> <h1 className="text-center text-lg font-semibold tracking-normal text-white/92"></h1>
</h1>
<p className="mt-3 text-sm text-white/44"> / / </p>
</div>
<section className="relative w-full max-w-[1010px] rounded-[28px] border border-white/8 bg-[#1b1d24] p-5 shadow-[0_30px_90px_rgba(0,0,0,0.34)]"> <section className="relative w-full rounded-2xl border border-white/7 bg-[#1d1f27] p-3 shadow-[0_28px_80px_rgba(0,0,0,0.28)]">
<div className="absolute left-7 top-7 flex h-16 w-12 -rotate-6 items-center justify-center rounded bg-[#2a2d37] text-white/42 shadow-lg"> <button
type="button"
onClick={() => fileInputRef.current?.click()}
className="absolute left-5 top-4 flex h-16 w-12 -rotate-6 items-center justify-center overflow-hidden rounded bg-[#2a2d37] text-white/34 shadow-lg transition hover:text-white"
aria-label="上传素材"
title="上传素材"
>
{currentReference ? ( {currentReference ? (
<MediaAssetTile <MediaAssetTile
src={currentReference} src={currentReference}
@@ -472,32 +482,42 @@ export default function Home() {
objectFit="cover" objectFit="cover"
previewObjectFit="contain" previewObjectFit="contain"
className="h-full w-full rounded" className="h-full w-full rounded"
onDelete={referenceFile ? () => onFileChange(null) : undefined} disablePreview={!currentReference}
/> />
) : ( ) : (
<Plus className="h-5 w-5" /> <Plus className="h-4 w-4" />
)} )}
</div> </button>
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
accept="image/png,image/jpeg,image/webp,video/mp4,video/webm,video/quicktime" accept="image/png,image/jpeg,image/webp"
className="hidden" className="hidden"
onChange={(event) => onFileChange(event.target.files?.[0] ?? null)} onChange={(event) => onFileChange(event.target.files?.[0] ?? null)}
/> />
<div className="min-h-[132px] pl-20"> <textarea
<textarea value={prompt}
value={prompt} onChange={(event) => setPrompt(event.target.value)}
onChange={(event) => setPrompt(event.target.value)} placeholder={activeMode.placeholder}
placeholder={activeMode.placeholder} className="min-h-9 w-full resize-none bg-transparent pl-16 pr-8 pt-0.5 text-sm leading-6 text-white outline-none placeholder:text-white/24"
className="h-32 w-full resize-none bg-transparent pr-4 pt-1 text-base leading-7 text-white outline-none placeholder:text-white/28" />
/>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-white/8 pt-4"> {referenceFile ? (
<div className="flex flex-wrap items-center gap-2"> <button
type="button"
onClick={() => onFileChange(null)}
className="absolute right-4 top-4 rounded-lg p-1 text-white/34 transition hover:bg-white/8 hover:text-white"
aria-label="移除素材"
title="移除素材"
>
<X className="h-3.5 w-3.5" />
</button>
) : null}
<div className="mt-1 flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-2">
{OUTPUT_MODES.map((item) => { {OUTPUT_MODES.map((item) => {
const Icon = item.icon const Icon = item.icon
const selected = item.id === mode const selected = item.id === mode
@@ -507,74 +527,48 @@ export default function Home() {
type="button" type="button"
onClick={() => setMode(item.id)} onClick={() => setMode(item.id)}
className={cx( className={cx(
"inline-flex h-9 items-center gap-2 rounded-xl border px-3 text-sm font-semibold transition focus:outline-none focus:ring-2 focus:ring-cyan-200/25", "inline-flex h-8 items-center gap-1.5 rounded-lg border px-2.5 text-xs font-semibold transition",
selected ? item.active : "border-white/8 bg-black/16 text-white/62 hover:border-white/18 hover:bg-white/[0.06] hover:text-white", selected ? "border-cyan-300/24 bg-cyan-300/10 text-cyan-200" : "border-white/7 bg-black/14 text-white/48 hover:border-white/14 hover:text-white",
)} )}
> >
<Icon className={cx("h-4 w-4", selected ? item.accent : "text-white/44")} /> <Icon className="h-3.5 w-3.5" />
{item.label} {item.label}
</button> </button>
) )
})} })}
<button <button
type="button" type="button"
onClick={() => fileInputRef.current?.click()} onClick={() => setShowSettings((value) => !value)}
className="inline-flex h-9 items-center gap-2 rounded-xl border border-white/8 bg-black/16 px-3 text-sm font-semibold text-white/62 transition hover:border-white/18 hover:bg-white/[0.06] hover:text-white" className="inline-flex h-8 items-center rounded-lg border border-white/7 bg-black/14 px-2.5 text-xs font-semibold text-white/48 transition hover:border-white/14 hover:text-white"
> >
<Upload className="h-4 w-4" />
{referenceFile ? referenceFile.name.slice(0, 18) : "上传素材"} </button>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-white/7 bg-black/14 px-2.5 text-xs font-semibold text-white/48 transition hover:border-white/14 hover:text-white"
>
<Upload className="h-3.5 w-3.5" />
{referenceFile ? "已上传" : "参考"}
</button> </button>
{referenceFile ? (
<button
type="button"
onClick={() => onFileChange(null)}
className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-white/8 bg-black/16 text-white/52 transition hover:border-rose-200/30 hover:text-rose-100"
aria-label="移除素材"
title="移除素材"
>
<X className="h-4 w-4" />
</button>
) : null}
</div> </div>
<button <button
type="button" type="button"
onClick={runPrimary} onClick={runPrimary}
disabled={!!busy} disabled={!!busy}
className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-[#3d4252] text-white transition hover:bg-cyan-400 hover:text-black disabled:cursor-not-allowed disabled:opacity-60" className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#424757] text-white/76 transition hover:bg-cyan-300 hover:text-black disabled:cursor-not-allowed disabled:opacity-60"
aria-label="开始生成" aria-label="开始生成"
title="开始生成" title="开始生成"
> >
{busy === mode || busy === "job" ? <Loader2 className="h-5 w-5 animate-spin" /> : <ArrowUp className="h-5 w-5" />} {busy === mode || busy === "job" ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-4 w-4" />}
</button> </button>
</div> </div>
</section> </section>
<div className="grid w-full max-w-[1010px] gap-3 md:grid-cols-[minmax(0,1fr)_auto]">
<div className="flex flex-wrap gap-2">
{PROMPT_PRESETS.slice(0, 4).map((item) => (
<button
key={item.title}
type="button"
onClick={() => useInspiration(item)}
className="rounded-2xl border border-white/8 bg-white/[0.04] px-4 py-3 text-left text-xs font-semibold text-white/58 transition hover:border-cyan-200/25 hover:bg-white/[0.07] hover:text-white"
>
{item.title}
</button>
))}
</div>
<button
type="button"
onClick={() => setShowSettings((value) => !value)}
className="inline-flex h-10 items-center justify-center rounded-2xl border border-white/8 bg-white/[0.04] px-4 text-sm font-semibold text-white/62 transition hover:border-white/18 hover:bg-white/[0.07] hover:text-white"
>
{showSettings ? "收起设置" : "默认设置"}
</button>
</div>
{showSettings ? ( {showSettings ? (
<section className="grid w-full max-w-[1010px] gap-3 rounded-3xl border border-white/8 bg-white/[0.04] p-4 md:grid-cols-2 lg:grid-cols-5"> <section className="grid w-full gap-3 rounded-2xl border border-white/7 bg-[#171922] p-3 md:grid-cols-2">
<label className="grid gap-1.5 lg:col-span-1"> <label className="grid gap-1.5">
<span className="text-xs font-medium text-white/42"></span> <span className="text-xs font-medium text-white/42"></span>
<input <input
value={product} value={product}
@@ -582,7 +576,7 @@ export default function Home() {
className="h-10 rounded-xl border border-white/10 bg-black/20 px-3 text-sm text-white outline-none focus:border-cyan-200/40" className="h-10 rounded-xl border border-white/10 bg-black/20 px-3 text-sm text-white outline-none focus:border-cyan-200/40"
/> />
</label> </label>
<label className="grid gap-1.5 lg:col-span-2"> <label className="grid gap-1.5">
<span className="text-xs font-medium text-white/42"></span> <span className="text-xs font-medium text-white/42"></span>
<input <input
value={audience} value={audience}
@@ -610,7 +604,7 @@ export default function Home() {
{[8, 12, 15, 20, 30, 45].map((value) => <option key={value} value={value}>{value} </option>)} {[8, 12, 15, 20, 30, 45].map((value) => <option key={value} value={value}>{value} </option>)}
</select> </select>
</label> </label>
<label className="grid gap-1.5 md:col-span-2 lg:col-span-5"> <label className="grid gap-1.5 md:col-span-2">
<span className="text-xs font-medium text-white/42"></span> <span className="text-xs font-medium text-white/42"></span>
<input <input
value={tone} value={tone}
@@ -622,235 +616,75 @@ export default function Home() {
) : null} ) : null}
{error ? ( {error ? (
<div className="w-full max-w-[1010px] rounded-2xl border border-rose-300/25 bg-rose-500/10 px-4 py-3 text-sm text-rose-100">{error}</div> <div className="w-full rounded-2xl border border-rose-300/25 bg-rose-500/10 px-4 py-3 text-sm text-rose-100">{error}</div>
) : null} ) : null}
</section>
<section className="grid gap-4"> <div className="mt-1 flex w-full flex-wrap justify-center gap-2">
<div className="grid gap-3 md:grid-cols-5"> {PROMPT_PRESETS.map((item) => (
{recentJobs.slice(0, 5).map((item) => (
<button
key={item.id}
type="button"
onClick={() => loadJob(item.id)}
className={cx(
"grid h-[74px] grid-cols-[68px_minmax(0,1fr)] items-center gap-3 rounded-3xl border bg-white/[0.04] p-2 text-left transition hover:border-cyan-200/24 hover:bg-white/[0.07]",
job?.id === item.id ? "border-cyan-200/40" : "border-white/6",
)}
>
<MediaAssetTile
src={item.thumbnail ? apiAssetUrl(item.thumbnail) : undefined}
alt=""
objectFit="cover"
className="h-[56px] w-[68px] rounded-2xl"
disablePreview={!item.thumbnail}
/>
<span className="min-w-0">
<span className="block truncate text-xs font-semibold text-white/74">{jobTitle(item)}</span>
<span className="mt-1 block truncate text-[11px] text-white/34">{statusLabel(item.status)} · {item.frame_count} · {item.video_count} </span>
</span>
</button>
))}
{!recentJobs.length ? Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="h-[74px] rounded-3xl border border-white/6 bg-white/[0.04] p-3">
<div className="mb-2 h-4 w-16 rounded bg-white/8" />
<div className="h-3 w-24 rounded bg-white/6" />
</div>
)) : null}
</div>
</section>
<section id="assets" className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_360px]">
<div className="rounded-[28px] border border-white/8 bg-[#14161d] p-4">
<div className="mb-4 flex items-center justify-between gap-3">
<div>
<h2 className="text-base font-semibold"></h2>
<p className="mt-1 text-xs text-white/38">{job ? jobTitle(job) : "生成后只展示最新结果,全部内容进详情页整理。"}</p>
</div>
{job ? (
<a
href={`/detail/?job=${job.id}`}
className="inline-flex h-9 items-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-3 text-sm font-semibold text-white/74 transition hover:border-cyan-200/24 hover:bg-white/[0.07]"
>
<ArrowRight className="h-3.5 w-3.5" />
</a>
) : null}
</div>
<div className="grid gap-3 md:grid-cols-3">
<div className="rounded-3xl border border-white/8 bg-black/18 p-3">
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-white/56">
<ImageIcon className="h-3.5 w-3.5" />
</div>
{latestImage ? (
<MediaAssetTile
src={apiAssetUrl(latestImage.url)}
alt="generated image"
objectFit="cover"
previewObjectFit="contain"
className="aspect-[4/5] w-full rounded-2xl"
label={latestImage.model}
meta={latestImage.mode}
onDelete={() => deleteImage(latestImage)}
/>
) : (
<div className="flex aspect-[4/5] items-center justify-center rounded-2xl border border-dashed border-white/10 bg-white/[0.03] text-sm text-white/28"></div>
)}
</div>
<div className="rounded-3xl border border-white/8 bg-black/18 p-3">
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-white/56">
<Clapperboard className="h-3.5 w-3.5" />
</div>
{latestVideo && job ? (
<div className="grid gap-2">
<MediaAssetTile
kind="video"
src={latestVideo.status === "completed" ? videoSrc(job, latestVideo) : undefined}
poster={apiAssetUrl(latestVideo.poster_url)}
objectFit="cover"
previewObjectFit="contain"
className="aspect-[4/5] w-full rounded-2xl"
label={latestVideo.model}
meta={`${latestVideo.status} · ${Math.round(latestVideo.progress)}%`}
busy={latestVideo.status === "queued" || latestVideo.status === "in_progress"}
onDelete={() => deleteVideo(latestVideo)}
/>
<div className="h-1 overflow-hidden rounded-full bg-white/8">
<div className="h-full rounded-full bg-cyan-300" style={{ width: `${Math.max(4, latestVideo.progress)}%` }} />
</div>
</div>
) : (
<div className="flex aspect-[4/5] items-center justify-center rounded-2xl border border-dashed border-white/10 bg-white/[0.03] text-sm text-white/28"></div>
)}
</div>
<div className="rounded-3xl border border-white/8 bg-black/18 p-3">
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-white/56">
<FileText className="h-3.5 w-3.5" />
</div>
{firstCopy ? (
<article className="grid min-h-[320px] content-start gap-3 rounded-2xl border border-white/8 bg-white/[0.04] p-4">
<div className="flex items-start justify-between gap-2">
<h3 className="text-sm font-semibold text-white">{firstCopy.title || "营销方案"}</h3>
<button
type="button"
onClick={() => copyText([firstCopy.hook_zh, firstCopy.script_zh, firstCopy.caption_zh].filter(Boolean).join("\n\n"))}
className="rounded-lg p-1 text-white/42 transition hover:bg-white/8 hover:text-white"
aria-label="复制文案"
title="复制文案"
>
<Copy className="h-4 w-4" />
</button>
</div>
<p className="text-sm leading-6 text-white/72">{firstCopy.hook_zh}</p>
<p className="line-clamp-[8] whitespace-pre-wrap text-xs leading-5 text-white/44">{firstCopy.script_zh}</p>
<div className="mt-auto grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => useVariant(firstCopy, "image")}
className="inline-flex h-9 items-center justify-center gap-2 rounded-xl bg-emerald-300/14 text-xs font-semibold text-emerald-100 transition hover:bg-emerald-300/20"
>
</button>
<button
type="button"
onClick={() => useVariant(firstCopy, "video")}
className="inline-flex h-9 items-center justify-center gap-2 rounded-xl bg-cyan-300/14 text-xs font-semibold text-cyan-100 transition hover:bg-cyan-300/20"
>
</button>
</div>
</article>
) : (
<div className="flex aspect-[4/5] items-center justify-center rounded-2xl border border-dashed border-white/10 bg-white/[0.03] text-sm text-white/28"></div>
)}
</div>
</div>
</div>
<aside className="rounded-[28px] border border-white/8 bg-[#14161d] p-4">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-base font-semibold"></h2>
<button
type="button"
onClick={refreshJobs}
className="rounded-xl p-2 text-white/42 transition hover:bg-white/8 hover:text-white"
aria-label="刷新任务"
title="刷新任务"
>
<RefreshCw className="h-4 w-4" />
</button>
</div>
<div className="grid max-h-[430px] gap-2 overflow-y-auto pr-1">
{recentJobs.length ? recentJobs.map((item) => (
<button
key={item.id}
type="button"
onClick={() => loadJob(item.id)}
className={cx(
"grid grid-cols-[54px_minmax(0,1fr)] gap-3 rounded-2xl border bg-white/[0.04] p-2 text-left transition hover:border-cyan-200/24 hover:bg-white/[0.07]",
job?.id === item.id ? "border-cyan-200/38" : "border-white/6",
)}
>
<MediaAssetTile
src={item.thumbnail ? apiAssetUrl(item.thumbnail) : undefined}
alt=""
objectFit="cover"
className="aspect-square rounded-xl"
disablePreview={!item.thumbnail}
/>
<span className="min-w-0 self-center">
<span className="block truncate text-sm font-semibold text-white/76">{jobTitle(item)}</span>
<span className="mt-1 block text-xs text-white/34">{statusLabel(item.status)} · {item.frame_count} · {item.video_count} </span>
</span>
</button>
)) : (
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-4 py-8 text-center text-sm text-white/30"></div>
)}
</div>
</aside>
</section>
<section id="inspiration" className="grid gap-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<button type="button" className="rounded-2xl bg-white/10 px-5 py-3 text-sm font-semibold text-white"></button>
<button type="button" className="rounded-2xl px-4 py-3 text-sm font-semibold text-white/42 transition hover:text-white"></button>
</div>
<div className="flex h-10 w-full max-w-[330px] items-center gap-2 rounded-xl border border-white/10 bg-black/18 px-3 text-white/42">
<Search className="h-4 w-4" />
<span className="text-sm"></span>
</div>
</div>
<div className="grid auto-rows-[190px] grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
{PROMPT_PRESETS.map((item, index) => (
<button <button
key={item.title} key={item.title}
type="button" type="button"
onClick={() => useInspiration(item)} onClick={() => useInspiration(item)}
className={cx( className="rounded-xl border border-white/7 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-white/38 transition hover:border-cyan-200/22 hover:text-white"
"group relative overflow-hidden rounded-[28px] border border-white/8 bg-gradient-to-br p-5 text-left transition hover:-translate-y-0.5 hover:border-cyan-200/24",
item.tone,
index === 0 ? "lg:col-span-2 lg:row-span-2" : "",
)}
> >
<div className="absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-black/74 to-transparent" /> {item.title}
<div className="relative z-10 flex h-full flex-col justify-between">
<span className="w-fit rounded-full border border-white/10 bg-black/24 px-3 py-1 text-xs font-semibold text-white/64">{item.tag}</span>
<div>
<h3 className="text-lg font-semibold text-white">{item.title}</h3>
<p className="mt-2 line-clamp-2 text-xs leading-5 text-white/52">{item.prompt}</p>
</div>
</div>
</button> </button>
))} ))}
</div> </div>
{(latestImage || latestVideo || firstCopy) && (
<section className="fixed bottom-6 right-6 z-20 w-[320px] rounded-2xl border border-white/8 bg-[#171922]/95 p-3 shadow-2xl backdrop-blur">
<div className="mb-2 flex items-center justify-between gap-2">
<h2 className="text-sm font-semibold text-white/88"></h2>
{job ? <a href={`/detail/?job=${job.id}`} className="text-xs font-semibold text-cyan-200/82 hover:text-cyan-100"></a> : null}
</div>
{latestVideo && job ? (
<MediaAssetTile
kind="video"
src={latestVideo.status === "completed" ? videoSrc(job, latestVideo) : undefined}
poster={apiAssetUrl(latestVideo.poster_url)}
objectFit="cover"
previewObjectFit="contain"
className="aspect-video w-full rounded-xl"
label={latestVideo.model}
meta={`${latestVideo.status} · ${Math.round(latestVideo.progress)}%`}
busy={latestVideo.status === "queued" || latestVideo.status === "in_progress"}
onDelete={() => deleteVideo(latestVideo)}
/>
) : latestImage ? (
<MediaAssetTile
src={apiAssetUrl(latestImage.url)}
alt="generated image"
objectFit="cover"
previewObjectFit="contain"
className="aspect-video w-full rounded-xl"
label={latestImage.model}
meta={latestImage.mode}
onDelete={() => deleteImage(latestImage)}
/>
) : firstCopy ? (
<article className="rounded-xl border border-white/8 bg-white/[0.04] p-3">
<div className="flex items-start justify-between gap-2">
<h3 className="line-clamp-1 text-sm font-semibold text-white">{firstCopy.title || "营销方案"}</h3>
<button
type="button"
onClick={() => copyText([firstCopy.hook_zh, firstCopy.script_zh, firstCopy.caption_zh].filter(Boolean).join("\n\n"))}
className="rounded-lg p-1 text-white/42 transition hover:bg-white/8 hover:text-white"
aria-label="复制文案"
title="复制文案"
>
<Copy className="h-4 w-4" />
</button>
</div>
<p className="mt-2 line-clamp-3 text-xs leading-5 text-white/58">{firstCopy.hook_zh}</p>
<div className="mt-3 grid grid-cols-2 gap-2">
<button type="button" onClick={() => useVariant(firstCopy, "image")} className="rounded-lg bg-emerald-300/12 px-2 py-2 text-xs font-semibold text-emerald-100"></button>
<button type="button" onClick={() => useVariant(firstCopy, "video")} className="rounded-lg bg-cyan-300/12 px-2 py-2 text-xs font-semibold text-cyan-100"></button>
</div>
</article>
) : null}
</section>
)}
</section> </section>
</div> </div>
</section> </section>