auto-save 2026-05-17 23:03 (~3)

This commit is contained in:
2026-05-17 23:03:08 +08:00
parent b4b2259440
commit 290a833019
3 changed files with 68 additions and 76 deletions

View File

@@ -1,18 +1,5 @@
{
"entries": [
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-15 14:10 (~1)",
"ts": "2026-05-15T06:14:46Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"hash": "b715b34",
"message": "auto-save 2026-05-15 14:16 (~1)",
"ts": "2026-05-15T14:17:11+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "00699ba",
@@ -3259,6 +3246,19 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交fix: reorganize source video frame workflow",
"files_changed": 1
},
{
"ts": "2026-05-17T22:57:27+08:00",
"type": "commit",
"message": "auto-save 2026-05-17 22:57 (~2)",
"hash": "b4b2259",
"files_changed": 2
},
{
"ts": "2026-05-17T14:58:31Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 2 项未提交变更 · 最近提交auto-save 2026-05-17 22:57 (~2)",
"files_changed": 2
}
]
}

View File

@@ -569,7 +569,7 @@
<section id="pipeline" data-search>
<h2>业务管线</h2>
<p>当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧先完成音频解析,再进入信息流复刻分镜工作台。用户粘贴 TK 链接或上传视频后点击“开始”,系统自动下载源视频;下载完成后优先提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效。分镜规划按逐句时间轴生成;视觉参考改为原版视频旁统一抽取 12 张动作/节奏参考帧,也可在原视频播放器右上角按当前播放点手动补帧,由人工选择后生成“类似但不复刻”的相似主角 6 张白底视图,再按分镜生成视频候选。</p>
<p>当前产品方向已收窄为“信息流广告快速复刻”:主界面左侧是素材输入列,右侧先完成音频解析,再进入信息流复刻分镜工作台。用户粘贴 TK 链接或上传视频后点击“开始”,系统自动下载源视频;下载完成后优先提取原音频文案/字幕,并分析讲话人、语速节奏、背景音乐/环境声/音效。分镜规划按逐句时间轴生成;视觉参考改为原版视频下方的关键帧池:显眼保留“自动抽帧 12 张”,也可在竖版播放器内按当前播放点手动补帧,由人工选择后生成“类似但不复刻”的相似主角 6 张白底视图,再按分镜生成视频候选。</p>
<div class="pipeline">
<div class="step"><div class="num">1</div><h3>导入素材</h3><p>粘贴 TK / 信息流视频链接或上传本地视频;“开始”只把任务放入第一步队列。</p></div>
<div class="step"><div class="num">2</div><h3>下载源视频</h3><p>后端用 yt-dlp 或本地上传文件落 <code>source.mp4</code>,记录时长、尺寸和视频只读地址。</p></div>
@@ -589,7 +589,7 @@
<tr><td><code>web/next.config.mjs</code></td><td>Next.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator并移除 Next 16 已不支持的 <code>eslint</code> 顶层配置,避免本地 dev 出现配置 Issue 提示。</td></tr>
<tr><td><code>web/app/globals.css</code></td><td>全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 <code>nextjs-portal</code> 遮挡隐藏规则。</td></tr>
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态jobs、activeJobId、生成任务状态主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始”编排状态只负责在下载完成后自动触发 <code>triggerTranscribe</code>不再默认触发抽帧、Vision 扫描或分镜初稿保存;底部吸附音频条不再从主界面渲染。</td></tr>
<tr><td><code>web/components/ad-recreation-board.tsx</code></td><td>信息流广告复刻工作表:左侧素材输入;右侧展示视频下载状态、默认折叠的音频文案依据,以及统一的音频解析结果面板;面板顶部是一行讲话人/节奏/背景音摘要,下方第一行左侧为原视频播放器、右侧为逐句时间轴,第二行铺开“关键帧 / 相似主角”。底部横向音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 <code>requestAnimationFrame</code> 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。原视频标题栏右侧提供“当前点抽帧”,按当前播放秒数手动补参考帧;关键帧区一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化,缩略图按竖版完整比例显示不裁切并横向多列铺开,鼠标停留会通过固定浮层放大展示完整帧。“生成 6 视图”放在相似主角白底视图区,不和抽参考按钮平齐;人工勾选后调用 <code>generateSubjectAssets</code><code>source_actor + similar</code> 模式生成 6 张白底相似主角视图;这是新演员重构,不做像素提取或精确复刻源人物身份。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,把横向空间留给新口播、画面规划和视频候选;生成本条视频时使用当前编辑后的新口播文案。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和 6 个候选视频槽;候选视频槽在宽屏下一排显示 6 个竖版预览,避免前面空旷、后面拥挤。单条生成会从全局选中关键帧或 12 张关键帧中取最贴近本句时间点的参考帧。单条生成会从产品素材池按分镜角色、视角优先级、用途标签、置信度和风险自动挑选最多 6 张相关产品图,不会把全部产品图提交给生视频模型,然后把产品坐标系、视角标注、方向、结构点和风险写入 Seedance 提示。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。</td></tr>
<tr><td><code>web/components/ad-recreation-board.tsx</code></td><td>信息流广告复刻工作表:左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧展示视频下载状态、默认折叠的文案依据,以及源视频工作区。音频解析结果改成默认折叠的辅助信息,展开后同一行讲话人/节奏/背景音;主工作区左侧是按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧;右侧上方是音频波形 / 切点参考,下方是逐句时间轴;下一行铺开“关键帧 / 相似主角”。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 <code>requestAnimationFrame</code> 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。关键帧区的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化,缩略图按竖版完整比例显示不裁切并用更多列紧凑铺开,鼠标停留会通过固定浮层放大展示完整帧。“生成 6 视图”放在相似主角白底视图区,不和抽参考按钮平齐;人工勾选后调用 <code>generateSubjectAssets</code><code>source_actor + similar</code> 模式生成 6 张白底相似主角视图;这是新演员重构,不做像素提取或精确复刻源人物身份。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,把横向空间留给新口播、画面规划和视频候选;生成本条视频时使用当前编辑后的新口播文案。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和 6 个候选视频槽;候选视频槽在宽屏下一排显示 6 个竖版预览,避免前面空旷、后面拥挤。单条生成会从全局选中关键帧或 12 张关键帧中取最贴近本句时间点的参考帧。单条生成会从产品素材池按分镜角色、视角优先级、用途标签、置信度和风险自动挑选最多 6 张相关产品图,不会把全部产品图提交给生视频模型,然后把产品坐标系、视角标注、方向、结构点和风险写入 Seedance 提示。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。</td></tr>
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr>
<tr><td><code>web/app/login/layout.tsx</code></td><td>登录路由专属 layout覆盖全站默认网页标题和描述为空避免 <code>/login</code> 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。</td></tr>
<tr><td><code>web/components/login/oasis-canvas.tsx</code></td><td>登录页全屏动态视觉层:用 iframe 直接承载下载包 <code>web/public/oasis-source/index.html</code> 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 <code>postMessage</code> 转发给 iframe避免登录面板或输入框遮挡时草地失去鼠标响应。</td></tr>
@@ -626,7 +626,7 @@
web/app/page.tsx
-> 信息流广告复刻工作表web/components/ad-recreation-board.tsx
-> 开始:创建/激活 job → 下载完成后自动触发音频处理
-> 左侧素材输入列 + 右侧默认折叠的音频文案依据 + 统一音频解析结果面板(声音摘要在上,原视频播放器右上角可当前点抽帧,逐句时间轴在原视频右侧,参考帧池在下方多列铺开,相似主角 6 白底视图生成按钮放在视图区,底部连续响度波形显示当前/总时长/指针停点
-> 左侧素材输入列 + 右侧默认折叠的文案依据 + 源视频工作区(音频解析结果默认折叠,竖版 9:16 原视频播放器可当前点抽帧,右侧上方连续响度波形显示当前/总时长/指针停点,右侧下方逐句时间轴联动滚动,参考帧池在下方多列铺开且主入口为“自动抽帧 12 张”,相似主角 6 白底视图生成按钮放在视图区
-> 信息流复刻分镜工作台:同一产品素材池不限量上传 → 自动识别视角 / 背景 / 用途 / 风险 → 人工检查备注 → 单条生成自动挑选最多 6 张相关产品图 → 逐句时间轴 → 原内容 / 新口播文案 / 画面规划与产品融入 / 6 个候选视频槽
-> 底部音频条:不再渲染,音频结果集中到右侧工作表
-> 旧节点/深度素材面板web/components/nodes/index.tsx、web/components/lightbox.tsx、web/components/storyboard-workbench.tsx底层保留当前不作为主入口
@@ -649,8 +649,8 @@ api/main.py
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>音频解析结果表</span></div>
<div><strong>主要源码</strong><span><code>AudioIntakePanel</code> / <code>AudioIntakeStatus</code> / <code>SourceReferenceBuildPanel</code> in <code>web/components/ad-recreation-board.tsx</code>;复用 <code>triggerTranscribe</code><code>AudioScript</code><code>analyzeJob</code><code>addManualFrame</code><code>deleteFrame</code><code>generateSubjectAssets</code></span></div>
<div><strong>适合怎么描述</strong><span>“原视频播放尺寸、当前播放点手动抽帧、关键帧删除、相似主角 6 白底视图、连续响度波形、逐句时间轴滚动、高亮和跳转联动还需要怎么调整”。</span></div>
<div><strong>主要源码</strong><span><code>AudioIntakePanel</code> / <code>SourceReferenceBuildPanel</code> in <code>web/components/ad-recreation-board.tsx</code>;复用 <code>triggerTranscribe</code><code>AudioScript</code><code>analyzeJob</code><code>addManualFrame</code><code>deleteFrame</code><code>generateSubjectAssets</code></span></div>
<div><strong>适合怎么描述</strong><span>竖版原视频尺寸、播放器内当前播放点手动抽帧、自动抽帧 12 张入口、关键帧删除、相似主角 6 白底视图、连续响度波形、逐句时间轴滚动、高亮和跳转联动还需要怎么调整”。</span></div>
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>信息流复刻分镜工作台</span></div>
@@ -925,7 +925,7 @@ ProductRefStateItem {
</tr>
<tr>
<td><span class="tag gray">音频条</span></td>
<td>复刻工作表顶部触发音频解析;全文音频文案依据默认折叠,主展示以统一音频解析结果面板为准:声音/节奏/背景音摘要在上,逐句时间轴在;底部 <code>AudioStrip</code> 当前不渲染。</td>
<td>复刻工作表顶部触发音频解析;全文文案依据默认折叠,音频解析结果也默认折叠为辅助信息;主展示以源视频工作区为准:竖版原视频在左,音频波形和逐句时间轴在;底部 <code>AudioStrip</code> 当前不渲染。</td>
<td>当前第一步不要默认展示底部音频条、新配音播放器、独立原文案提取大卡片,或把 MiniMax 配音当作已完成结果。</td>
<td><code>web/components/audio-strip.tsx</code><code>pipeline_transcribe</code><code>AudioScript</code></td>
</tr>
@@ -1003,6 +1003,18 @@ ProductRefStateItem {
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-17 · 竖版源视频工作区重排并恢复自动抽帧主入口</h3>
<span class="tag rose">UI</span>
<span class="tag cyan">Workflow</span>
</header>
<div class="body">
<p><strong>问题:</strong>顶部“音频文案依据 / 当前步骤 / 音频解析结果”仍然抢占主操作空间;左侧素材列重复放横版原视频预览,竖版视频会产生大黑边;“自动抽帧”入口不够显眼,用户会误以为能力被去掉。</p>
<p><strong>改动:</strong><code>AdRecreationBoard</code> 把状态压成紧凑标签,文案依据保持默认折叠;<code>MaterialColumn</code> 不再重复显示原视频预览;<code>AudioIntakePanel</code> 把音频解析结果改成默认折叠,主区域改为左侧 9:16 竖版播放器、右侧音频波形 + 逐句时间轴,播放器内覆盖“当前点抽帧”;<code>SourceReferenceBuildPanel</code> 保留并强化“自动抽帧 12 张”主按钮,缩略图更小更密,下面再放相似主角 6 视图。</p>
<p><strong>影响:</strong><code>web/components/ad-recreation-board.tsx</code><code>docs/source-analysis.html</code>。后续源视频分析先看竖版播放器、波形和逐句时间轴;参考帧池负责自动 12 帧、手动补帧后的删除和相似主角选择。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-17 · 时间轴贴近原视频并压小参考帧</h3>

View File

@@ -826,8 +826,8 @@ export function AdRecreationBoard({
</ActionButton>
</div>
<div className="mt-2 grid grid-cols-1 gap-2 xl:grid-cols-[minmax(0,1fr)_260px]">
<div className="grid grid-cols-4 gap-2 rounded-md border border-white/10 bg-black/28 p-2 text-[11px] text-white/52">
<div className="mt-2 grid grid-cols-1 gap-2 xl:grid-cols-[minmax(0,1fr)_360px]">
<div className="flex min-w-0 flex-wrap items-center gap-1.5 text-[11px] text-white/46">
<Requirement label="素材" ready={!!job} detail={job ? shortId(job.id) : "待输入"} />
<Requirement label="视频" ready={!!job?.video_url} detail={job?.status === "downloading" ? "下载中" : job?.video_url ? "已就绪" : "待下载"} />
<Requirement label="音频" ready={!!job?.source_audio_url} detail={job?.status === "transcribing" ? "解析中" : job?.source_audio_url ? "已提取" : "待提取"} />
@@ -956,41 +956,10 @@ function MaterialColumn({
<EmptyState text="还没有素材。每导入一个链接或上传一个文件,就会新增一个素材任务。" />
)}
</div>
{job?.video_url && (
<video
src={videoUrl(job.id)}
controls
playsInline
className="aspect-video w-full rounded-lg border border-white/10 bg-black object-contain"
/>
)}
</section>
)
}
function AudioIntakeStatus({ job, audioReady }: { job: Job | null; audioReady: boolean }) {
const downloading = !!job && ["created", "downloading"].includes(job.status)
const audioRunning = !!job && (job.status === "transcribing" || job.audio_script?.status === "rewriting")
return (
<div className="rounded-lg border border-white/10 bg-black/32 p-2.5">
<div className="mb-2 flex items-center justify-between gap-2">
<SectionTitle icon={<PanelRight className="h-4 w-4" />} title="当前步骤" />
<StatusPill ready={audioReady} running={downloading || audioRunning} />
</div>
<div className="grid grid-cols-4 gap-2 text-[11px] text-white/52">
<Requirement label="素材" ready={!!job} detail={job ? shortId(job.id) : "待输入"} />
<Requirement label="视频" ready={!!job?.video_url} detail={downloading ? "下载中" : job?.video_url ? "已就绪" : "待下载"} />
<Requirement label="音频" ready={!!job?.source_audio_url} detail={audioRunning ? "解析中" : job?.source_audio_url ? "已提取" : "待提取"} />
<Requirement label="文案" ready={audioReady} detail={audioReady ? `${job?.transcript.length ?? 0}` : "待解析"} />
</div>
<div className="mt-2 truncate rounded-md border border-white/10 bg-black/28 px-3 py-2 text-[11px] text-white/42" title={job?.message}>
{job?.message || "粘贴 TK 链接或上传视频后,点击开始进入下载和音频解析。"}
</div>
</div>
)
}
function AudioIntakePanel({
job,
selectedFrames,
@@ -1132,21 +1101,32 @@ function AudioIntakePanel({
</div>
</div>
<div className="mb-2 grid grid-cols-3 gap-2">
{profiles.map((item) => (
<ProfileTile key={item.label} label={item.label} value={item.value} running={processing} />
))}
</div>
<details className="group mb-2 rounded-md border border-white/10 bg-black/24 p-2">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3">
<SectionTitle icon={<Mic className="h-4 w-4" />} title="音频解析结果" />
<div className="flex min-w-0 items-center gap-2">
<span className="hidden min-w-0 truncate text-[11px] text-white/40 md:inline">
/ /
</span>
<ChevronDown className="h-4 w-4 shrink-0 text-white/38 transition group-open:rotate-180" />
</div>
</summary>
<div className="mt-2 grid grid-cols-1 gap-2 md:grid-cols-3">
{profiles.map((item) => (
<ProfileTile key={item.label} label={item.label} value={item.value} running={processing} />
))}
</div>
</details>
<div className="grid gap-2 border-t border-white/8 pt-2">
<div className="grid gap-2">
<div className="grid gap-3 xl:grid-cols-[280px_minmax(0,1fr)] 2xl:grid-cols-[300px_minmax(0,1fr)]">
<div className="grid gap-3 xl:grid-cols-[300px_minmax(0,1fr)] 2xl:grid-cols-[330px_minmax(0,1fr)]">
<div className="min-w-0">
<div className="mb-2 flex items-center justify-between gap-3">
<SectionTitle icon={<Play className="h-4 w-4" />} title="原版视频" />
<span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s</span>
</div>
<div className="mx-auto aspect-[9/16] h-[420px] overflow-hidden rounded-md border border-white/10 bg-black 2xl:h-[480px]">
<div className="relative mx-auto aspect-[9/16] h-[400px] overflow-hidden rounded-md border border-white/10 bg-black 2xl:h-[460px]">
{job.video_url ? (
<video
ref={videoRef}
@@ -1168,17 +1148,17 @@ function AudioIntakePanel({
) : (
<div className="flex h-full items-center justify-center text-[12px] text-white/38"></div>
)}
<button
type="button"
onClick={() => void addFrameAtCurrentTime()}
disabled={!job.video_url || !onAddFrame || manualBusy || job.status === "splitting"}
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"
>
{manualBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
</button>
</div>
<button
type="button"
onClick={() => void addFrameAtCurrentTime()}
disabled={!job.video_url || !onAddFrame || manualBusy || job.status === "splitting"}
title={`按当前播放位置手动抽帧:${currentTime.toFixed(1)}s`}
className="mt-2 inline-flex h-8 w-full items-center justify-center gap-1 rounded-md border border-emerald-300/20 bg-emerald-300/[0.08] px-2 text-[11px] font-semibold text-emerald-100 transition hover:border-emerald-200/45 hover:bg-emerald-300/[0.14] 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" />}
· {currentTime.toFixed(1)}s
</button>
</div>
<div className="min-w-0 space-y-2">
@@ -1415,20 +1395,20 @@ function SourceReferenceBuildPanel({
onClick={() => void extractKeyframes()}
disabled={!job.video_url || extracting || job.status === "splitting"}
title="自动按动作峰值抽 12 张参考帧,更偏向手势、表情变化、节奏点和镜头变化"
className="inline-flex h-7 items-center justify-center gap-1 rounded-md bg-white px-2.5 text-[10.5px] font-semibold text-black transition hover:bg-white/90 disabled:cursor-not-allowed disabled:opacity-40"
className="inline-flex h-8 items-center justify-center gap-1 rounded-md bg-white px-3 text-[11px] font-semibold text-black transition hover:bg-white/90 disabled:cursor-not-allowed disabled:opacity-40"
>
{extracting || job.status === "splitting" ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Scissors className="h-3.5 w-3.5" />}
12
</button>
</div>
</div>
<div className="h-[300px] overflow-y-auto rounded-md border border-white/10 bg-black/32 p-2 2xl:h-[340px]">
<div className="h-[250px] overflow-y-auto rounded-md border border-white/10 bg-black/32 p-2 2xl:h-[290px]">
<div className="flex items-center justify-between gap-2">
<span className="text-[10.5px] text-white/34"></span>
<span className="text-[10.5px] text-white/30"></span>
</div>
<div className="mt-2 grid grid-cols-6 gap-1.5 md:grid-cols-8 xl:grid-cols-12">
<div className="mt-2 grid grid-cols-6 gap-1.5 md:grid-cols-8 xl:grid-cols-12 2xl:grid-cols-16">
{frames.map((frame, index) => {
const selected = selectedFrames.has(frame.index)
return (
@@ -1473,7 +1453,7 @@ function SourceReferenceBuildPanel({
})}
{!frames.length && (
<div className="col-span-full flex h-[106px] items-center justify-center rounded border border-dashed border-white/12 text-[11px] text-white/34">
12
12
</div>
)}
</div>
@@ -2902,10 +2882,10 @@ function EmptyState({ text }: { text: string }) {
function Requirement({ label, ready, detail }: { label: string; ready: boolean; detail: string }) {
return (
<div className="flex h-10 min-w-0 items-center gap-2 rounded-md border border-white/10 bg-black/28 px-2">
{ready ? <Check className="h-3.5 w-3.5 shrink-0 text-emerald-200" /> : <Circle className="h-3.5 w-3.5 shrink-0 text-white/38" />}
<div className="flex h-7 min-w-0 items-center gap-1.5 rounded-md border border-white/10 bg-black/24 px-2">
{ready ? <Check className="h-3 w-3 shrink-0 text-emerald-200" /> : <Circle className="h-3 w-3 shrink-0 text-white/38" />}
<span className="shrink-0 whitespace-nowrap">{label}</span>
<span className="min-w-0 truncate font-mono text-[11px] text-white/42">{detail}</span>
<span className="min-w-0 truncate font-mono text-[10.5px] text-white/42">{detail}</span>
</div>
)
}