feat(脚本): 按流程文档重写脚本阶段(自动提取人物/场景 + 标签胶囊改写 + 基础资产按标签 seed)
后端: - 出稿后新增轻量提取调用,从脚本抽取人物/场景标签及每个标签的建议生图提示词, 写入 project.metadata(cast/scenes/cast_prompts/scene_prompts) - 提取调用接入计费闭环(reserve→charge/release,记为 script_optimization); best-effort:无模型/预扣失败/调用失败/解析失败都吞掉返回空,绝不阻断出稿 - generate_base_asset 增加 label,落 group.metadata,把生成结果归回脚本标签卡 前端: - 设定卡风格补齐到 5 个(真实测评/痛点种草/小红书种草/开箱测评/对比展示) - 标签改为待确认胶囊流:增删不立即落库,排进队列在输入框上方显示可撤销胶囊, 点发送才整体重写;待删划线、待加虚线 - 基础资产人物/场景按提取标签逐个 seed 卡,显示 AI 提示词、可改后生成 测试:新增提取+计费+release+label 共 4 个用例,全套 24 个通过(sqlite test 设置) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,8 @@ const VO_VOICES = [
|
||||
{ key: "BV002_streaming", label: "通用男声" },
|
||||
];
|
||||
// 新建向导落进 metadata.wizard 的是选项 key,这里映射回中文(对齐 projects.tsx 的 WIZ_STYLES/WIZ_PERSONAS)
|
||||
const WIZ_STYLE_LABEL: Record<string, string> = { pain: "痛点种草", review: "开箱测评", compare: "对比展示" };
|
||||
// 脚本风格预设(顺序对齐设计稿设定卡下拉:真实测评 / 痛点种草 / 小红书种草 / 开箱测评 / 对比展示)
|
||||
const WIZ_STYLE_LABEL: Record<string, string> = { real: "真实测评", pain: "痛点种草", xhs: "小红书种草", review: "开箱测评", compare: "对比展示" };
|
||||
const WIZ_PERSONA_LABEL: Record<string, string> = { urban: "都市白领女性", bestie: "闺蜜种草", ceo: "总裁亲选", reviewer: "专业测评师", mom: "实用宝妈", genz: "学生党" };
|
||||
const THEME_TIP = "好,用一句话描述主题(5-30 字),例如「熬夜党的早八续命面膜」,输入后点发送。";
|
||||
// 视频片段状态 pill 文案(色调走 statusPill:ok/info/err/neutral)
|
||||
@@ -371,7 +372,7 @@ export function PipelinePage(props: {
|
||||
onSaveProjectMeta?: (meta: Record<string, unknown>) => Promise<unknown>;
|
||||
onAdoptVideoVersion: (segmentId: string, versionId: string) => Promise<unknown>;
|
||||
onGenerateVoiceover: (payload: { items: Array<{ index: number; text: string }>; voice_type?: string }) => Promise<unknown>;
|
||||
onGenerateBaseAsset: (kind: "product" | "person" | "scene", prompt: string) => void;
|
||||
onGenerateBaseAsset: (kind: "product" | "person" | "scene", prompt: string, label?: string) => void;
|
||||
onAdoptBaseAsset: (groupId: string, assetId: string) => void | Promise<unknown>;
|
||||
onGenerateStoryboard: (prompt: string) => void;
|
||||
onSkipStoryboard: () => Promise<unknown>;
|
||||
@@ -425,9 +426,43 @@ export function PipelinePage(props: {
|
||||
// 后台刷新(action 后 refreshProjectDetail)回填的 metadata 同步到本地态,保证多端/重进一致
|
||||
useEffect(() => { setCastTags(project.metadata?.cast ?? []); }, [project.metadata?.cast]);
|
||||
useEffect(() => { setSceneTags(project.metadata?.scenes ?? []); }, [project.metadata?.scenes]);
|
||||
// 标签增删:先本地更新(即时反馈)再合并落库
|
||||
const saveCastTags = (next: string[]) => { setCastTags(next); void onSaveProjectMeta?.({ cast: next }); };
|
||||
const saveSceneTags = (next: string[]) => { setSceneTags(next); void onSaveProjectMeta?.({ scenes: next }); };
|
||||
// 行34/流程步骤3 · 标签「待确认修改」队列:点 chip × 或加新标签都不立即落库,而是排进这里,
|
||||
// 在脚本助手输入框上方显示为可撤销胶囊;点发送才把这些增删拼成指令整体重写脚本(AI 重写后会重新提取标签)。
|
||||
type TagEdit = { id: number; op: "add" | "remove"; kind: "cast" | "scene"; value: string };
|
||||
const [pendingTagEdits, setPendingTagEdits] = useState<TagEdit[]>([]);
|
||||
const tagEditId = () => Date.now() + Math.floor(Math.random() * 1000);
|
||||
const removingSet = (kind: "cast" | "scene") =>
|
||||
new Set(pendingTagEdits.filter((e) => e.kind === kind && e.op === "remove").map((e) => e.value));
|
||||
const addingList = (kind: "cast" | "scene") =>
|
||||
pendingTagEdits.filter((e) => e.kind === kind && e.op === "add").map((e) => e.value);
|
||||
// 删除某标签:若它本是「待加」则直接撤掉那条 add(等于没加过);否则排一条 remove
|
||||
function queueRemoveTag(kind: "cast" | "scene", value: string) {
|
||||
setPendingTagEdits((list) => {
|
||||
const addEntry = list.find((e) => e.kind === kind && e.op === "add" && e.value === value);
|
||||
if (addEntry) return list.filter((e) => e !== addEntry);
|
||||
if (list.some((e) => e.kind === kind && e.op === "remove" && e.value === value)) return list;
|
||||
return [...list, { id: tagEditId(), op: "remove", kind, value }];
|
||||
});
|
||||
}
|
||||
// 添加某标签:若它本是「待删」则撤掉删除(等于恢复);否则(且原本不存在)排一条 add
|
||||
function queueAddTag(kind: "cast" | "scene", raw: string) {
|
||||
const value = raw.trim();
|
||||
if (!value) return;
|
||||
setPendingTagEdits((list) => {
|
||||
const rmEntry = list.find((e) => e.kind === kind && e.op === "remove" && e.value === value);
|
||||
if (rmEntry) return list.filter((e) => e !== rmEntry);
|
||||
const base = kind === "cast" ? castTags : sceneTags;
|
||||
if (base.includes(value) || list.some((e) => e.kind === kind && e.op === "add" && e.value === value)) return list;
|
||||
return [...list, { id: tagEditId(), op: "add", kind, value }];
|
||||
});
|
||||
}
|
||||
const undoTagEdit = (id: number) => setPendingTagEdits((list) => list.filter((e) => e.id !== id));
|
||||
// 已落库标签的 × :已排队待删 → 撤销恢复;否则排一条待删
|
||||
function toggleRemoveTag(kind: "cast" | "scene", value: string) {
|
||||
const rm = pendingTagEdits.find((e) => e.kind === kind && e.op === "remove" && e.value === value);
|
||||
if (rm) undoTagEdit(rm.id); else queueRemoveTag(kind, value);
|
||||
}
|
||||
const tagEditVerb = (e: TagEdit) => `${e.op === "add" ? "添加" : "删除"}${e.kind === "cast" ? "人物" : "场景"}:${e.value}`;
|
||||
// 行34 · 「添加分镜」插入的本地空白可编辑卡(尚未落库;失焦有内容才真正 onAddShot)
|
||||
type DraftShot = { id: string; afterId: string | null; narration: string; visual: string };
|
||||
const [draftShots, setDraftShots] = useState<DraftShot[]>([]);
|
||||
@@ -654,7 +689,28 @@ export function PipelinePage(props: {
|
||||
}
|
||||
await runScriptGeneration(`AI 全生 · 风格:${styleLabel},目标人群:${personaLabel}。突出商品卖点,节奏紧凑,适合短视频投放`, `${sourceLabel}:${styleLabel} · ${personaLabel}`, "ai");
|
||||
}
|
||||
// 流程步骤3 · 把待确认的标签增删拼成给 AI 的重写指令 + 用户消息标签
|
||||
function tagEditsInstruction() {
|
||||
if (!pendingTagEdits.length) return "";
|
||||
const parts = pendingTagEdits.map((e) => `${e.op === "add" ? "添加" : "删除"}${e.kind === "cast" ? "人物" : "场景"}「${e.value}」`);
|
||||
return `请据此调整并整体重写脚本:${parts.join("、")}。其余分镜尽量保持不变。`;
|
||||
}
|
||||
// 发送框统一入口:设定卡开着 → 走 setup 生成;否则把「待确认标签增删 + 输入文字」合成一条整体重写
|
||||
function submitChat() {
|
||||
if (loading) return;
|
||||
if (setupOpen) { void runScriptWithSetup(); return; }
|
||||
const text = chatText.trim();
|
||||
const tagInstr = tagEditsInstruction();
|
||||
if (!text && !tagInstr) return;
|
||||
const prompt = [tagInstr, text].filter(Boolean).join("\n");
|
||||
const label = [pendingTagEdits.map(tagEditVerb).join(" · "), text].filter(Boolean).join(" · ");
|
||||
setChatText("");
|
||||
setChatAttachments([]);
|
||||
setPendingTagEdits([]);
|
||||
void runScriptGeneration(prompt, label || undefined);
|
||||
}
|
||||
function clearChat() {
|
||||
setPendingTagEdits([]);
|
||||
setChatText("");
|
||||
setChatAttachments([]);
|
||||
setChatMode("ai");
|
||||
@@ -1632,28 +1688,36 @@ export function PipelinePage(props: {
|
||||
<span className="pill neutral script-brief-pill"><span className="k">风格</span><span className="v" id="brief-style">{(() => { const k = project.metadata?.wizard?.script_style; return k ? (WIZ_STYLE_LABEL[k] || k) : "待确认"; })()}</span></span>
|
||||
<span className="pill neutral script-brief-pill"><span className="k">人物</span><span className="v" id="brief-persona">{(() => { const k = project.metadata?.wizard?.persona; return k ? (WIZ_PERSONA_LABEL[k] || k) : "待确认"; })()}</span></span>
|
||||
</div>
|
||||
{/* 行34 · 该脚本的人物 / 场景标签:可编辑(增删 chip);出现分镜后才展示 */}
|
||||
{/* 行34/流程步骤3 · 人物 / 场景标签:AI 出稿自动提取;增删不立即生效,排进待确认队列,
|
||||
点发送才整体重写。待删的灰删除线、待加的虚线高亮,均可在输入框上方胶囊撤销。 */}
|
||||
<div className="script-tags" id="script-tags">
|
||||
<div className="tag-group" data-kind="char">
|
||||
<span className="tg-lbl">// 人物</span>
|
||||
{castTags.map((tag, i) => (
|
||||
<span className="script-chip" key={`cast-${i}-${tag}`}>
|
||||
{tag}
|
||||
<button type="button" className="chip-x" aria-label={`移除人物 ${tag}`} onClick={() => saveCastTags(castTags.filter((_, j) => j !== i))}>×</button>
|
||||
</span>
|
||||
))}
|
||||
<AddTagInline placeholder="人物名" onAdd={(v) => { if (!castTags.includes(v)) saveCastTags([...castTags, v]); }} ariaLabel="添加人物" />
|
||||
</div>
|
||||
<div className="tag-group" data-kind="scene">
|
||||
<span className="tg-lbl">// 场景</span>
|
||||
{sceneTags.map((tag, i) => (
|
||||
<span className="script-chip" key={`scene-${i}-${tag}`}>
|
||||
{tag}
|
||||
<button type="button" className="chip-x" aria-label={`移除场景 ${tag}`} onClick={() => saveSceneTags(sceneTags.filter((_, j) => j !== i))}>×</button>
|
||||
</span>
|
||||
))}
|
||||
<AddTagInline placeholder="场景名" onAdd={(v) => { if (!sceneTags.includes(v)) saveSceneTags([...sceneTags, v]); }} ariaLabel="添加场景" />
|
||||
</div>
|
||||
{(["cast", "scene"] as const).map((kind) => {
|
||||
const baseTags = kind === "cast" ? castTags : sceneTags;
|
||||
const removing = removingSet(kind);
|
||||
const adds = addingList(kind);
|
||||
const label = kind === "cast" ? "人物" : "场景";
|
||||
return (
|
||||
<div className="tag-group" data-kind={kind === "cast" ? "char" : "scene"} key={kind}>
|
||||
<span className="tg-lbl">// {label}</span>
|
||||
{baseTags.map((tag, i) => {
|
||||
const pendingRemove = removing.has(tag);
|
||||
return (
|
||||
<span className={`script-chip${pendingRemove ? " pending-remove" : ""}`} key={`${kind}-${i}-${tag}`}>
|
||||
{tag}
|
||||
<button type="button" className="chip-x" aria-label={`${pendingRemove ? "恢复" : "移除"}${label} ${tag}`} title={pendingRemove ? "待删除 · 点恢复" : undefined} onClick={() => toggleRemoveTag(kind, tag)}>{pendingRemove ? "↺" : "×"}</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{adds.map((tag) => (
|
||||
<span className="script-chip pending-add" key={`${kind}-add-${tag}`} title="待添加 · 点发送生效">
|
||||
{tag}
|
||||
<button type="button" className="chip-x" aria-label={`撤销添加${label} ${tag}`} onClick={() => queueRemoveTag(kind, tag)}>×</button>
|
||||
</span>
|
||||
))}
|
||||
<AddTagInline placeholder={`${label}名`} onAdd={(v) => queueAddTag(kind, v)} ariaLabel={`添加${label}`} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span className="spacer"></span>
|
||||
<button className="btn btn-ghost btn-sm" type="button" id="chat-regen-btn" disabled={loading} onClick={() => void runScriptGeneration("整体重新生成 · 突出商品卖点,节奏紧凑,适合短视频投放", "整体重写")}>↻ 整体重写</button>
|
||||
@@ -1824,6 +1888,15 @@ export function PipelinePage(props: {
|
||||
</div>
|
||||
<div className="chat-input">
|
||||
<div className="chat-input-card">
|
||||
{/* 流程步骤3 · 待确认的标签增删:悬浮显示「添加/删除人物/场景:xxx」,× 撤销,点发送整体重写 */}
|
||||
<div className="chat-tagedit-row" id="chat-tagedit-row" hidden={pendingTagEdits.length === 0}>
|
||||
{pendingTagEdits.map((edit) => (
|
||||
<span className={`chip tagedit-chip ${edit.op}`} key={edit.id} title={`${tagEditVerb(edit)}(点发送确定更改)`}>
|
||||
{tagEditVerb(edit)}
|
||||
<button type="button" aria-label={`撤销 ${tagEditVerb(edit)}`} style={{ marginLeft: 4, background: "none", border: 0, cursor: "pointer", color: "inherit" }} onClick={() => undoTagEdit(edit.id)}>×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="chat-attach-row" id="chat-attach-row" hidden={chatAttachments.length === 0}>
|
||||
{chatAttachments.map((att, index) => (
|
||||
<span className="chip" key={`${att.name}-${index}`} style={{ marginRight: 6 }}>
|
||||
@@ -1838,13 +1911,7 @@ export function PipelinePage(props: {
|
||||
// 行31 · Enter 发送,Shift+Enter 换行(输入法组合期不触发)
|
||||
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
||||
event.preventDefault();
|
||||
if (loading) return;
|
||||
if (setupOpen) { void runScriptWithSetup(); return; }
|
||||
const text = chatText.trim();
|
||||
if (!text) return;
|
||||
setChatText("");
|
||||
setChatAttachments([]);
|
||||
void runScriptGeneration(text);
|
||||
submitChat();
|
||||
}
|
||||
}}></textarea>
|
||||
<input ref={chatFileRef} type="file" accept=".txt,.md,.text,text/plain" style={{ display: "none" }} onChange={onPickScriptFile} />
|
||||
@@ -1853,7 +1920,7 @@ export function PipelinePage(props: {
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14M5 12h14" /></svg>
|
||||
</button>
|
||||
<span className="spacer"></span>
|
||||
<button className="chat-send-btn" id="chat-send-btn" type="button" title="发送" aria-label="发送" disabled={loading || (!setupOpen && !chatText.trim())} onClick={() => { if (setupOpen) { void runScriptWithSetup(); return; } const text = chatText.trim(); if (!text) return; setChatText(""); setChatAttachments([]); void runScriptGeneration(text); }}>
|
||||
<button className="chat-send-btn" id="chat-send-btn" type="button" title="发送" aria-label="发送" disabled={loading || (!setupOpen && !chatText.trim() && pendingTagEdits.length === 0)} onClick={submitChat}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1948,19 +2015,58 @@ export function PipelinePage(props: {
|
||||
|
||||
{(["person", "scene"] as const).map((kind) => {
|
||||
const list = groupsByKind(kind);
|
||||
// 流程步骤4 · 人物←脚本提取的人物标签,场景←场景标签;提示词用脚本提取时 AI 生成的(metadata.*_prompts)
|
||||
const tags = kind === "person" ? castTags : sceneTags;
|
||||
const promptMap = (kind === "person" ? project.metadata?.cast_prompts : project.metadata?.scene_prompts) || {};
|
||||
const fallbackPrompt = (tag: string) => kind === "person"
|
||||
? `${tag},真人模特出镜,自然光,${productName} 上身展示,9:16 竖屏`
|
||||
: `${tag},使用场景,氛围统一,干净构图,9:16 竖屏`;
|
||||
const tagPrompt = (tag: string) => (promptMap[tag]?.trim() || fallbackPrompt(tag));
|
||||
// 已生成的标签(按 group.metadata.label 归类),其余标签显示为「待生成」seed 卡
|
||||
const generatedLabels = new Set(list.map((g) => g.metadata?.label).filter(Boolean) as string[]);
|
||||
const pendingTags = tags.filter((t) => !generatedLabels.has(t));
|
||||
const genPrompt = kind === "person"
|
||||
? `${productName} 真人模特出镜,自然光,商品上身展示,9:16 竖屏`
|
||||
: `${productName} 使用场景,氛围统一,干净构图,9:16 竖屏`;
|
||||
return (
|
||||
<section className="asset-sec" id={`asset-sec-${kind}`} key={kind}>
|
||||
<div className="sec-h">
|
||||
<h3>{KIND_LABEL[kind]} · {list.length} 个</h3>
|
||||
<h3>{KIND_LABEL[kind]} · {list.length}{tags.length ? ` / ${tags.length}` : ""} 个</h3>
|
||||
<span className="spacer"></span>
|
||||
<button className="btn-aigen" type="button" data-stop disabled={loading} onClick={() => onGenerateBaseAsset(kind, genPrompt)}>
|
||||
<svg className="ai-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3z" /><path d="M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7L19 14z" /></svg>
|
||||
AI 生成{KIND_LABEL[kind]}
|
||||
自定义{KIND_LABEL[kind]}
|
||||
</button>
|
||||
</div>
|
||||
{/* 流程步骤4 · 脚本提取出但还没生成的人物/场景:逐个 seed 卡,显示 AI 提示词,可改后生成 */}
|
||||
{pendingTags.length > 0 && (
|
||||
<div className="asset-grid-2" style={{ marginBottom: list.length ? "10px" : 0 }}>
|
||||
{pendingTags.map((tag) => {
|
||||
const seedKey = `seed:${kind}:${tag}`;
|
||||
const promptValue = assetPromptDraft[seedKey] ?? tagPrompt(tag);
|
||||
return (
|
||||
<div className="asset-card-2 asset-seed" data-asset-kind={kind} data-seed-tag={tag} key={seedKey}>
|
||||
<div className="placeholder thumb-2"><span className="ph-frame">{tag} · 待生成</span></div>
|
||||
<div className="body-2">
|
||||
<div className="hstack">
|
||||
<strong style={{ fontSize: "14px" }}>{tag}</strong>
|
||||
<span className="spacer"></span>
|
||||
<span className="pill neutral"><span className="dot"></span>来自脚本</span>
|
||||
</div>
|
||||
<textarea className="asset-prompt-edit" rows={3} placeholder="描述这个素材的提示词…" value={promptValue} onChange={(e) => setAssetPromptDraft((m) => ({ ...m, [seedKey]: e.target.value }))} />
|
||||
<div className="asset-card-actions">
|
||||
<span className="spacer"></span>
|
||||
<button className="btn btn-primary btn-sm" type="button" disabled={loading} onClick={() => onGenerateBaseAsset(kind, (promptValue.trim() || tagPrompt(tag)), tag)}>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 4 }}><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z" /></svg>
|
||||
AI 生成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{list.length ? (
|
||||
<div className="asset-grid-2">
|
||||
{list.map((group, gi) => {
|
||||
@@ -1968,14 +2074,17 @@ export function PipelinePage(props: {
|
||||
const cands = (group.candidate_assets ?? []).filter((id) => id !== group.adopted_asset).slice(0, 4);
|
||||
// 行38 · 可编辑提示词:草稿优先,落空回退原 prompt;重跑/替换都带它
|
||||
const promptValue = assetPromptDraft[group.id] ?? group.prompt ?? "";
|
||||
// 流程步骤4 · 该卡对应的脚本标签(若有),作卡片标题并在重跑/替换时保留归类
|
||||
const label = group.metadata?.label || "";
|
||||
const cardName = label || assetName(group.adopted_asset) || `${KIND_LABEL[kind]} ${gi + 1}`;
|
||||
return (
|
||||
<div className="asset-card-2" data-asset-kind={kind} data-asset-id={group.id} key={group.id}>
|
||||
<div className={`placeholder thumb-2${mainUrl ? " has-mock-media" : ""}`} style={mainUrl ? mediaStyle(mainUrl) : undefined}>
|
||||
<span className="ph-frame">{assetName(group.adopted_asset) || `${KIND_LABEL[kind]} ${gi + 1}`}</span>
|
||||
<span className="ph-frame">{cardName}</span>
|
||||
</div>
|
||||
<div className="body-2">
|
||||
<div className="hstack">
|
||||
<strong style={{ fontSize: "14px" }}>{assetName(group.adopted_asset) || `${KIND_LABEL[kind]} ${gi + 1}`}</strong>
|
||||
<strong style={{ fontSize: "14px" }}>{cardName}</strong>
|
||||
<span className="spacer"></span>
|
||||
{group.adopted_asset
|
||||
? <span className="pill ok"><span className="dot"></span>已采用</span>
|
||||
@@ -1998,12 +2107,12 @@ export function PipelinePage(props: {
|
||||
)}
|
||||
{/* 行38 · 重跑 / 替换:重跑=据当前提示词重新生成;替换=有候选先采下一张,无候选回退重生成 */}
|
||||
<div className="asset-card-actions">
|
||||
<button className="btn btn-ghost btn-sm" type="button" disabled={loading} onClick={() => onGenerateBaseAsset(kind, (promptValue.trim() || genPrompt))}>
|
||||
<button className="btn btn-ghost btn-sm" type="button" disabled={loading} onClick={() => onGenerateBaseAsset(kind, (promptValue.trim() || genPrompt), label)}>
|
||||
<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>
|
||||
重跑
|
||||
</button>
|
||||
<span className="spacer"></span>
|
||||
<button className="btn btn-ghost btn-sm" type="button" disabled={loading} onClick={() => { if (cands.length) { void onAdoptBaseAsset(group.id, cands[0]); } else { onGenerateBaseAsset(kind, (promptValue.trim() || genPrompt)); } }}>替换</button>
|
||||
<button className="btn btn-ghost btn-sm" type="button" disabled={loading} onClick={() => { if (cands.length) { void onAdoptBaseAsset(group.id, cands[0]); } else { onGenerateBaseAsset(kind, (promptValue.trim() || genPrompt), label); } }}>替换</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user