feat(pipeline): 未生成的人物/场景卡可点进详情(先立绘后三视图)+ 卡内提示词框固定5行
- seed(未生成·来自脚本)卡:缩略图/名字可点 → 进资产详情弹窗(保留卡上内联「AI 生成」快捷, 两条路都能生成,A 方案)。详情弹窗支持「还没生成组」的空壳实体(用脚本名+提示词兜底), 生成立绘后刷新按同名 key 自动切回真实体。 - 顺序限制(本就在详情里,现对 seed 也生效):没立绘时「AI 生成三视图」禁用 + 提示 「请先生成左侧立绘」;立绘按钮首次显示「生成立绘」(非「重跑」),替换走 seed 路由。 - 卡内提示词框(.prompt-box,外层人物/场景卡)固定 5 行高、超长框内滚动,不再被长提示词撑高。 自检:复用现成详情弹窗与 .btn/.prompt-box 共享类,无裸色值/无重写共享类;单橙锚点不变。 验证:tsc + vite build 绿;无头(独立 sqlite 栈)真点 seed 卡 → 详情打开、三视图禁用+提示、 立绘按钮「生成立绘」、提示词框 115px 固定且超长可滚动、0 控制台错误(截图存档)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e5b3833ba5
commit
be4bc1c8f4
@@ -375,7 +375,8 @@
|
|||||||
.prompt-box[contenteditable="true"] { cursor: text; outline: none; }
|
.prompt-box[contenteditable="true"] { cursor: text; outline: none; }
|
||||||
.prompt-box[contenteditable="true"]:hover { border-color: var(--heat-20); }
|
.prompt-box[contenteditable="true"]:hover { border-color: var(--heat-20); }
|
||||||
.prompt-box[contenteditable="true"]:focus { border-color: var(--heat); background: var(--surface); color: var(--accent-black); box-shadow: 0 0 0 3px var(--heat-12); }
|
.prompt-box[contenteditable="true"]:focus { border-color: var(--heat); background: var(--surface); color: var(--accent-black); box-shadow: 0 0 0 3px var(--heat-12); }
|
||||||
.fail-icon { width: 28px; height: 28px; background: var(--accent-crimson); color: var(--accent-white); display: grid; place-items: center; font-weight: 600; font-size: 16px; border-radius: 50%; }
|
/* 外层人物/场景卡提示词框:固定 5 行高(行高 1.55 + 内边距/边框),超长不撑高、框内上下滚动 */
|
||||||
|
.prompt-box { height: calc(5 * 1.55em + 22px); overflow-y: auto; box-sizing: border-box; }
|
||||||
|
|
||||||
/* ============= STAGE 3 · 故事板 ============= */
|
/* ============= STAGE 3 · 故事板 ============= */
|
||||||
.content--fh .stage[data-stage-pane="3"].active > .stage-storyboard { flex: 1 1 0; min-height: 0; overflow-y: auto; }
|
.content--fh .stage[data-stage-pane="3"].active > .stage-storyboard { flex: 1 1 0; min-height: 0; overflow-y: auto; }
|
||||||
|
|||||||
@@ -799,6 +799,14 @@ export function PipelinePage(props: {
|
|||||||
setAdTriId(null); // 跟随当前立绘的最新三视图
|
setAdTriId(null); // 跟随当前立绘的最新三视图
|
||||||
setAdPrompt(entity.group.prompt ?? "");
|
setAdPrompt(entity.group.prompt ?? "");
|
||||||
}
|
}
|
||||||
|
// seed(未生成)卡也能点进详情:还没组,用脚本名(=key)+ 当前提示词开一个「空壳」详情。
|
||||||
|
// 进去先生成立绘;三视图按既有逻辑(无立绘则禁用)天然挡住。生成完刷新 → 同 key(=名字)自动切到真实体。
|
||||||
|
function openSeedDetail(kind: "person" | "scene", tag: string, promptValue: string) {
|
||||||
|
setAdDetail({ kind, key: tag });
|
||||||
|
setAdPortraitId(null);
|
||||||
|
setAdTriId(null);
|
||||||
|
setAdPrompt(promptValue);
|
||||||
|
}
|
||||||
// 流程步骤4 · 演员库覆盖层:replace 带要替换的目标(已生成卡=groupId;seed 占位卡还没组=kind+label);
|
// 流程步骤4 · 演员库覆盖层:replace 带要替换的目标(已生成卡=groupId;seed 占位卡还没组=kind+label);
|
||||||
// browse 带 studio 直接进「添加人物工作台」
|
// browse 带 studio 直接进「添加人物工作台」
|
||||||
const [actorLib, setActorLib] = useState<{ mode: "browse" | "replace"; studio?: boolean; groupId?: string; seedKind?: "person" | "scene"; seedLabel?: string; addNew?: boolean } | null>(null);
|
const [actorLib, setActorLib] = useState<{ mode: "browse" | "replace"; studio?: boolean; groupId?: string; seedKind?: "person" | "scene"; seedLabel?: string; addNew?: boolean } | null>(null);
|
||||||
@@ -2819,13 +2827,14 @@ export function PipelinePage(props: {
|
|||||||
const busy = isBusy(seedKey) || isBusy(`${seedKey}:tri`) || pendingHas(kind, tag);
|
const busy = isBusy(seedKey) || isBusy(`${seedKey}:tri`) || pendingHas(kind, tag);
|
||||||
return (
|
return (
|
||||||
<div className="asset-card-2 asset-seed" data-asset-kind={kind} data-seed-tag={tag} key={seedKey}>
|
<div className="asset-card-2 asset-seed" data-asset-kind={kind} data-seed-tag={tag} key={seedKey}>
|
||||||
<div className="placeholder thumb-2">
|
<div className="placeholder thumb-2" role="button" tabIndex={0} style={{ cursor: "pointer" }} title="点击进详情(先生成立绘,再生成三视图)"
|
||||||
|
onClick={() => openSeedDetail(kind, tag, promptValue)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openSeedDetail(kind, tag, promptValue); } }}>
|
||||||
{busy
|
{busy
|
||||||
? <div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "center" }}><span className="spinner" aria-hidden="true"></span><span className="ph-frame">生成中 · 约 8s</span></div>
|
? <div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "center" }}><span className="spinner" aria-hidden="true"></span><span className="ph-frame">生成中 · 约 8s</span></div>
|
||||||
: <span className="ph-frame">{tag} · 待生成</span>}
|
: <span className="ph-frame">{tag} · 待生成</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="body-2">
|
<div className="body-2">
|
||||||
<div className="hstack"><strong style={{ fontSize: "13.5px" }}>{tag}</strong><span className="spacer"></span><span className="pill neutral"><span className="dot"></span>来自脚本</span></div>
|
<div className="hstack"><strong style={{ fontSize: "13.5px", cursor: "pointer" }} onClick={() => openSeedDetail(kind, tag, promptValue)}>{tag}</strong><span className="spacer"></span><span className="pill neutral"><span className="dot"></span>来自脚本</span></div>
|
||||||
<PromptBox key={seedKey} value={promptValue} onChange={(v) => setAssetPromptDraft((m) => ({ ...m, [seedKey]: v }))} />
|
<PromptBox key={seedKey} value={promptValue} onChange={(v) => setAssetPromptDraft((m) => ({ ...m, [seedKey]: v }))} />
|
||||||
<div className="hstack" style={{ marginTop: 10 }}>
|
<div className="hstack" style={{ marginTop: 10 }}>
|
||||||
<button className="btn btn-ghost btn-sm" type="button" data-stop disabled={busy} onClick={() => openActorReplaceSeed(kind, tag)}>替换</button>
|
<button className="btn btn-ghost btn-sm" type="button" data-stop disabled={busy} onClick={() => openActorReplaceSeed(kind, tag)}>替换</button>
|
||||||
@@ -3612,8 +3621,15 @@ export function PipelinePage(props: {
|
|||||||
{/* ── 流程步骤4 · 人物/场景详情弹窗:立绘 + 三视图(人物)+ 提示词重获 + 版本历史 + 应用到当前项目 ── */}
|
{/* ── 流程步骤4 · 人物/场景详情弹窗:立绘 + 三视图(人物)+ 提示词重获 + 版本历史 + 应用到当前项目 ── */}
|
||||||
{adDetail && (() => {
|
{adDetail && (() => {
|
||||||
const entities = buildEntities(adDetail.kind);
|
const entities = buildEntities(adDetail.kind);
|
||||||
const entity = entities.find((e) => e.key === adDetail.key);
|
const realEntity = entities.find((e) => e.key === adDetail.key);
|
||||||
if (!entity) return null;
|
// seed(还没生成组)也能开详情:用脚本名(=key)+ 当前提示词兜个「空壳」实体,进去先生成立绘。
|
||||||
|
// 生成完刷新 → buildEntities 按同 key(label=名字)收到真实组,自动切回真实体。
|
||||||
|
const entity: AssetEntity = realEntity ?? {
|
||||||
|
key: adDetail.key, label: adDetail.key, name: adDetail.key, kind: adDetail.kind,
|
||||||
|
group: { id: "", kind: adDetail.kind, prompt: adPrompt, adopted_asset: null, candidate_assets: [] },
|
||||||
|
versions: [],
|
||||||
|
};
|
||||||
|
const isSeed = !realEntity; // 空壳态:还没立绘
|
||||||
const isPerson = adDetail.kind === "person";
|
const isPerson = adDetail.kind === "person";
|
||||||
const grp = entity.group;
|
const grp = entity.group;
|
||||||
const portraitVersions = grp.candidate_assets ?? [];
|
const portraitVersions = grp.candidate_assets ?? [];
|
||||||
@@ -3626,7 +3642,8 @@ export function PipelinePage(props: {
|
|||||||
const viewTriAsset = (adTriId && triVersions.includes(adTriId)) ? adTriId : (triGroup?.adopted_asset || triVersions.at(-1) || "");
|
const viewTriAsset = (adTriId && triVersions.includes(adTriId)) ? adTriId : (triGroup?.adopted_asset || triVersions.at(-1) || "");
|
||||||
const triUrl = candUrl(triGroup, viewTriAsset);
|
const triUrl = candUrl(triGroup, viewTriAsset);
|
||||||
const pBK = `addet-portrait:${entity.key}`;
|
const pBK = `addet-portrait:${entity.key}`;
|
||||||
const busyPortrait = isBusy(pBK);
|
// 没立绘时(尤其 seed 首次生成),立绘在 worker 跑 ~30s,本地 isBusy 早已落回 → 用 pendingHas 兜底转圈
|
||||||
|
const busyPortrait = isBusy(pBK) || (!portraitUrl && pendingHas(adDetail.kind, entity.name));
|
||||||
const busyTri = isBusy(`addet-tri:${entity.key}`) || isBusy(`${pBK}:tri`);
|
const busyTri = isBusy(`addet-tri:${entity.key}`) || isBusy(`${pBK}:tri`);
|
||||||
async function regenPortrait() {
|
async function regenPortrait() {
|
||||||
const prompt = adPrompt.trim() || grp.prompt || `${entity!.name},9:16 竖屏`;
|
const prompt = adPrompt.trim() || grp.prompt || `${entity!.name},9:16 竖屏`;
|
||||||
@@ -3701,7 +3718,7 @@ export function PipelinePage(props: {
|
|||||||
{!triUrl && !busyTri && (
|
{!triUrl && !busyTri && (
|
||||||
<div className="asset-detail-tip">
|
<div className="asset-detail-tip">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></svg>
|
||||||
<span>三视图据这版立绘生成,保证正/侧/背多角度一致。点下方按钮生成。</span>
|
<span>{viewPortraitAsset ? "三视图据这版立绘生成,保证正/侧/背多角度一致。点下方按钮生成。" : "请先生成左侧「立绘」,有了立绘才能据它生成三视图。"}</span>
|
||||||
<button className="ai-gen-btn" type="button" disabled={busyTri || !viewPortraitAsset} onClick={() => void regenTri()}>AI 生成三视图</button>
|
<button className="ai-gen-btn" type="button" disabled={busyTri || !viewPortraitAsset} onClick={() => void regenTri()}>AI 生成三视图</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -3725,9 +3742,9 @@ export function PipelinePage(props: {
|
|||||||
<div style={{ display: "flex", gap: 8, marginTop: 10, justifyContent: "flex-end" }}>
|
<div style={{ display: "flex", gap: 8, marginTop: 10, justifyContent: "flex-end" }}>
|
||||||
<button className="btn btn-primary btn-sm" type="button" disabled={busyPortrait} onClick={() => void regenPortrait()}>
|
<button className="btn btn-primary btn-sm" type="button" disabled={busyPortrait} onClick={() => void regenPortrait()}>
|
||||||
{busyPortrait ? <span className="asset-spinner sm" aria-hidden="true" style={{ marginRight: 4 }}></span> : <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 4 }}><path d="M4 12a8 8 0 0 1 14-5.5L21 9" /><path d="M21 4v5h-5" /><path d="M20 12a8 8 0 0 1-14 5.5L3 15" /><path d="M3 20v-5h5" /></svg>}
|
{busyPortrait ? <span className="asset-spinner sm" aria-hidden="true" style={{ marginRight: 4 }}></span> : <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 4 }}><path d="M4 12a8 8 0 0 1 14-5.5L21 9" /><path d="M21 4v5h-5" /><path d="M20 12a8 8 0 0 1-14 5.5L3 15" /><path d="M3 20v-5h5" /></svg>}
|
||||||
{busyPortrait ? "生成中…" : (isPerson ? "重跑立绘" : "重跑")}
|
{busyPortrait ? "生成中…" : (portraitVersions.length === 0 ? (isPerson ? "生成立绘" : "生成场景图") : (isPerson ? "重跑立绘" : "重跑"))}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-ghost btn-sm" type="button" onClick={() => openActorReplace(entity)}>替换</button>
|
<button className="btn btn-ghost btn-sm" type="button" onClick={() => isSeed ? openActorReplaceSeed(adDetail.kind, entity.name) : openActorReplace(entity)}>替换</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user