2 Commits

Author SHA1 Message Date
c805012adc feat: restyle workbench with glassmorphism shell 2026-05-21 10:37:54 +08:00
536b4d7f59 auto-save 2026-05-21 02:09 (~2) 2026-05-21 02:09:07 +08:00
5 changed files with 2620 additions and 2440 deletions

View File

@@ -1,6 +1,6 @@
# 项目接力 # 项目接力
- 生成时间May 20, 2026 at 16:25 - 生成时间May 21, 2026 at 02:06
- 项目SKG Marketing Studio / SKG 营销内容工作台 - 项目SKG Marketing Studio / SKG 营销内容工作台
- 路径:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证 - 路径:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证
- 状态active - 状态active
@@ -9,7 +9,7 @@
## 最近助手会话概览 ## 最近助手会话概览
- Claudea9e0449c-d9cb-4a2a-bb16-16596dfb552a · 时间未知 - Claudea9e0449c-d9cb-4a2a-bb16-16596dfb552a · 时间未知
- Codex019e3db1-012e-7163-bc78-acf7cde326e7 · 时间未知 - Codex019e447d-68c7-7db1-a499-b5eb6a98a7c2 · 时间未知
- Cursor未找到匹配当前项目的最近会话 - Cursor未找到匹配当前项目的最近会话
## Claude 最近会话 ## Claude 最近会话
@@ -92,27 +92,42 @@
## Codex 最近会话 ## Codex 最近会话
- Session ID019e3db1-012e-7163-bc78-acf7cde326e7 - Session ID019e447d-68c7-7db1-a499-b5eb6a98a7c2
- Transcript/Users/kangwan/.codex/sessions/2026/05/19/rollout-2026-05-19T08-44-30-019e3db1-012e-7163-bc78-acf7cde326e7.jsonl - Transcript/Users/kangwan/.codex/sessions/2026/05/20/rollout-2026-05-20T16-25-30-019e447d-68c7-7db1-a499-b5eb6a98a7c2.jsonl
- 工作目录:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证 - 工作目录:/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证
- 分支main - 分支main
- 敏感字段:已对 token / key / password / secret 做脱敏 - 敏感字段:已对 token / key / password / secret 做脱敏
### 最近用户要求 ### 最近用户要求
-之前的案例呢 我更新一次就清空了么 -看的没改变呀 我感觉这个已经彻底调整崩了
- 怎么避免这样的事情出现,我都快炸了 - figma mcp是通的么
- 我要开新的窗口 继续这个项目了 你帮我打开 - 连一些被 我都打开
### Codex 最近回复 ### Codex 最近回复
> 这个旧路径不存在,我查一下当前机器上工作看板实际装在哪里,然后打开它 > 现在命令行访问本机端口被沙箱拦了一次,`nc` 明确返回 `Operation not permitted`。我用已授权外部执行再测一次这样能区分“Figma 没开”和“Codex 沙箱不让连本地端口”
> 打开工作看板 > 连上本地 Figma Desktop MCP
> >
> 新窗口继续这个项目时,项目根目录用 > 我做了这些验证
> >
> `/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证` > - Figma 桌面端已打开 Design 文件,并显示 `MCP server enabled on http://127.0.0.1:3845/mcp`
> - 本机端口确认监听:`Figma ... 127.0.0.1:3845 (LISTEN)`
> - MCP `initialize` 成功返回:`Figma Dev Mode MCP Server 1.0.0`
> - 工具列表可用:`get_design_context`、`get_screenshot`、`get_metadata`、`get_variable_defs`
> - 实际调用 `get_metadata` 成功,返回当前文件页面:`0:1: Page 1`
>
> 注意:官方远程 `figma` 那条还没通,因为当前环境没有 `FIGMA_OAUTH_TOKEN``codex mcp login figma` 也返回了 `invalid_redirect_uri`。但本地 `figma-desktop` 这条已经能用。Figma 官方文档也是这个流程:打开桌面端 Design 文件,启用本地 MCP然后配置 `http://127.0.0.1:3845/mcp`。
>
> 当前仓库没改代码,状态还是:
>
> ```text
> ## main...origin/main
> M .memory/worklog.json
> ```
>
> 来源Figma 官方 Codex MCP 设置文档 https://help.figma.com/hc/en-us/articles/39888629089175-Codex-and-Figma-Set-up-the-MCP-server
## Cursor 最近会话 ## Cursor 最近会话

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -469,29 +469,33 @@ nextjs-portal {
信息流工作台 · 登录页同源质感 信息流工作台 · 登录页同源质感
============================================================ */ ============================================================ */
.skg-board-theme { .skg-board-theme {
--skg-gold-1: #d6b36a; --skg-gold-1: #c8cd19;
--skg-gold-2: #c89b3c; --skg-gold-2: #a2c638;
--skg-cream: #f5efe3; --skg-cream: #f6f6ee;
--skg-bg-1: #0a0a0a; --skg-bg-1: #1b1b1b;
--skg-bg-2: #111111; --skg-bg-2: #242424;
--skg-bg-3: rgba(255, 255, 255, 0.035); --skg-bg-3: rgba(255, 255, 255, 0.1);
--skg-border: rgba(255, 255, 255, 0.1); --skg-border: rgba(255, 255, 255, 0.14);
--skg-text-1: #ffffff; --skg-text-1: #ffffff;
--skg-text-2: rgba(255, 255, 255, 0.62); --skg-text-2: rgba(255, 255, 255, 0.56);
--skg-text-3: rgba(255, 255, 255, 0.34); --skg-text-3: rgba(255, 255, 255, 0.36);
--skg-success: #34d399; --skg-success: #a2c638;
--skg-warn: #fcd34d; --skg-warn: #c8cd19;
--skg-danger: #fb7185; --skg-danger: #fb7185;
--skg-info: #67e8f9; --skg-info: #a6d533;
--skg-radius-sm: 6px; --skg-radius-sm: 6px;
--skg-radius-md: 8px; --skg-radius-md: 8px;
--skg-radius-lg: 12px; --skg-radius-lg: 20px;
--skg-shadow-button: 0 6px 24px -8px rgba(0, 0, 0, 0.45); --skg-shadow-button: 10px 10px 10px rgba(0, 0, 0, 0.3);
--skg-shadow-card: 10px 10px 10px rgba(0, 0, 0, 0.3);
--skg-glass-bg: rgba(255, 255, 255, 0.1);
--skg-glass-bg-soft: rgba(255, 255, 255, 0.055);
--skg-rail: #383838;
color: var(--skg-text-1); color: var(--skg-text-1);
background: background:
radial-gradient(circle at 52% 4%, rgba(214, 179, 106, 0.1), transparent 30%), radial-gradient(circle at 20% 18%, rgba(162, 198, 56, 0.11), transparent 28%),
radial-gradient(circle at 12% 96%, rgba(214, 179, 106, 0.065), transparent 34%), radial-gradient(circle at 86% 78%, rgba(200, 205, 25, 0.12), transparent 28%),
linear-gradient(120deg, #0a0a0a 0%, #10100f 46%, #050505 100%); linear-gradient(120deg, #202020 0%, #242424 48%, #171717 100%);
} }
.skg-board-theme::before { .skg-board-theme::before {
@@ -501,10 +505,10 @@ nextjs-portal {
z-index: 0; z-index: 0;
pointer-events: none; pointer-events: none;
background: background:
linear-gradient(90deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px),
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 1px, transparent 1px); linear-gradient(180deg, rgba(255, 255, 255, 0.016) 1px, transparent 1px);
background-size: 64px 64px; background-size: 56px 56px;
opacity: 0.44; opacity: 0.34;
} }
.skg-board-theme::after { .skg-board-theme::after {
@@ -514,39 +518,38 @@ nextjs-portal {
z-index: 0; z-index: 0;
pointer-events: none; pointer-events: none;
background: background:
linear-gradient(180deg, rgba(0, 0, 0, 0.22), transparent 42%, rgba(0, 0, 0, 0.4)), linear-gradient(180deg, rgba(0, 0, 0, 0.18), transparent 45%, rgba(0, 0, 0, 0.38)),
linear-gradient(90deg, rgba(0, 0, 0, 0.28), transparent 38%, rgba(0, 0, 0, 0.24)); linear-gradient(90deg, rgba(0, 0, 0, 0.34), transparent 36%, rgba(0, 0, 0, 0.2));
} }
.skg-board-ambient { .skg-board-ambient {
background: background:
radial-gradient(circle at 78% 0%, rgba(232, 201, 122, 0.08), transparent 30%), radial-gradient(circle at 72% 12%, rgba(162, 198, 56, 0.13), transparent 28%),
radial-gradient(circle at 8% 100%, rgba(214, 179, 106, 0.06), transparent 34%); radial-gradient(circle at 18% 92%, rgba(200, 205, 25, 0.12), transparent 32%);
} }
.skg-board-topbar, .skg-board-topbar,
.skg-board-panel { .skg-board-panel {
border-color: var(--skg-border) !important; border-color: var(--skg-border) !important;
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.022)), radial-gradient(circle at 88% 22%, rgba(162, 198, 56, 0.08), transparent 38%),
rgba(17, 17, 17, 0.74) !important; linear-gradient(180deg, rgba(255, 255, 255, 0.105), rgba(255, 255, 255, 0.052)),
box-shadow: rgba(36, 36, 36, 0.68) !important;
inset 0 1px 0 rgba(255, 255, 255, 0.06), box-shadow: var(--skg-shadow-card);
0 18px 54px rgba(0, 0, 0, 0.34); backdrop-filter: blur(5px);
backdrop-filter: blur(10px);
} }
.skg-board-topbar { .skg-board-topbar {
background: background:
linear-gradient(100deg, rgba(214, 179, 106, 0.075), rgba(255, 255, 255, 0.03) 54%, rgba(214, 179, 106, 0.035)), linear-gradient(100deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.045) 58%, rgba(162, 198, 56, 0.08)),
rgba(12, 12, 12, 0.76) !important; rgba(38, 38, 38, 0.72) !important;
} }
.skg-board-theme input:focus, .skg-board-theme input:focus,
.skg-board-theme textarea:focus, .skg-board-theme textarea:focus,
.skg-board-theme select:focus { .skg-board-theme select:focus {
border-color: rgba(214, 179, 106, 0.58) !important; border-color: rgba(214, 179, 106, 0.58) !important;
box-shadow: 0 0 0 2px rgba(214, 179, 106, 0.14); box-shadow: 0 0 0 2px rgba(162, 198, 56, 0.18);
} }
.skg-board-theme input[type="checkbox"] { .skg-board-theme input[type="checkbox"] {
@@ -558,6 +561,80 @@ nextjs-portal {
color: #fff; color: #fff;
} }
.skg-board-shell {
min-height: calc(100vh - 32px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 24px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.032), rgba(255, 255, 255, 0.012)),
rgba(26, 26, 26, 0.58);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.38);
backdrop-filter: blur(8px);
}
.skg-board-rail {
width: 65px;
min-height: 514px;
border: 1px solid #383838;
border-radius: 0 70px 70px 0;
background: #383838;
box-shadow: 10px 10px 10px rgba(0, 0, 0, 0.3);
}
.skg-board-rail__logo {
border: 2px solid rgba(255, 255, 255, 0.2);
background:
radial-gradient(circle at 68% 38%, #a2c638 0 34%, transparent 36%),
radial-gradient(circle at 50% 50%, #c8cd19 0 47%, transparent 49%),
#ffffff;
color: #ffffff;
}
.skg-board-rail__button {
color: rgba(255, 255, 255, 0.52);
transition: color 180ms ease, background 180ms ease, transform 180ms ease;
}
.skg-board-rail__button:hover,
.skg-board-rail__button:focus-visible,
.skg-board-rail__button.is-active {
color: #ffffff;
background: rgba(255, 255, 255, 0.08);
transform: translateX(2px);
}
.skg-glass-card {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 20px;
background:
radial-gradient(circle at 80% 86%, rgba(162, 198, 56, 0.18), transparent 36%),
linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.052)),
rgba(56, 56, 56, 0.62);
box-shadow: var(--skg-shadow-card);
backdrop-filter: blur(5px);
}
.skg-glass-card--flat {
border-radius: 16px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.035)),
rgba(24, 24, 24, 0.52);
}
.skg-status-orb {
display: inline-flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.2);
border-radius: 999px;
background:
radial-gradient(circle at 78% 32%, #a2c638 0 12%, transparent 13%),
conic-gradient(from 40deg, #a2c638 0 74%, rgba(255, 255, 255, 0.22) 75% 100%);
color: #ffffff;
}
.skg-board-theme--light { .skg-board-theme--light {
--skg-bg-1: #faf8f4; --skg-bg-1: #faf8f4;
--skg-bg-2: #ffffff; --skg-bg-2: #ffffff;
@@ -748,43 +825,46 @@ nextjs-portal {
} }
.skg-stat-card { .skg-stat-card {
border: 1px solid rgba(214, 179, 106, 0.18); border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--skg-radius-md); border-radius: 12px;
background: var(--skg-cream); background:
color: #0a0a0a; linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.045)),
box-shadow: var(--skg-shadow-button); rgba(0, 0, 0, 0.16);
color: #ffffff;
box-shadow: 8px 8px 10px rgba(0, 0, 0, 0.22);
backdrop-filter: blur(5px);
} }
.skg-stat-card__label { .skg-stat-card__label {
color: rgba(0, 0, 0, 0.5); color: rgba(255, 255, 255, 0.48);
} }
.skg-stat-card__value { .skg-stat-card__value {
color: #0a0a0a; color: #ffffff;
} }
.skg-primary-action { .skg-primary-action {
border-radius: var(--skg-radius-md); border-radius: var(--skg-radius-md);
background: #f5efe3; background: linear-gradient(135deg, #c8cd19, #a2c638);
color: #0a0a0a; color: #101010;
box-shadow: var(--skg-shadow-button); box-shadow: var(--skg-shadow-button);
} }
.skg-primary-action:hover { .skg-primary-action:hover {
background: #fff7df; background: linear-gradient(135deg, #d6db25, #b0d83d);
} }
.skg-secondary-action { .skg-secondary-action {
border: 1px solid rgba(214, 179, 106, 0.3); border: 1px solid rgba(255, 255, 255, 0.13);
border-radius: var(--skg-radius-md); border-radius: var(--skg-radius-md);
background: rgba(214, 179, 106, 0.08); background: rgba(255, 255, 255, 0.06);
color: var(--skg-gold-1); color: rgba(255, 255, 255, 0.76);
} }
.skg-secondary-action:hover { .skg-secondary-action:hover {
border-color: rgba(214, 179, 106, 0.54); border-color: rgba(162, 198, 56, 0.44);
background: rgba(214, 179, 106, 0.12); background: rgba(162, 198, 56, 0.11);
color: #f5d98e; color: #ffffff;
} }
.skg-empty-state { .skg-empty-state {

View File

@@ -2476,108 +2476,104 @@ export function AdRecreationBoard({
return ( return (
<section className={`skg-board-theme ${boardTheme === "light" ? "skg-board-theme--light" : ""} relative z-20 min-h-screen w-full overflow-auto bg-black text-white`}> <section className={`skg-board-theme ${boardTheme === "light" ? "skg-board-theme--light" : ""} relative z-20 min-h-screen w-full overflow-auto bg-black text-white`}>
<div className="skg-board-ambient pointer-events-none fixed inset-0" /> <div className="skg-board-ambient pointer-events-none fixed inset-0" />
<div className="relative z-10 mx-auto flex min-h-screen w-full min-w-[1280px] max-w-[1920px] flex-col px-4 py-4 xl:px-5"> <div className="relative z-10 mx-auto min-h-screen w-full min-w-[1280px] max-w-[1920px] px-4 py-4 xl:px-5">
<header className="skg-board-topbar mb-3 flex items-center justify-between gap-4 rounded-lg border border-white/10 bg-white/[0.04] px-4 py-3"> <div className="skg-board-shell flex gap-4 p-4">
<div className="skg-board-brand"> <WorkbenchRail
<div className="skg-board-brand__logo-chip" aria-hidden="true">
<img className="skg-board-brand__logo" src="/skg-logo-black.svg" alt="" />
</div>
<div className="min-w-0">
<div className="skg-board-brand__system"> · </div>
<h1 className="skg-board-brand__title"> · TK </h1>
<p className="skg-board-brand__subtitle">广线</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<button
type="button"
onClick={() => setLibraryOpen(true)}
className="skg-secondary-action inline-flex h-10 items-center gap-1.5 px-3 text-[11px] font-semibold transition"
title="打开全局资源中心"
>
<BookOpen className="h-3.5 w-3.5" />
</button>
<button
type="button"
onClick={toggleBoardTheme}
className="skg-board-theme-toggle skg-secondary-action inline-flex h-10 items-center gap-1.5 px-3 text-[11px] font-semibold transition"
title={boardTheme === "dark" ? "切换到明亮模式" : "切换到暗色模式"}
>
{boardTheme === "dark" ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
{boardTheme === "dark" ? "明亮" : "暗色"}
</button>
<div className="grid min-w-[520px] grid-cols-5 gap-2 text-[11px]">
<Metric label="素材" value={`${jobs.length}`} />
<Metric label="当前" value={shortId(activeJobId)} />
<Metric label="视频" value={job?.video_url ? "ready" : "-"} />
<Metric label="文案段" value={`${transcriptCount}`} />
<Metric label="背景音" value={backgroundReady ? "ready" : "-"} />
</div>
</div>
</header>
<div className="grid min-h-0 flex-1 grid-cols-[320px_minmax(0,1fr)] gap-3">
<MaterialColumn
data={data}
step={workflow.input}
jobs={jobs}
job={job} job={job}
activeJobId={activeJobId} jobsCount={jobs.length}
url={url} audioReady={audioReady}
setUrl={setUrl} visualReady={visualReady}
fileRef={fileRef} subjectAssetCount={subjectAssetCount}
onSubmitUrl={submitUrl} generatedVideoCount={generatedVideos.length}
onStartProduction={startProduction} onOpenLibrary={() => setLibraryOpen(true)}
onToggleTheme={toggleBoardTheme}
boardTheme={boardTheme}
/> />
<section className="skg-board-panel flex min-h-0 flex-col rounded-lg border border-white/10 bg-white/[0.035] shadow-2xl"> <div className="flex min-w-0 flex-1 flex-col gap-3">
<header className="shrink-0 border-b border-white/10 p-3"> <header className="skg-board-topbar flex items-center justify-between gap-4 rounded-[20px] border border-white/10 bg-white/[0.04] px-4 py-3">
<div className="flex items-center justify-between gap-4"> <div className="min-w-0">
<div className="min-w-0"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2"> <span className="skg-status-orb shrink-0 text-[10px] font-semibold">SKG</span>
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-[#d6b36a]/18 bg-[#d6b36a]/10 text-[#f2d58a]"><Mic className="h-3.5 w-3.5" /></span> <div className="min-w-0">
<WorkflowStepBadge step={workflow.source} compact /> <div className="text-[11px] font-medium uppercase tracking-[0.28em] text-white/40">Marketing Production Console</div>
<h2 className="text-[15px] font-semibold leading-tight text-white"></h2> <h1 className="mt-1 truncate text-[22px] font-semibold leading-tight text-white">广</h1>
</div>
<div className="mt-1 truncate text-[11px] text-white/38" title={statusMessage}>
{statusMessage || "下载源视频后解析音频,再抽参考帧并生成相似主体。"}
</div> </div>
</div> </div>
<div className="flex shrink-0 items-center gap-2"> <p className="mt-2 truncate text-[12px] text-white/42"></p>
<ModelTrace trace={audioModelTrace(runtimeModels)} compact /> </div>
<ActionButton disabled={!job?.video_url || audioRunning} onClick={() => data.onTranscribeAudio?.(job?.id)}> <div className="grid min-w-[560px] grid-cols-5 gap-2 text-[11px]">
<Mic className="h-3.5 w-3.5" /> <Metric label="素材" value={`${jobs.length}`} />
<Metric label="当前" value={shortId(activeJobId)} />
</ActionButton> <Metric label="视频" value={job?.video_url ? "ready" : "-"} />
</div> <Metric label="文案段" value={`${transcriptCount}`} />
<Metric label="背景音" value={backgroundReady ? "ready" : "-"} />
</div> </div>
</header> </header>
<div className="min-h-0 flex-1 overflow-y-auto p-4"> <div className="grid min-h-0 flex-1 grid-cols-[340px_minmax(0,1fr)] gap-3">
<AudioIntakePanel <MaterialColumn
data={data}
step={workflow.input}
jobs={jobs}
job={job} job={job}
selectedFrames={data.selectedFrames} activeJobId={activeJobId}
onToggleFrame={data.onToggleFrame} url={url}
onJobUpdate={data.onJobUpdate} setUrl={setUrl}
onAddFrame={data.onAddManualFrameForJob} fileRef={fileRef}
onDeleteFrame={data.onDeleteFrameForJob} onSubmitUrl={submitUrl}
runtimeModels={runtimeModels} onStartProduction={startProduction}
/>
<AudioStoryboardPlanPanel
job={job}
selectedFrames={data.selectedFrames}
onJobUpdate={data.onJobUpdate}
onDeleteVideo={data.onDeleteVideo}
runtimeModels={runtimeModels}
productStep={workflow.product}
scriptStep={workflow.script}
sceneStep={workflow.scene}
videoStep={workflow.video}
/> />
<section className="skg-board-panel flex min-h-0 flex-col rounded-[20px] border border-white/10 bg-white/[0.035] shadow-2xl">
<header className="shrink-0 border-b border-white/10 p-3">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/12 bg-white/[0.07] text-[#c8cd19]"><Mic className="h-4 w-4" /></span>
<WorkflowStepBadge step={workflow.source} compact />
<h2 className="text-[15px] font-semibold leading-tight text-white"></h2>
</div>
<div className="mt-1 truncate text-[11px] text-white/38" title={statusMessage}>
{statusMessage || "下载源视频后解析音频,再抽参考帧并生成相似主体。"}
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<ModelTrace trace={audioModelTrace(runtimeModels)} compact />
<ActionButton disabled={!job?.video_url || audioRunning} onClick={() => data.onTranscribeAudio?.(job?.id)}>
<Mic className="h-3.5 w-3.5" />
</ActionButton>
</div>
</div>
</header>
<div className="min-h-0 flex-1 overflow-y-auto p-4">
<AudioIntakePanel
job={job}
selectedFrames={data.selectedFrames}
onToggleFrame={data.onToggleFrame}
onJobUpdate={data.onJobUpdate}
onAddFrame={data.onAddManualFrameForJob}
onDeleteFrame={data.onDeleteFrameForJob}
runtimeModels={runtimeModels}
/>
<AudioStoryboardPlanPanel
job={job}
selectedFrames={data.selectedFrames}
onJobUpdate={data.onJobUpdate}
onDeleteVideo={data.onDeleteVideo}
runtimeModels={runtimeModels}
productStep={workflow.product}
scriptStep={workflow.script}
sceneStep={workflow.scene}
videoStep={workflow.video}
/>
</div>
</section>
</div> </div>
</section> </div>
</div> </div>
</div> </div>
<LibraryDrawer <LibraryDrawer
open={libraryOpen} open={libraryOpen}
@@ -2589,6 +2585,77 @@ export function AdRecreationBoard({
) )
} }
function WorkbenchRail({
job,
jobsCount,
audioReady,
visualReady,
subjectAssetCount,
generatedVideoCount,
onOpenLibrary,
onToggleTheme,
boardTheme,
}: {
job: Job | null
jobsCount: number
audioReady: boolean
visualReady: boolean
subjectAssetCount: number
generatedVideoCount: number
onOpenLibrary: () => void
onToggleTheme: () => void
boardTheme: BoardThemeMode
}) {
const railItems = [
{ key: "jobs", label: "素材任务", icon: <Link2 className="h-[18px] w-[18px]" />, active: jobsCount > 0 },
{ key: "source", label: "源视频", icon: <Play className="h-[18px] w-[18px]" />, active: !!job?.video_url },
{ key: "audio", label: "音频文案", icon: <Mic className="h-[18px] w-[18px]" />, active: audioReady },
{ key: "frames", label: "参考帧", icon: <ImageIcon className="h-[18px] w-[18px]" />, active: visualReady },
{ key: "subject", label: "主体套图", icon: <Sparkles className="h-[18px] w-[18px]" />, active: subjectAssetCount > 0 },
{ key: "video", label: "视频候选", icon: <Film className="h-[18px] w-[18px]" />, active: generatedVideoCount > 0 },
]
return (
<aside className="skg-board-rail sticky top-4 flex shrink-0 flex-col items-center py-5" aria-label="工作台导航">
<div className="skg-board-rail__logo mb-8 flex h-9 w-9 items-center justify-center rounded-full text-[12px] font-semibold" title="SKG Marketing Studio">
S
</div>
<nav className="flex flex-1 flex-col items-center gap-4">
{railItems.map((item) => (
<button
key={item.key}
type="button"
className={`skg-board-rail__button inline-flex h-9 w-9 items-center justify-center rounded-full ${item.active ? "is-active" : ""}`}
title={item.label}
aria-label={item.label}
>
{item.icon}
</button>
))}
</nav>
<div className="mt-8 flex flex-col items-center gap-3">
<button
type="button"
onClick={onOpenLibrary}
className="skg-board-rail__button inline-flex h-9 w-9 items-center justify-center rounded-full"
title="资源库"
aria-label="打开资源库"
>
<BookOpen className="h-[18px] w-[18px]" />
</button>
<button
type="button"
onClick={onToggleTheme}
className="skg-board-rail__button inline-flex h-9 w-9 items-center justify-center rounded-full"
title={boardTheme === "dark" ? "切换到明亮模式" : "切换到暗色模式"}
aria-label={boardTheme === "dark" ? "切换到明亮模式" : "切换到暗色模式"}
>
{boardTheme === "dark" ? <Sun className="h-[18px] w-[18px]" /> : <Moon className="h-[18px] w-[18px]" />}
</button>
</div>
</aside>
)
}
function MaterialColumn({ function MaterialColumn({
data, data,
step, step,
@@ -2616,10 +2683,10 @@ function MaterialColumn({
? job.video_url ? "重新解析" : "重新下载" ? job.video_url ? "重新解析" : "重新下载"
: "开始分析" : "开始分析"
return ( return (
<section className="skg-board-panel flex min-h-0 flex-col gap-3 rounded-lg border border-white/10 bg-white/[0.035] p-3 shadow-2xl"> <section className="skg-board-panel flex min-h-0 flex-col gap-3 rounded-[20px] border border-white/10 bg-white/[0.035] p-3 shadow-2xl">
<header className="shrink-0 border-b border-white/10 pb-3"> <header className="shrink-0 border-b border-white/10 pb-3">
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-[#d6b36a]/18 bg-[#d6b36a]/10 text-[#f2d58a]"><Plus className="h-4 w-4" /></span> <span className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/12 bg-white/[0.07] text-[#c8cd19]"><Plus className="h-4 w-4" /></span>
<WorkflowStepBadge step={step} compact /> <WorkflowStepBadge step={step} compact />
</div> </div>
<h2 className="text-[15px] font-semibold leading-tight text-white"></h2> <h2 className="text-[15px] font-semibold leading-tight text-white"></h2>
@@ -2632,7 +2699,7 @@ function MaterialColumn({
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") onSubmitUrl() }} onKeyDown={(e) => { if (e.key === "Enter") onSubmitUrl() }}
placeholder="粘贴 TK / 信息流视频链接" placeholder="粘贴 TK / 信息流视频链接"
className="h-10 min-w-0 flex-1 rounded-md border border-white/10 bg-black/45 px-3 text-[13px] text-white outline-none placeholder:text-white/28 focus:border-[#d6b36a]/60" className="h-10 min-w-0 flex-1 rounded-md border border-white/10 bg-black/35 px-3 text-[13px] text-white outline-none placeholder:text-white/28 focus:border-[#a2c638]/60"
/> />
<button <button
type="button" type="button"
@@ -2909,7 +2976,7 @@ function AudioIntakePanel({
} }
return ( return (
<section className="rounded-lg border border-white/10 bg-black/28 p-2.5"> <section className="skg-glass-card border border-white/10 p-3">
<div className="mb-2 flex items-center justify-between gap-3"> <div className="mb-2 flex items-center justify-between gap-3">
<SectionTitle icon={<Film className="h-4 w-4" />} title="源视频工作区" /> <SectionTitle icon={<Film className="h-4 w-4" />} title="源视频工作区" />
<div className="flex items-center gap-2 font-mono text-[11px] text-white/38"> <div className="flex items-center gap-2 font-mono text-[11px] text-white/38">
@@ -2930,7 +2997,7 @@ function AudioIntakePanel({
<span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s</span> <span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s</span>
</div> </div>
<div <div
className="relative mx-auto aspect-[9/16] overflow-hidden rounded-md border border-white/10 bg-black" className="relative mx-auto aspect-[9/16] overflow-hidden rounded-[20px] border border-white/10 bg-black shadow-[10px_10px_10px_rgba(0,0,0,0.3)]"
style={{ height: SOURCE_VIDEO_HEIGHT }} style={{ height: SOURCE_VIDEO_HEIGHT }}
> >
{job.video_url ? ( {job.video_url ? (
@@ -2959,7 +3026,7 @@ function AudioIntakePanel({
onClick={() => void addFrameAtCurrentTime()} onClick={() => void addFrameAtCurrentTime()}
disabled={!job.video_url || !onAddFrame || manualBusy || job.status === "splitting"} disabled={!job.video_url || !onAddFrame || manualBusy || job.status === "splitting"}
title={`按当前播放位置手动抽帧:${currentTime.toFixed(1)}s`} title={`按当前播放位置手动抽帧:${currentTime.toFixed(1)}s`}
className="absolute right-2 top-2 inline-flex h-7 items-center justify-center gap-1 rounded-md border border-emerald-200/30 bg-black/78 px-2 text-[10.5px] font-semibold text-emerald-100 shadow-lg backdrop-blur transition hover:border-emerald-100/65 hover:bg-emerald-300/18 disabled:cursor-not-allowed disabled:opacity-35" className="absolute right-2 top-2 inline-flex h-7 items-center justify-center gap-1 rounded-md border border-[#a2c638]/35 bg-black/78 px-2 text-[10.5px] font-semibold text-[#e0f5a0] shadow-lg backdrop-blur transition hover:border-[#a2c638]/70 hover:bg-[#a2c638]/18 disabled:cursor-not-allowed disabled:opacity-35"
> >
{manualBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />} {manualBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
@@ -2976,7 +3043,7 @@ function AudioIntakePanel({
</div> </div>
<div className="min-w-0 space-y-2"> <div className="min-w-0 space-y-2">
<div className="relative z-40 overflow-visible rounded-md border border-white/10 bg-black/32 p-2"> <div className="skg-glass-card--flat relative z-40 overflow-visible border border-white/10 p-2">
<div className="mb-1 flex items-center justify-end gap-3 text-[10px] text-white/40"> <div className="mb-1 flex items-center justify-end gap-3 text-[10px] text-white/40">
<FilmstripDensityControls <FilmstripDensityControls
density={filmstripDensity} density={filmstripDensity}
@@ -3916,13 +3983,13 @@ function SourceSubjectPipeline({
</button> </button>
</div> </div>
<div <div
className={`rounded-md border p-1.5 transition ${ className={`skg-glass-card--flat border p-1.5 transition ${
filmstripDragging filmstripDragging
? referenceDropActive ? referenceDropActive
? "border-[#d6b36a]/80 bg-[#d6b36a]/12 ring-1 ring-[#d6b36a]/45" ? "border-[#a2c638]/80 bg-[#a2c638]/12 ring-1 ring-[#a2c638]/45"
: "border-[#d6b36a]/45 bg-[#d6b36a]/[0.065]" : "border-[#a2c638]/45 bg-[#a2c638]/[0.065]"
: "border-white/10 bg-black/32" : "border-white/10 bg-black/28"
}`} }`}
onDragEnter={(event) => { onDragEnter={(event) => {
if (!onDropFilmstripFrame) return if (!onDropFilmstripFrame) return
event.preventDefault() event.preventDefault()
@@ -4017,7 +4084,7 @@ function SourceSubjectPipeline({
</span> </span>
</div> </div>
<div <div
className="flex flex-col gap-2 overflow-y-auto rounded-md border border-white/10 bg-black/24 p-2" className="skg-glass-card--flat flex flex-col gap-2 overflow-y-auto border border-white/10 p-2"
style={{ maxHeight: SOURCE_CONVERSION_MAX_HEIGHT }} style={{ maxHeight: SOURCE_CONVERSION_MAX_HEIGHT }}
> >
<div className="grid shrink-0 grid-cols-2 gap-1.5"> <div className="grid shrink-0 grid-cols-2 gap-1.5">
@@ -4028,8 +4095,8 @@ function SourceSubjectPipeline({
onClick={() => setSubjectModelBundle(option.value)} onClick={() => setSubjectModelBundle(option.value)}
className={`rounded-md border px-2 py-1.5 text-left transition ${ className={`rounded-md border px-2 py-1.5 text-left transition ${
subjectModelBundle === option.value subjectModelBundle === option.value
? "border-cyan-200/65 bg-cyan-300/12 text-cyan-50" ? "border-[#a2c638]/65 bg-[#a2c638]/12 text-white"
: "border-white/10 bg-black/26 text-white/52 hover:border-white/24 hover:text-white/76" : "border-white/10 bg-black/24 text-white/52 hover:border-white/24 hover:text-white/76"
}`} }`}
title={option.detail} title={option.detail}
> >
@@ -4041,9 +4108,9 @@ function SourceSubjectPipeline({
<div <div
className={`shrink-0 rounded-md border p-2 transition ${ className={`shrink-0 rounded-md border p-2 transition ${
agentDropActive || referenceFrameDragging || filmstripDragging || agentReferenceUploadBusy agentDropActive || referenceFrameDragging || filmstripDragging || agentReferenceUploadBusy
? "border-cyan-200/65 bg-cyan-300/[0.08] ring-1 ring-cyan-200/25" ? "border-[#a2c638]/65 bg-[#a2c638]/[0.08] ring-1 ring-[#a2c638]/25"
: "border-white/10 bg-black/22" : "border-white/10 bg-black/22"
}`} }`}
onDragEnter={handleAgentReferenceDragEnter} onDragEnter={handleAgentReferenceDragEnter}
onDragOver={handleAgentReferenceDragOver} onDragOver={handleAgentReferenceDragOver}
@@ -4076,7 +4143,7 @@ function SourceSubjectPipeline({
</div> </div>
) : ( ) : (
<div className="flex h-[72px] flex-col items-center justify-center rounded border border-dashed border-white/15 px-3 text-center text-[10px] leading-snug text-white/34"> <div className="flex h-[72px] flex-col items-center justify-center rounded border border-dashed border-white/15 px-3 text-center text-[10px] leading-snug text-white/34">
{agentReferenceUploadBusy ? <Loader2 className="mb-1.5 h-4 w-4 animate-spin text-cyan-100/80" /> : <Upload className="mb-1.5 h-4 w-4 text-cyan-100/55" />} {agentReferenceUploadBusy ? <Loader2 className="mb-1.5 h-4 w-4 animate-spin text-[#d7efbc]" /> : <Upload className="mb-1.5 h-4 w-4 text-[#d7efbc]/70" />}
<span className="font-semibold text-white/50"></span> <span className="font-semibold text-white/50"></span>
<span className="mt-0.5 text-white/28"> +</span> <span className="mt-0.5 text-white/28"> +</span>
</div> </div>
@@ -4098,9 +4165,9 @@ function SourceSubjectPipeline({
</div> </div>
{agentAnalysis ? ( {agentAnalysis ? (
<div className="shrink-0 rounded-md border border-emerald-200/18 bg-emerald-300/[0.055] p-2"> <div className="shrink-0 rounded-md border border-[#a2c638]/24 bg-[#a2c638]/[0.07] p-2">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[10px] font-semibold text-emerald-50/76"></span> <span className="text-[10px] font-semibold text-[#d7efbc]"></span>
<span className="text-[9px] text-white/34"> <span className="text-[9px] text-white/34">
= · = ·
</span> </span>
@@ -4109,7 +4176,7 @@ function SourceSubjectPipeline({
{agentTraits.length ? ( {agentTraits.length ? (
<> <>
<div className="mt-2 flex items-center justify-between gap-2 text-[9px]"> <div className="mt-2 flex items-center justify-between gap-2 text-[9px]">
<span className={agentSelectedTraitsDirty ? "text-cyan-100/56" : "text-white/34"}> <span className={agentSelectedTraitsDirty ? "text-[#d7efbc]/70" : "text-white/34"}>
{selectedAgentTraits.length} {agentSelectedTraitsDirty ? " · 待发送" : ""} {selectedAgentTraits.length} {agentSelectedTraitsDirty ? " · 待发送" : ""}
</span> </span>
{selectedAgentTraits.length ? ( {selectedAgentTraits.length ? (
@@ -4134,7 +4201,7 @@ function SourceSubjectPipeline({
title={active ? "已作为保留元素,再点取消" : "点一下加入保留元素"} title={active ? "已作为保留元素,再点取消" : "点一下加入保留元素"}
className={`inline-flex min-h-[22px] cursor-pointer items-center gap-1 rounded-full border px-2 py-0.5 text-[9px] transition ${ className={`inline-flex min-h-[22px] cursor-pointer items-center gap-1 rounded-full border px-2 py-0.5 text-[9px] transition ${
active active
? "border-emerald-100/65 bg-emerald-300/16 text-emerald-50 shadow-[0_0_0_1px_rgba(167,243,208,0.12)]" ? "border-[#a2c638]/65 bg-[#a2c638]/16 text-[#e9f6b8] shadow-[0_0_0_1px_rgba(162,198,56,0.12)]"
: "border-white/10 bg-black/26 text-white/46 hover:border-white/22 hover:text-white/70" : "border-white/10 bg-black/26 text-white/46 hover:border-white/22 hover:text-white/70"
}`} }`}
> >
@@ -4149,14 +4216,14 @@ function SourceSubjectPipeline({
</div> </div>
) : null} ) : null}
<div className="flex shrink-0 flex-col rounded-md border border-white/10 bg-black/22 p-2"> <div className="flex shrink-0 flex-col rounded-md border border-white/10 bg-black/22 p-2">
<div className="mb-1.5 flex items-center justify-between gap-2"> <div className="mb-1.5 flex items-center justify-between gap-2">
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-white/72"> <span className="inline-flex items-center gap-1 text-[10px] font-semibold text-white/72">
<MessageSquare className="h-3.5 w-3.5 text-cyan-100/55" /> <MessageSquare className="h-3.5 w-3.5 text-[#d7efbc]/70" />
</span> </span>
{effectivePrompt ? ( {effectivePrompt ? (
<span className="inline-flex h-6 items-center gap-1 rounded-md border border-[#d6b36a]/24 bg-[#d6b36a]/[0.07] px-2 text-[9px] font-semibold text-[#f4dc88]"> <span className="inline-flex h-6 items-center gap-1 rounded-md border border-[#a2c638]/28 bg-[#a2c638]/[0.08] px-2 text-[9px] font-semibold text-[#d7efbc]">
<Check className="h-3 w-3" /> <Check className="h-3 w-3" />
· {effectiveAgentViews.length} · {effectiveAgentViews.length}
</span> </span>
@@ -4171,7 +4238,7 @@ function SourceSubjectPipeline({
value={agentInput} value={agentInput}
onChange={(event) => setAgentInput(event.target.value)} onChange={(event) => setAgentInput(event.target.value)}
placeholder="直接写要怎么生成,或补充要改什么。" placeholder="直接写要怎么生成,或补充要改什么。"
className="h-32 w-full resize-none rounded border border-transparent bg-transparent px-2 py-2 text-[11px] leading-relaxed text-white outline-none transition placeholder:text-white/24 focus:border-cyan-200/45" className="h-32 w-full resize-none rounded border border-transparent bg-transparent px-2 py-2 text-[11px] leading-relaxed text-white outline-none transition placeholder:text-white/24 focus:border-[#a2c638]/45"
/> />
<div className="mt-2 flex items-center gap-2"> <div className="mt-2 flex items-center gap-2">
<div className="flex h-10 shrink-0 items-center overflow-hidden rounded-md border border-white/10 bg-black/35"> <div className="flex h-10 shrink-0 items-center overflow-hidden rounded-md border border-white/10 bg-black/35">
@@ -4242,7 +4309,7 @@ function SourceSubjectPipeline({
{subjectAssetPacks.length ? `${subjectAssetPacks.length}` : "待生成"} {subjectAssetPacks.length ? `${subjectAssetPacks.length}` : "待生成"}
</span> </span>
</div> </div>
<div className="rounded-md border border-white/10 bg-black/32 p-2"> <div className="skg-glass-card--flat border border-white/10 p-2">
{subjectBusyFor ? ( {subjectBusyFor ? (
<div className="mb-2 rounded-md border border-cyan-200/20 bg-cyan-300/[0.07] px-2.5 py-2 text-[10px] leading-snug text-cyan-50/70"> <div className="mb-2 rounded-md border border-cyan-200/20 bg-cyan-300/[0.07] px-2.5 py-2 text-[10px] leading-snug text-cyan-50/70">
{reconstructionModeConfig(subjectBusyFor.mode).label} {subjectBusyFor.viewCount} {subjectBusyFor.sourceCount || "自主描述"} {reconstructionModeConfig(subjectBusyFor.mode).label} {subjectBusyFor.viewCount} {subjectBusyFor.sourceCount || "自主描述"}
@@ -4253,7 +4320,7 @@ function SourceSubjectPipeline({
{subjectAssetPacks.length ? ( {subjectAssetPacks.length ? (
<div className="grid grid-cols-[minmax(0,1fr)_260px] gap-2"> <div className="grid grid-cols-[minmax(0,1fr)_260px] gap-2">
{activeSubjectPack ? ( {activeSubjectPack ? (
<div className="rounded-md border border-[#d6b36a]/28 bg-[#d6b36a]/[0.07] p-2"> <div className="rounded-md border border-[#a2c638]/28 bg-[#a2c638]/[0.07] p-2">
<div className="mb-2 flex items-center justify-between gap-2"> <div className="mb-2 flex items-center justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-[11px] font-semibold text-white">{activeSubjectPack.label}</div> <div className="truncate text-[11px] font-semibold text-white">{activeSubjectPack.label}</div>
@@ -7883,7 +7950,7 @@ function MaterialCard({
<button <button
type="button" type="button"
onClick={onClick} onClick={onClick}
className={`group w-full rounded-lg border p-3 text-left transition ${active ? "border-[#d6b36a]/55 bg-[#d6b36a]/10 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]" : "border-white/10 bg-black/28 hover:border-[#d6b36a]/28 hover:bg-white/[0.045]"}`} className={`group skg-glass-card--flat w-full border p-3 text-left transition ${active ? "border-[#a2c638]/55 bg-[#a2c638]/10 shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]" : "border-white/10 bg-black/24 hover:border-[#a2c638]/30 hover:bg-white/[0.055]"}`}
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
@@ -7918,7 +7985,7 @@ function MaterialCard({
onDelete() onDelete()
} }
}} }}
className="mt-3 hidden h-8 items-center justify-center gap-1 rounded-md border border-white/10 text-[11px] text-white/50 transition hover:border-[#d6b36a]/40 hover:text-[#f2d58a] group-hover:flex" className="mt-3 hidden h-8 items-center justify-center gap-1 rounded-md border border-white/10 text-[11px] text-white/50 transition hover:border-[#a2c638]/40 hover:text-[#c8cd19] group-hover:flex"
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" />