|
|
|
@@ -605,6 +605,9 @@ type GenBatch = {
|
|
|
|
|
pendingIds?: string[];
|
|
|
|
|
/** 该批全部后端任务 id:删除/恢复批次时使用稳定锚点,终态批次也必须保留。 */
|
|
|
|
|
taskIds?: string[];
|
|
|
|
|
/** 补图/单图重跑任务:删掉其成图时不应减少原始批次的失败格数量。 */
|
|
|
|
|
rerunTaskIds?: string[];
|
|
|
|
|
failedTaskIds?: string[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@@ -643,7 +646,7 @@ export function ImageWorkbenchPage({
|
|
|
|
|
modelConfigs: ModelConfig[];
|
|
|
|
|
onBack: () => void;
|
|
|
|
|
navigate?: (page: Page) => void;
|
|
|
|
|
onGenerate: (payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; model_id?: string; ratio?: string; image_model?: string; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string; onSubmitted?: (taskIds: string[], batchId?: string) => void }) => Promise<{ assets: Asset[]; conversation_id?: string; batch_id?: string } | null>;
|
|
|
|
|
onGenerate: (payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; model_id?: string; ratio?: string; image_model?: string; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string; retry_of_task_id?: string; onSubmitted?: (taskIds: string[], batchId?: string) => void }) => Promise<{ assets: Asset[]; conversation_id?: string; batch_id?: string } | null>;
|
|
|
|
|
onResume?: (mode: "image" | "model" | "cover", ids: string[]) => Promise<{ assets: Asset[] } | null>;
|
|
|
|
|
/** 行19:从商品页带入的初始商品 id(可选);未传则回退到第一个商品 */
|
|
|
|
|
initialProductId?: string;
|
|
|
|
@@ -876,6 +879,11 @@ export function ImageWorkbenchPage({
|
|
|
|
|
const result: GenBatch[] = [];
|
|
|
|
|
for (const [key, list] of groups) {
|
|
|
|
|
// R109:成功但成图已全部删除(软删进垃圾桶)的任务不再回显 —— 生成记录与资产库数据绑定
|
|
|
|
|
const coveredFailureIds = new Set(
|
|
|
|
|
list.filter((t) => t.rerun && t.status === "succeeded" && t.retry_of_task_id).map((t) => t.retry_of_task_id!)
|
|
|
|
|
);
|
|
|
|
|
// 旧补图任务没有记录它替代的失败格;按一张补图抵一张失败格兼容,避免历史批次显示超过原始张数。
|
|
|
|
|
const legacyReplacementCount = list.filter((t) => t.rerun && t.status === "succeeded" && !t.retry_of_task_id).length;
|
|
|
|
|
const live = list.filter((t) => t.status !== "succeeded" || (t.assets || []).length > 0);
|
|
|
|
|
if (!live.length) continue;
|
|
|
|
|
const assets = live.flatMap((t) => t.assets || []);
|
|
|
|
@@ -885,14 +893,19 @@ export function ImageWorkbenchPage({
|
|
|
|
|
const status: GenBatch["status"] = hasRunning ? "generating" : assets.length > 0 ? "done" : "failed";
|
|
|
|
|
// 应出张数只数原始任务:重跑/补图任务(rerun)是替补,计入会把失败格越滚越多
|
|
|
|
|
// (原失败任务仍在批里,重跑一次多一个格)。成功补图后 results 增长,失败格自然收掉。
|
|
|
|
|
const failedTaskIds = list
|
|
|
|
|
.filter((t) => !t.rerun && ["failed", "cancelled"].includes(t.status) && !(t.assets || []).length && !coveredFailureIds.has(t.id))
|
|
|
|
|
.map((t) => t.id)
|
|
|
|
|
.slice(legacyReplacementCount);
|
|
|
|
|
const intended = live.filter((t) => !t.rerun).length;
|
|
|
|
|
const count = hasRunning ? Math.max(1, intended) : Math.max(1, assets.length + failedTaskIds.length);
|
|
|
|
|
// 该批用过的参考图(后端解析回 {id,name,url}):批次头回显 + 重跑凭 assetId 原样复用
|
|
|
|
|
const refSrc = live.find((t) => (t.reference_images || []).length)?.reference_images || [];
|
|
|
|
|
result.push({
|
|
|
|
|
id: key,
|
|
|
|
|
prompt: live[0]?.prompt || "",
|
|
|
|
|
ratio: live[0]?.ratio || meta.ratio,
|
|
|
|
|
count: Math.max(1, intended),
|
|
|
|
|
count,
|
|
|
|
|
status,
|
|
|
|
|
results: assets,
|
|
|
|
|
ts: new Date(live[0]?.created_at || Date.now()).getTime(),
|
|
|
|
@@ -900,6 +913,8 @@ export function ImageWorkbenchPage({
|
|
|
|
|
// 真 batch_id 才能作重跑归属;老任务无 batch_id 时 key=任务 id,不能带给后端
|
|
|
|
|
backendBatchId: live[0]?.batch_id || undefined,
|
|
|
|
|
taskIds: live.map((t) => t.id),
|
|
|
|
|
rerunTaskIds: live.filter((t) => t.rerun).map((t) => t.id),
|
|
|
|
|
failedTaskIds,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// 旧批次在上、新批次在下(对话流自上而下时间序)
|
|
|
|
@@ -920,6 +935,11 @@ export function ImageWorkbenchPage({
|
|
|
|
|
const TERMINAL = new Set(["succeeded", "failed", "cancelled"]);
|
|
|
|
|
const result: GenBatch[] = [];
|
|
|
|
|
for (const [key, list] of groups) {
|
|
|
|
|
const coveredFailureIds = new Set(
|
|
|
|
|
list.filter((t) => t.rerun && t.status === "succeeded" && t.retry_of_task_id).map((t) => t.retry_of_task_id!)
|
|
|
|
|
);
|
|
|
|
|
// 同图片创作:历史补图没有目标格关联时,仍按一张补图替代一张失败格回放。
|
|
|
|
|
const legacyReplacementCount = list.filter((t) => t.rerun && t.status === "succeeded" && !t.retry_of_task_id).length;
|
|
|
|
|
const live = list.filter((t) => t.status !== "succeeded" || (t.assets || []).length > 0);
|
|
|
|
|
if (!live.length) continue;
|
|
|
|
|
const assets = live.flatMap((t) => t.assets || []);
|
|
|
|
@@ -928,12 +948,17 @@ export function ImageWorkbenchPage({
|
|
|
|
|
const first = live[0];
|
|
|
|
|
const platformKey = first.platform_id ? PLATFORM_KEY_MAP[first.platform_id] : "";
|
|
|
|
|
// 应出张数只数原始任务(重跑/补图任务是替补,不计入,与 batchesFromConvTasks 同理)
|
|
|
|
|
const failedTaskIds = list
|
|
|
|
|
.filter((t) => !t.rerun && ["failed", "cancelled"].includes(t.status) && !(t.assets || []).length && !coveredFailureIds.has(t.id))
|
|
|
|
|
.map((t) => t.id)
|
|
|
|
|
.slice(legacyReplacementCount);
|
|
|
|
|
const intended = live.filter((t) => !t.rerun).length;
|
|
|
|
|
const count = hasRunning ? Math.max(1, intended) : Math.max(1, assets.length + failedTaskIds.length);
|
|
|
|
|
result.push({
|
|
|
|
|
id: key,
|
|
|
|
|
prompt: first.prompt || "",
|
|
|
|
|
ratio: first.ratio || meta.ratio,
|
|
|
|
|
count: Math.max(1, intended),
|
|
|
|
|
count,
|
|
|
|
|
status,
|
|
|
|
|
results: assets,
|
|
|
|
|
ts: new Date(first.created_at || Date.now()).getTime(),
|
|
|
|
@@ -942,6 +967,8 @@ export function ImageWorkbenchPage({
|
|
|
|
|
platformIds: platformKey ? [platformKey] : undefined,
|
|
|
|
|
backendBatchId: first.batch_id || undefined,
|
|
|
|
|
taskIds: live.map((t) => t.id),
|
|
|
|
|
rerunTaskIds: live.filter((t) => t.rerun).map((t) => t.id),
|
|
|
|
|
failedTaskIds,
|
|
|
|
|
// 续轮询要带上整批任务 id(含已终态的):onResume 的结果会整体替换 results,
|
|
|
|
|
// 只传未完成 id 会把已出的好图从结果里丢掉(终态任务首轮轮询即回带成图,秒完成)。
|
|
|
|
|
pendingIds: hasRunning ? live.map((t) => t.id) : undefined,
|
|
|
|
@@ -1062,6 +1089,8 @@ export function ImageWorkbenchPage({
|
|
|
|
|
appendToBatchId?: string;
|
|
|
|
|
/** 重跑/补图:原批次的后端 batch_id,带给后端沿用 → 新任务归回原批次(刷新后不裂新记录) */
|
|
|
|
|
batchId?: string;
|
|
|
|
|
/** 单格重跑要替代的原失败任务。 */
|
|
|
|
|
retryOfTaskId?: string;
|
|
|
|
|
}) {
|
|
|
|
|
// 原地重跑/追加:沿用原批次 id,不再生成新 id、不再新增卡片
|
|
|
|
|
const batchId = opts.reuseBatchId || opts.appendToBatchId || `b-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
|
|
@@ -1115,8 +1144,16 @@ export function ImageWorkbenchPage({
|
|
|
|
|
// 带上当前对话 id(空则后端自动开一条并回传);conversation_id 用 ref 取最新值,避免闭包旧值
|
|
|
|
|
// batch_id:重跑/补图带原批次 id → 后端沿用,记录归回原批次(刷新后不裂新聊天记录)
|
|
|
|
|
// onSubmitted:提交成功拿到任务 id 记进本批 pendingIds → 切走再回来由后端记录接续轮询(R100)
|
|
|
|
|
const result = await onGenerate({ prompt: opts.prompt, mode, count: opts.count, product_id: opts.productId, model_id: opts.modelId, ratio: opts.ratio, image_model: genModel, platform_id: opts.platformId, conversation_id: activeConvRef.current || undefined, reference_image_ids: referenceImageIds, batch_id: opts.batchId,
|
|
|
|
|
onSubmitted: (taskIds, submittedBatchId) => setBatches((prev) => prev.map((b) => (b.id === batchId ? { ...b, pendingIds: taskIds, taskIds, backendBatchId: b.backendBatchId || submittedBatchId } : b))) });
|
|
|
|
|
const result = await onGenerate({ prompt: opts.prompt, mode, count: opts.count, product_id: opts.productId, model_id: opts.modelId, ratio: opts.ratio, image_model: genModel, platform_id: opts.platformId, conversation_id: activeConvRef.current || undefined, reference_image_ids: referenceImageIds, batch_id: opts.batchId, retry_of_task_id: opts.retryOfTaskId,
|
|
|
|
|
onSubmitted: (taskIds, submittedBatchId) => {
|
|
|
|
|
setBatches((prev) => prev.map((b) => {
|
|
|
|
|
if (b.id !== batchId) return b;
|
|
|
|
|
const allTaskIds = opts.appendToBatchId ? [...new Set([...(b.taskIds || []), ...taskIds])] : taskIds;
|
|
|
|
|
const rerunTaskIds = opts.appendToBatchId ? [...new Set([...(b.rerunTaskIds || []), ...taskIds])] : b.rerunTaskIds;
|
|
|
|
|
return { ...b, pendingIds: taskIds, taskIds: allTaskIds, rerunTaskIds, backendBatchId: b.backendBatchId || submittedBatchId };
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 首发新对话:把后端建的对话登记进左栏并设为 active;刷新列表拿到真标题/计数
|
|
|
|
|
const convId = result?.conversation_id;
|
|
|
|
|
if (convId && convId !== activeConvRef.current) {
|
|
|
|
@@ -1132,7 +1169,18 @@ export function ImageWorkbenchPage({
|
|
|
|
|
if (opts.appendToBatchId) {
|
|
|
|
|
const seen = new Set(b.results.map((a) => a.id));
|
|
|
|
|
const merged = [...b.results, ...newAssets.filter((a) => !a.id || !seen.has(a.id))];
|
|
|
|
|
return { ...b, backendBatchId, status: merged.length ? ("done" as const) : ("failed" as const), results: merged, pendingIds: undefined };
|
|
|
|
|
const failedTaskIds = opts.retryOfTaskId && newAssets.length
|
|
|
|
|
? b.failedTaskIds?.filter((id) => id !== opts.retryOfTaskId)
|
|
|
|
|
: b.failedTaskIds;
|
|
|
|
|
return {
|
|
|
|
|
...b,
|
|
|
|
|
backendBatchId,
|
|
|
|
|
status: merged.length ? ("done" as const) : ("failed" as const),
|
|
|
|
|
results: merged,
|
|
|
|
|
failedTaskIds,
|
|
|
|
|
count: failedTaskIds ? Math.max(1, merged.length + failedTaskIds.length) : b.count,
|
|
|
|
|
pendingIds: undefined
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return { ...b, backendBatchId, status: newAssets.length ? ("done" as const) : ("failed" as const), results: newAssets, pendingIds: undefined };
|
|
|
|
|
}));
|
|
|
|
@@ -1221,9 +1269,21 @@ export function ImageWorkbenchPage({
|
|
|
|
|
// 单张:从批次里摘掉;整批删空(且不在生成中)则整卡移除
|
|
|
|
|
setBatches((prev) => prev.flatMap((b) => {
|
|
|
|
|
if (b.id !== target.batchId) return [b];
|
|
|
|
|
const deletedAsset = b.results.find((a) => a.id === target.assetId);
|
|
|
|
|
const deletedTaskId = deletedAsset?.origin_task || undefined;
|
|
|
|
|
const results = b.results.filter((a) => a.id !== target.assetId);
|
|
|
|
|
if (!results.length && b.status === "done") return [];
|
|
|
|
|
return [{ ...b, results, count: Math.max(1, b.count - 1) }];
|
|
|
|
|
const taskIds = deletedTaskId ? b.taskIds?.filter((id) => id !== deletedTaskId) : b.taskIds;
|
|
|
|
|
const rerunTaskIds = deletedTaskId ? b.rerunTaskIds?.filter((id) => id !== deletedTaskId) : b.rerunTaskIds;
|
|
|
|
|
// 单图删除只移除该图对应的任务;同批仍有失败、成功或补图任务时,批次卡必须保留。
|
|
|
|
|
if (taskIds && taskIds.length === 0) return [];
|
|
|
|
|
return [{
|
|
|
|
|
...b,
|
|
|
|
|
results,
|
|
|
|
|
taskIds,
|
|
|
|
|
rerunTaskIds,
|
|
|
|
|
count: b.failedTaskIds ? Math.max(1, results.length + b.failedTaskIds.length) : Math.max(1, b.count - 1),
|
|
|
|
|
status: b.status === "generating" ? "generating" : results.length ? "done" : "failed"
|
|
|
|
|
}];
|
|
|
|
|
}));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -1252,7 +1312,7 @@ export function ImageWorkbenchPage({
|
|
|
|
|
|
|
|
|
|
/* 单图「再生成」/「重跑这张」(§4.18 悬浮① / YYX#5):
|
|
|
|
|
PMC#25——补一张图进原批次卡(不新开任务、不覆盖已有好图)。 */
|
|
|
|
|
async function regenSingleImage(src: GenBatch) {
|
|
|
|
|
async function regenSingleImage(src: GenBatch, retryOfTaskId?: string) {
|
|
|
|
|
await startBatch({
|
|
|
|
|
prompt: src.prompt,
|
|
|
|
|
ratio: src.ratio,
|
|
|
|
@@ -1266,7 +1326,8 @@ export function ImageWorkbenchPage({
|
|
|
|
|
// 原批次的参考图 + 后端批次 id 一并带回:补的这张仍参考原素材,且归回原批次不裂新记录
|
|
|
|
|
refs: src.refs,
|
|
|
|
|
batchId: src.backendBatchId,
|
|
|
|
|
appendToBatchId: src.id
|
|
|
|
|
appendToBatchId: src.id,
|
|
|
|
|
retryOfTaskId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1335,7 +1396,9 @@ export function ImageWorkbenchPage({
|
|
|
|
|
② 失败格占位 + 单张重跑。R109:成图自动入资产库,不再有「已入库」角标/入库切换。 */
|
|
|
|
|
function renderBatchGrid(batch: GenBatch) {
|
|
|
|
|
const generating = batch.status === "generating";
|
|
|
|
|
const n = batch.results.length || batch.count;
|
|
|
|
|
const n = generating
|
|
|
|
|
? Math.max(batch.results.length, batch.count)
|
|
|
|
|
: Math.max(1, batch.results.length + (batch.failedTaskIds?.length ?? Math.max(0, batch.count - batch.results.length)));
|
|
|
|
|
const cols = n >= 4 ? 4 : 2;
|
|
|
|
|
const ratioBatchVar = batch.ratio.replace(":", " / ");
|
|
|
|
|
return (
|
|
|
|
@@ -1345,11 +1408,12 @@ export function ImageWorkbenchPage({
|
|
|
|
|
>
|
|
|
|
|
{/* YYX#5:渲染满 count 个格子 —— 已出图的格用真图;非生成中又缺图的格 = 该张失败,
|
|
|
|
|
给失败占位符 + 单张重跑,不再因「部分失败」整批不显示失败格。 */}
|
|
|
|
|
{Array.from({ length: Math.max(batch.count, batch.results.length) }).map((_, index) => {
|
|
|
|
|
{Array.from({ length: n }).map((_, index) => {
|
|
|
|
|
const asset = batch.results[index];
|
|
|
|
|
const failedTaskId = batch.failedTaskIds?.[index - batch.results.length];
|
|
|
|
|
const url = asset?.files?.[0]?.preview_url;
|
|
|
|
|
const assetId = asset?.id || "";
|
|
|
|
|
const failed = !generating && !asset; // 非生成中且该格无图 → 这张失败了
|
|
|
|
|
const failed = !generating && !asset && (Boolean(failedTaskId) || !batch.failedTaskIds); // 非生成中且该格无图 → 这张失败了
|
|
|
|
|
const key = assetId || `ph-${index}`;
|
|
|
|
|
const cellKey = `${batch.id}:${assetId || key}`;
|
|
|
|
|
return (
|
|
|
|
@@ -1367,7 +1431,7 @@ export function ImageWorkbenchPage({
|
|
|
|
|
<span className="gen-failed">
|
|
|
|
|
<X size={18} />
|
|
|
|
|
<span className="gf-text">这张生成失败</span>
|
|
|
|
|
<button type="button" className="gf-retry" onClick={() => regenSingleImage(batch)}>
|
|
|
|
|
<button type="button" className="gf-retry" onClick={() => regenSingleImage(batch, failedTaskId)}>
|
|
|
|
|
<RefreshCw size={12} />
|
|
|
|
|
重跑这张
|
|
|
|
|
</button>
|
|
|
|
|