fix(agent): preserve detail replacement results
This commit is contained in:
parent
7c9fd8a796
commit
0a81f15f11
@ -8,6 +8,11 @@ import {
|
||||
isAgentProjectRow,
|
||||
} from "@/lib/agents/agent-project-compat";
|
||||
import { normalizeAgentGenerationProjectPersistedState } from "@/lib/agents/generation-project-state";
|
||||
import type {
|
||||
AgentGenerationProjectPersistedState,
|
||||
AgentGenerationProjectReplacementResultState,
|
||||
AgentGenerationProjectResultCollections,
|
||||
} from "@/types/agent";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
@ -25,6 +30,144 @@ function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
||||
};
|
||||
}
|
||||
|
||||
const RESULT_COLLECTION_KEYS = [
|
||||
"scene_generation",
|
||||
"detail_generation",
|
||||
"detail_replacement",
|
||||
"sku_replacement",
|
||||
] as const;
|
||||
|
||||
const REPLACEMENT_RESULT_KEYS = [
|
||||
"detail_replacement",
|
||||
"sku_replacement",
|
||||
] as const;
|
||||
|
||||
function normalizeProjectResultUrlList(urls: string[] | null | undefined) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(urls || [])
|
||||
.map((url) => String(url || "").trim())
|
||||
.filter((url) => /^https?:\/\//i.test(url))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function mergeResultCollections(
|
||||
existing: AgentGenerationProjectResultCollections | null | undefined,
|
||||
incoming: AgentGenerationProjectResultCollections | null | undefined
|
||||
): AgentGenerationProjectResultCollections {
|
||||
return RESULT_COLLECTION_KEYS.reduce<AgentGenerationProjectResultCollections>(
|
||||
(next, key) => {
|
||||
next[key] = normalizeProjectResultUrlList([
|
||||
...normalizeProjectResultUrlList(existing?.[key]),
|
||||
...normalizeProjectResultUrlList(incoming?.[key]),
|
||||
]);
|
||||
return next;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
function getResultCollectionUrlSet(
|
||||
collections: AgentGenerationProjectResultCollections | null | undefined
|
||||
) {
|
||||
return new Set(
|
||||
RESULT_COLLECTION_KEYS.flatMap((key) => normalizeProjectResultUrlList(collections?.[key]))
|
||||
);
|
||||
}
|
||||
|
||||
function removeDeletedUrlsFromCollections(
|
||||
collections: AgentGenerationProjectResultCollections,
|
||||
deletedUrls: Set<string>
|
||||
): AgentGenerationProjectResultCollections {
|
||||
return RESULT_COLLECTION_KEYS.reduce<AgentGenerationProjectResultCollections>(
|
||||
(next, key) => {
|
||||
next[key] = normalizeProjectResultUrlList(collections[key]).filter(
|
||||
(url) => !deletedUrls.has(url)
|
||||
);
|
||||
return next;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
function mergeReplacementResultState(
|
||||
existing: AgentGenerationProjectReplacementResultState | null | undefined,
|
||||
incoming: AgentGenerationProjectReplacementResultState | null | undefined,
|
||||
deletedUrls: Set<string>
|
||||
): AgentGenerationProjectReplacementResultState {
|
||||
return REPLACEMENT_RESULT_KEYS.reduce<AgentGenerationProjectReplacementResultState>(
|
||||
(next, key) => {
|
||||
const existingModeState = existing?.[key] || {};
|
||||
const incomingModeState = incoming?.[key] || {};
|
||||
const mergedModeState = { ...existingModeState, ...incomingModeState };
|
||||
next[key] = Object.fromEntries(
|
||||
Object.entries(mergedModeState).filter(([, record]) => {
|
||||
const url = String(record?.url || "").trim();
|
||||
return !url || !deletedUrls.has(url);
|
||||
})
|
||||
);
|
||||
return next;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
function getReplacementResultUrls(
|
||||
state: AgentGenerationProjectReplacementResultState | null | undefined
|
||||
) {
|
||||
return REPLACEMENT_RESULT_KEYS.flatMap((key) =>
|
||||
Object.values(state?.[key] || {})
|
||||
.map((record) => String(record?.url || "").trim())
|
||||
.filter((url) => /^https?:\/\//i.test(url))
|
||||
);
|
||||
}
|
||||
|
||||
function getAllResultUrlsFromState(state: AgentGenerationProjectPersistedState) {
|
||||
return normalizeProjectResultUrlList([
|
||||
...normalizeProjectResultUrlList(state.resultImageUrls),
|
||||
...RESULT_COLLECTION_KEYS.flatMap((key) =>
|
||||
normalizeProjectResultUrlList(state.resultCollections?.[key])
|
||||
),
|
||||
...getReplacementResultUrls(state.replacementResultState),
|
||||
]);
|
||||
}
|
||||
|
||||
function mergeAgentProjectStateForSave(params: {
|
||||
existingState: AgentGenerationProjectPersistedState;
|
||||
incomingState: unknown;
|
||||
}) {
|
||||
const existingState = normalizeAgentGenerationProjectPersistedState(params.existingState);
|
||||
const incomingState = normalizeAgentGenerationProjectPersistedState(params.incomingState);
|
||||
const deletedResultCollections = mergeResultCollections(
|
||||
existingState.deletedResultCollections,
|
||||
incomingState.deletedResultCollections
|
||||
);
|
||||
const deletedUrls = getResultCollectionUrlSet(deletedResultCollections);
|
||||
const replacementResultState = mergeReplacementResultState(
|
||||
existingState.replacementResultState,
|
||||
incomingState.replacementResultState,
|
||||
deletedUrls
|
||||
);
|
||||
const resultCollections = removeDeletedUrlsFromCollections(
|
||||
mergeResultCollections(existingState.resultCollections, incomingState.resultCollections),
|
||||
deletedUrls
|
||||
);
|
||||
|
||||
return normalizeAgentGenerationProjectPersistedState({
|
||||
...incomingState,
|
||||
resultCollections,
|
||||
deletedResultCollections,
|
||||
replacementResultState,
|
||||
resultImageUrls: normalizeProjectResultUrlList([
|
||||
...getAllResultUrlsFromState(existingState),
|
||||
...getAllResultUrlsFromState(incomingState),
|
||||
...RESULT_COLLECTION_KEYS.flatMap((key) => resultCollections[key] || []),
|
||||
...getReplacementResultUrls(replacementResultState),
|
||||
]).filter((url) => !deletedUrls.has(url)),
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@ -79,16 +222,28 @@ export async function PUT(
|
||||
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
const title = body.title !== undefined ? String(body.title || "").trim() || "未命名生图项目" : undefined;
|
||||
const title =
|
||||
body.title !== undefined
|
||||
? String(body.title || "").trim() || "未命名生图项目"
|
||||
: existingProject.title;
|
||||
const description =
|
||||
body.description !== undefined ? String(body.description || "").trim() || null : undefined;
|
||||
const thumbnail =
|
||||
body.thumbnail !== undefined ? String(body.thumbnail || "").trim() || null : undefined;
|
||||
body.description !== undefined
|
||||
? String(body.description || "").trim() || null
|
||||
: existingProject.description || null;
|
||||
|
||||
const existingProjectState = extractAgentProjectStateFromCanvasData(existingProject.canvasData);
|
||||
const nextProjectState =
|
||||
body.projectState !== undefined
|
||||
? normalizeAgentGenerationProjectPersistedState(body.projectState)
|
||||
: extractAgentProjectStateFromCanvasData(existingProject.canvasData);
|
||||
? mergeAgentProjectStateForSave({
|
||||
existingState: existingProjectState,
|
||||
incomingState: body.projectState,
|
||||
})
|
||||
: existingProjectState;
|
||||
const firstResultUrl = getAllResultUrlsFromState(nextProjectState)[0] || null;
|
||||
const requestedThumbnail =
|
||||
body.thumbnail !== undefined ? String(body.thumbnail || "").trim() || null : undefined;
|
||||
const thumbnail =
|
||||
requestedThumbnail || existingProject.thumbnail || firstResultUrl || null;
|
||||
|
||||
const [project] = await db
|
||||
.update(canvasProject)
|
||||
|
||||
@ -82,6 +82,7 @@ import {
|
||||
LOCAL_EDIT_MAX_REGIONS,
|
||||
createLocalEditReferenceImageId,
|
||||
createLocalEditRegionId,
|
||||
hasScaleAdjustTargetChanged,
|
||||
reindexLocalEditRegions,
|
||||
type LocalEditReferenceImage,
|
||||
type LocalEditRegion,
|
||||
@ -122,6 +123,8 @@ import type {
|
||||
AgentDetailBriefForm,
|
||||
AgentDetailGenerationProgressState,
|
||||
AgentDetailReplacementStrategy,
|
||||
AgentGenerationProjectReplacementResultState,
|
||||
AgentGenerationReplacementResultRecord,
|
||||
AgentGenerationProjectWorkspaceState,
|
||||
AgentGenerationProjectResultCollections,
|
||||
AgentGenerationResultModeType,
|
||||
@ -146,6 +149,7 @@ import {
|
||||
import {
|
||||
createEmptyAgentGenerationProjectWorkspaceState,
|
||||
normalizeAgentGenerationProjectPersistedState,
|
||||
normalizeAgentGenerationProjectReplacementResultState,
|
||||
} from "@/lib/agents/generation-project-state";
|
||||
|
||||
export interface AgentGenerationWorkspaceDarkV2Props {
|
||||
@ -173,6 +177,10 @@ export interface AgentGenerationWorkspaceDarkV2Props {
|
||||
onWorkspaceStateChange?: (next: AgentGenerationProjectWorkspaceState) => void;
|
||||
resultCollections?: AgentGenerationProjectResultCollections | null;
|
||||
deletedResultCollections?: AgentGenerationProjectResultCollections | null;
|
||||
replacementResultState?: AgentGenerationProjectReplacementResultState | null;
|
||||
onReplacementResultStateChange?: (
|
||||
next: AgentGenerationProjectReplacementResultState
|
||||
) => void;
|
||||
onResultPreviewUrlsChange?: (
|
||||
modeType: AgentGenerationResultModeType,
|
||||
urls: string[]
|
||||
@ -181,6 +189,7 @@ export interface AgentGenerationWorkspaceDarkV2Props {
|
||||
modeType: AgentGenerationResultModeType,
|
||||
imageUrl: string
|
||||
) => void;
|
||||
onPersistProject?: () => Promise<boolean> | boolean;
|
||||
topNavRightSlot?: ReactNode;
|
||||
showcaseConfig?: AgentWorkflowShowcaseConfig | null;
|
||||
workflowStylePanel?: ReactNode;
|
||||
@ -247,6 +256,8 @@ type DetailResultLocalEditTarget = {
|
||||
|
||||
type DetailResultLocalEditMode = "local_edit" | "scale_adjust";
|
||||
|
||||
type DetailResultLocalEditImageSize = "1K" | "2K";
|
||||
|
||||
type DetailResultLocalEditRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
@ -269,6 +280,7 @@ type DetailResultLocalEditRegionInteraction = {
|
||||
|
||||
type DetailResultLocalEditSubmitParams = {
|
||||
imageUrl: string;
|
||||
imageSize: DetailResultLocalEditImageSize;
|
||||
mode: DetailResultLocalEditMode;
|
||||
title: string;
|
||||
regions: LocalEditRegion[];
|
||||
@ -469,6 +481,15 @@ function normalizeResultCollectionUrls(urls?: string[] | null) {
|
||||
);
|
||||
}
|
||||
|
||||
function areResultUrlListsEqual(left?: string[] | null, right?: string[] | null) {
|
||||
const normalizedLeft = normalizeResultCollectionUrls(left);
|
||||
const normalizedRight = normalizeResultCollectionUrls(right);
|
||||
return (
|
||||
normalizedLeft.length === normalizedRight.length &&
|
||||
normalizedLeft.every((url, index) => normalizedRight[index] === url)
|
||||
);
|
||||
}
|
||||
|
||||
function excludeDeletedResultUrls(
|
||||
urls: string[] | undefined | null,
|
||||
deletedUrls: string[] | undefined | null
|
||||
@ -894,6 +915,7 @@ function DetailResultLocalEditDialog({
|
||||
const [uploadingRegionId, setUploadingRegionId] = useState<string | null>(null);
|
||||
const [drawingTargetRegionId, setDrawingTargetRegionId] = useState<string | null>(null);
|
||||
const [mode, setMode] = useState<DetailResultLocalEditMode>("local_edit");
|
||||
const [imageSize, setImageSize] = useState<DetailResultLocalEditImageSize>("1K");
|
||||
const dragStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const editSurfaceRef = useRef<HTMLDivElement | null>(null);
|
||||
const isScaleAdjustMode = mode === "scale_adjust";
|
||||
@ -918,6 +940,7 @@ function DetailResultLocalEditDialog({
|
||||
setRegionInteraction(null);
|
||||
setUploadingRegionId(null);
|
||||
setDrawingTargetRegionId(null);
|
||||
setImageSize("1K");
|
||||
dragStartRef.current = null;
|
||||
}, [target?.cardId, target?.imageUrl, target?.mode]);
|
||||
|
||||
@ -1211,7 +1234,7 @@ function DetailResultLocalEditDialog({
|
||||
return;
|
||||
}
|
||||
const unchangedRegion = validRegions.find(
|
||||
(region) => deriveDetailScalePercentFromRects(region.rect, region.targetRect) === 0
|
||||
(region) => !hasScaleAdjustTargetChanged(region.rect, region.targetRect)
|
||||
);
|
||||
if (unchangedRegion) {
|
||||
setActiveRegionId(unchangedRegion.id);
|
||||
@ -1228,11 +1251,12 @@ function DetailResultLocalEditDialog({
|
||||
}
|
||||
onSubmit({
|
||||
imageUrl: target.imageUrl,
|
||||
imageSize,
|
||||
mode,
|
||||
title: target.title,
|
||||
regions: validRegions,
|
||||
});
|
||||
}, [isScaleAdjustMode, mode, onSubmit, regions, target, uploadingRegionId]);
|
||||
}, [imageSize, isScaleAdjustMode, mode, onSubmit, regions, target, uploadingRegionId]);
|
||||
|
||||
const completeDraftRegion = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
@ -1817,6 +1841,36 @@ function DetailResultLocalEditDialog({
|
||||
? `先框主体,再框目标大小和位置。最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域。${completionCopy}`
|
||||
: `框选要局部修改的位置,填写提示词;可为每个区域添加参考图。最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域。${completionCopy}`}
|
||||
</div>
|
||||
{!activePreviewResult ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.035] p-3">
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-white/38">
|
||||
输出尺寸
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{(
|
||||
[
|
||||
{ label: "快速 1K", value: "1K" },
|
||||
{ label: "高清 2K", value: "2K" },
|
||||
] as const
|
||||
).map((option) => (
|
||||
<button
|
||||
className={cn(
|
||||
"h-9 rounded-xl border px-3 text-sm font-semibold transition",
|
||||
imageSize === option.value
|
||||
? "border-white bg-white text-black"
|
||||
: "border-white/10 bg-black/20 text-white/58 hover:bg-white/[0.08] hover:text-white"
|
||||
)}
|
||||
disabled={submitting}
|
||||
key={option.value}
|
||||
onClick={() => setImageSize(option.value)}
|
||||
type="button"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!activePreviewResult && isScaleAdjustMode ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.035] p-3 text-sm text-white/58">
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-white/38">
|
||||
@ -2352,13 +2406,7 @@ type ReplacementModeType = Extract<
|
||||
"detail_replacement" | "sku_replacement"
|
||||
>;
|
||||
|
||||
interface ReplacementResultRecord {
|
||||
url: string | null;
|
||||
text: string;
|
||||
status: Extract<ReplacementCardStatus, "completed" | "passthrough" | "fallback">;
|
||||
updatedAt: number;
|
||||
compareSourceUrl?: string | null;
|
||||
}
|
||||
interface ReplacementResultRecord extends AgentGenerationReplacementResultRecord {}
|
||||
|
||||
interface ReplacementExecutionContext {
|
||||
modeType: ReplacementModeType;
|
||||
@ -2405,13 +2453,26 @@ type ReplacementResultState = Record<
|
||||
const MANUAL_PASSTHROUGH_RESULT_TEXT =
|
||||
"已按人工标记沿用参考图,直接应用当前切片,不参与继续生成。";
|
||||
|
||||
function createEmptyReplacementResultState(): ReplacementResultState {
|
||||
function normalizeReplacementResultStateForWorkspace(
|
||||
value: AgentGenerationProjectReplacementResultState | null | undefined
|
||||
): ReplacementResultState {
|
||||
const normalized = normalizeAgentGenerationProjectReplacementResultState(value);
|
||||
return {
|
||||
detail_replacement: {},
|
||||
sku_replacement: {},
|
||||
detail_replacement: { ...(normalized.detail_replacement || {}) },
|
||||
sku_replacement: { ...(normalized.sku_replacement || {}) },
|
||||
};
|
||||
}
|
||||
|
||||
function areReplacementResultStatesEqual(
|
||||
left: ReplacementResultState,
|
||||
right: ReplacementResultState
|
||||
) {
|
||||
return (
|
||||
JSON.stringify(normalizeAgentGenerationProjectReplacementResultState(left)) ===
|
||||
JSON.stringify(normalizeAgentGenerationProjectReplacementResultState(right))
|
||||
);
|
||||
}
|
||||
|
||||
function inferReplacementCardStatusFromText(text: string | null | undefined): ReplacementCardStatus | null {
|
||||
const normalized = normalizeAssistantText(String(text || ""))
|
||||
.replace(/\s+/g, " ")
|
||||
@ -3059,9 +3120,12 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
workspaceState = null,
|
||||
resultCollections = null,
|
||||
deletedResultCollections = null,
|
||||
replacementResultState: persistedReplacementResultState = null,
|
||||
onWorkspaceStateChange,
|
||||
onReplacementResultStateChange,
|
||||
onResultPreviewUrlsChange,
|
||||
onDeleteResultImage,
|
||||
onPersistProject,
|
||||
topNavRightSlot,
|
||||
showcaseConfig,
|
||||
workflowStylePanel,
|
||||
@ -3151,7 +3215,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
);
|
||||
const [selectedResultUrl, setSelectedResultUrl] = useState<string | null>(null);
|
||||
const [activeBusinessModeKey, setActiveBusinessModeKey] =
|
||||
useState<AgentWorkflowBusinessModeKey>("main_sku");
|
||||
useState<AgentWorkflowBusinessModeKey>(
|
||||
normalizedIncomingWorkspaceState.activeBusinessModeKey || "main_sku"
|
||||
);
|
||||
const [activeResultModeType, setActiveResultModeType] =
|
||||
useState<AgentGenerationResultModeType>("scene_generation");
|
||||
const [fullscreenPreview, setFullscreenPreview] = useState<string | null>(null);
|
||||
@ -3213,7 +3279,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [replacementResultState, setReplacementResultState] = useState<ReplacementResultState>(
|
||||
() => createEmptyReplacementResultState()
|
||||
() => normalizeReplacementResultStateForWorkspace(persistedReplacementResultState)
|
||||
);
|
||||
const [replacementExecutionContext, setReplacementExecutionContext] =
|
||||
useState<ReplacementExecutionContext | null>(null);
|
||||
@ -3229,6 +3295,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const [detailWatermarkRemoveCardIds, setDetailWatermarkRemoveCardIds] =
|
||||
useState<Set<string>>(() => new Set());
|
||||
const processedResultHintTimerRef = useRef<Record<string, number>>({});
|
||||
const transportGenerationStartedAtRef = useRef<number | null>(null);
|
||||
const closePsdTemplateDialog = useCallback(() => {
|
||||
setIsPsdTemplateDialogOpen(false);
|
||||
setPsdTemplateDialogSourceUrl(null);
|
||||
@ -3262,6 +3329,22 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
},
|
||||
[activeResultModeType, isPsdTemplateEnabled]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const nextState = normalizeReplacementResultStateForWorkspace(
|
||||
persistedReplacementResultState
|
||||
);
|
||||
setReplacementResultState((current) =>
|
||||
areReplacementResultStatesEqual(current, nextState) ? current : nextState
|
||||
);
|
||||
}, [persistedReplacementResultState]);
|
||||
|
||||
useEffect(() => {
|
||||
onReplacementResultStateChange?.(
|
||||
normalizeAgentGenerationProjectReplacementResultState(replacementResultState)
|
||||
);
|
||||
}, [onReplacementResultStateChange, replacementResultState]);
|
||||
|
||||
const normalizedDeletedResultCollections = useMemo(
|
||||
() => ({
|
||||
scene_generation: normalizeResultCollectionUrls(
|
||||
@ -4510,63 +4593,48 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
skuReplacementReferenceCards,
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (latestReplacementResults.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detailReferenceCardById = new Map(
|
||||
detailReplacementReferenceCards.map((card) => [card.id, card])
|
||||
);
|
||||
const skuReferenceCardById = new Map(
|
||||
skuReplacementReferenceCards.map((card) => [card.id, card])
|
||||
);
|
||||
const fallbackModeType: ReplacementModeType | null =
|
||||
currentResultModeType === "detail_replacement" || currentResultModeType === "sku_replacement"
|
||||
? currentResultModeType
|
||||
: null;
|
||||
|
||||
setReplacementResultState((current) => {
|
||||
const detailCardIds = new Set(detailReplacementReferenceCards.map((card) => card.id));
|
||||
const skuCardIds = new Set(skuReplacementReferenceCards.map((card) => card.id));
|
||||
let changed = false;
|
||||
|
||||
const nextDetail = Object.fromEntries(
|
||||
Object.entries(current.detail_replacement).filter(([cardId]) => detailCardIds.has(cardId))
|
||||
);
|
||||
if (Object.keys(nextDetail).length !== Object.keys(current.detail_replacement).length) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const nextSku = Object.fromEntries(
|
||||
Object.entries(current.sku_replacement).filter(([cardId]) => skuCardIds.has(cardId))
|
||||
);
|
||||
if (Object.keys(nextSku).length !== Object.keys(current.sku_replacement).length) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
detail_replacement: nextDetail,
|
||||
sku_replacement: nextSku,
|
||||
const nextState: ReplacementResultState = {
|
||||
detail_replacement: { ...current.detail_replacement },
|
||||
sku_replacement: { ...current.sku_replacement },
|
||||
};
|
||||
});
|
||||
}, [detailReplacementReferenceCards, skuReplacementReferenceCards]);
|
||||
useEffect(() => {
|
||||
if (
|
||||
(currentResultModeType !== "detail_replacement" && currentResultModeType !== "sku_replacement") ||
|
||||
latestReplacementResults.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cardIds = currentReplacementExecutionCardIds;
|
||||
if (cardIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReplacementResultState((current) => {
|
||||
const nextModeState = { ...current[currentResultModeType] };
|
||||
let changed = false;
|
||||
|
||||
latestReplacementResults.forEach((result, index) => {
|
||||
const cardId = result.replacementCardId || cardIds[index];
|
||||
const applyResult = (
|
||||
modeType: ReplacementModeType,
|
||||
cardId: string | null | undefined,
|
||||
result: {
|
||||
url: string | null;
|
||||
text: string;
|
||||
replacementCardId: string | null;
|
||||
},
|
||||
referenceCard?: {
|
||||
imageUrl?: string | null;
|
||||
label?: string | null;
|
||||
previewUrl?: string | null;
|
||||
} | null
|
||||
) => {
|
||||
if (!cardId) {
|
||||
return;
|
||||
}
|
||||
const inferredStatus = inferReplacementCardStatusFromText(result.text);
|
||||
const nextModeState = nextState[modeType];
|
||||
const previousRecord = nextModeState[cardId];
|
||||
const referenceCard =
|
||||
currentResultModeType === "detail_replacement"
|
||||
? detailReplacementReferenceCards.find((card) => card.id === cardId)
|
||||
: skuReplacementReferenceCards.find((card) => card.id === cardId);
|
||||
const resultUrl = result.url || null;
|
||||
const previousUrl =
|
||||
previousRecord?.url && previousRecord.url !== resultUrl
|
||||
@ -4586,27 +4654,59 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
resultUrl,
|
||||
previousUrl || referenceSourceUrl || previousRecord?.compareSourceUrl || null
|
||||
),
|
||||
referenceImageUrl: referenceCard?.imageUrl || previousRecord?.referenceImageUrl || null,
|
||||
referencePreviewUrl:
|
||||
referenceCard?.previewUrl || previousRecord?.referencePreviewUrl || null,
|
||||
label: referenceCard?.label || previousRecord?.label || null,
|
||||
};
|
||||
if (
|
||||
previousRecord?.url === nextRecord.url &&
|
||||
previousRecord?.text === nextRecord.text &&
|
||||
previousRecord?.status === nextRecord.status &&
|
||||
previousRecord?.compareSourceUrl === nextRecord.compareSourceUrl
|
||||
previousRecord?.compareSourceUrl === nextRecord.compareSourceUrl &&
|
||||
previousRecord?.referenceImageUrl === nextRecord.referenceImageUrl &&
|
||||
previousRecord?.referencePreviewUrl === nextRecord.referencePreviewUrl &&
|
||||
previousRecord?.label === nextRecord.label
|
||||
) {
|
||||
return;
|
||||
}
|
||||
nextModeState[cardId] = nextRecord;
|
||||
changed = true;
|
||||
};
|
||||
|
||||
latestReplacementResults.forEach((result, index) => {
|
||||
const explicitCardId = result.replacementCardId || null;
|
||||
if (explicitCardId) {
|
||||
const detailReferenceCard = detailReferenceCardById.get(explicitCardId);
|
||||
if (detailReferenceCard) {
|
||||
applyResult("detail_replacement", explicitCardId, result, detailReferenceCard);
|
||||
return;
|
||||
}
|
||||
|
||||
const skuReferenceCard = skuReferenceCardById.get(explicitCardId);
|
||||
if (skuReferenceCard) {
|
||||
applyResult("sku_replacement", explicitCardId, result, skuReferenceCard);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fallbackModeType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackCardId = currentReplacementExecutionCardIds[index];
|
||||
const fallbackReferenceCard =
|
||||
fallbackModeType === "detail_replacement"
|
||||
? detailReferenceCardById.get(fallbackCardId)
|
||||
: skuReferenceCardById.get(fallbackCardId);
|
||||
applyResult(fallbackModeType, fallbackCardId, result, fallbackReferenceCard);
|
||||
});
|
||||
|
||||
if (!changed) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
[currentResultModeType]: nextModeState,
|
||||
};
|
||||
return nextState;
|
||||
});
|
||||
}, [
|
||||
currentReplacementExecutionCardIds,
|
||||
@ -4616,8 +4716,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
skuReplacementReferenceCards,
|
||||
]);
|
||||
const detailReplacementResultUrls = useMemo(
|
||||
() =>
|
||||
detailReplacementReferenceCards
|
||||
() => {
|
||||
const urlsFromCards = detailReplacementReferenceCards
|
||||
.map((card) => {
|
||||
const recordUrl = replacementResultState.detail_replacement[card.id]?.url || null;
|
||||
const fallbackUrl = !recordUrl && card.passthrough ? card.imageUrl || null : null;
|
||||
@ -4627,7 +4727,15 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
? nextUrl
|
||||
: null;
|
||||
})
|
||||
.filter((url): url is string => Boolean(url)),
|
||||
.filter((url): url is string => Boolean(url));
|
||||
const urlsFromRecords = Object.values(replacementResultState.detail_replacement)
|
||||
.map((record) => record.url)
|
||||
.filter((url): url is string => Boolean(url))
|
||||
.filter(
|
||||
(url) => !normalizedDeletedResultCollections.detail_replacement.includes(url)
|
||||
);
|
||||
return Array.from(new Set([...urlsFromCards, ...urlsFromRecords]));
|
||||
},
|
||||
[
|
||||
detailReplacementReferenceCards,
|
||||
normalizedDeletedResultCollections.detail_replacement,
|
||||
@ -4635,15 +4743,21 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
]
|
||||
);
|
||||
const skuReplacementResultUrls = useMemo(
|
||||
() =>
|
||||
skuReplacementReferenceCards
|
||||
() => {
|
||||
const urlsFromCards = skuReplacementReferenceCards
|
||||
.map((card) => {
|
||||
const nextUrl = replacementResultState.sku_replacement[card.id]?.url || null;
|
||||
return nextUrl && !normalizedDeletedResultCollections.sku_replacement.includes(nextUrl)
|
||||
? nextUrl
|
||||
: null;
|
||||
})
|
||||
.filter((url): url is string => Boolean(url)),
|
||||
.filter((url): url is string => Boolean(url));
|
||||
const urlsFromRecords = Object.values(replacementResultState.sku_replacement)
|
||||
.map((record) => record.url)
|
||||
.filter((url): url is string => Boolean(url))
|
||||
.filter((url) => !normalizedDeletedResultCollections.sku_replacement.includes(url));
|
||||
return Array.from(new Set([...urlsFromCards, ...urlsFromRecords]));
|
||||
},
|
||||
[
|
||||
normalizedDeletedResultCollections.sku_replacement,
|
||||
replacementResultState.sku_replacement,
|
||||
@ -4734,24 +4848,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
resultCollections?.detail_generation,
|
||||
normalizedDeletedResultCollections.detail_generation
|
||||
),
|
||||
detail_replacement: Array.from(
|
||||
new Set([
|
||||
...detailReplacementResultUrls,
|
||||
...excludeDeletedResultUrls(
|
||||
resultCollections?.detail_replacement,
|
||||
normalizedDeletedResultCollections.detail_replacement
|
||||
),
|
||||
])
|
||||
),
|
||||
sku_replacement: Array.from(
|
||||
new Set([
|
||||
...skuReplacementResultUrls,
|
||||
...excludeDeletedResultUrls(
|
||||
resultCollections?.sku_replacement,
|
||||
normalizedDeletedResultCollections.sku_replacement
|
||||
),
|
||||
])
|
||||
),
|
||||
detail_replacement:
|
||||
detailReplacementResultUrls.length > 0
|
||||
? detailReplacementResultUrls
|
||||
: excludeDeletedResultUrls(
|
||||
resultCollections?.detail_replacement,
|
||||
normalizedDeletedResultCollections.detail_replacement
|
||||
),
|
||||
sku_replacement:
|
||||
skuReplacementResultUrls.length > 0
|
||||
? skuReplacementResultUrls
|
||||
: excludeDeletedResultUrls(
|
||||
resultCollections?.sku_replacement,
|
||||
normalizedDeletedResultCollections.sku_replacement
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
@ -4957,6 +5067,52 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
skuReplacementBatchDownloadUrls.length > 0
|
||||
? skuReplacementBatchDownloadUrls
|
||||
: mergedResultCollections.sku_replacement;
|
||||
const syncReplacementResultCollectionsToParent = useCallback(
|
||||
(targetModeType?: ReplacementModeType) => {
|
||||
if (!onResultPreviewUrlsChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncMode = (modeType: ReplacementModeType, liveUrls: string[]) => {
|
||||
if (targetModeType && targetModeType !== modeType) {
|
||||
return;
|
||||
}
|
||||
const normalizedLiveUrls = normalizeResultCollectionUrls(liveUrls);
|
||||
if (normalizedLiveUrls.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deletedUrlSet = new Set(
|
||||
normalizedDeletedResultCollections[modeType]
|
||||
);
|
||||
const nextUrls = Array.from(
|
||||
new Set(normalizedLiveUrls.filter((url) => !deletedUrlSet.has(url)))
|
||||
);
|
||||
|
||||
if (!areResultUrlListsEqual(resultCollections?.[modeType], nextUrls)) {
|
||||
onResultPreviewUrlsChange(modeType, nextUrls);
|
||||
}
|
||||
};
|
||||
|
||||
onReplacementResultStateChange?.(
|
||||
normalizeAgentGenerationProjectReplacementResultState(
|
||||
replacementResultState
|
||||
)
|
||||
);
|
||||
syncMode("detail_replacement", detailReplacementResultUrls);
|
||||
syncMode("sku_replacement", skuReplacementResultUrls);
|
||||
},
|
||||
[
|
||||
detailReplacementResultUrls,
|
||||
normalizedDeletedResultCollections,
|
||||
onReplacementResultStateChange,
|
||||
onResultPreviewUrlsChange,
|
||||
replacementResultState,
|
||||
resultCollections,
|
||||
skuReplacementResultUrls,
|
||||
]
|
||||
);
|
||||
|
||||
const detailRemainingCount = useMemo(
|
||||
() =>
|
||||
detailResultCards.filter(
|
||||
@ -5196,6 +5352,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
};
|
||||
return areImageGenerationParamsEqual(current, nextParams) ? current : nextParams;
|
||||
});
|
||||
setActiveBusinessModeKey((current) => {
|
||||
const nextBusinessModeKey =
|
||||
normalizedIncomingWorkspaceState.activeBusinessModeKey || "main_sku";
|
||||
return current === nextBusinessModeKey ? current : nextBusinessModeKey;
|
||||
});
|
||||
}, [isWorkflowModelLocked, normalizedIncomingWorkspaceState]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -5212,6 +5373,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
: null
|
||||
),
|
||||
selectedModelId,
|
||||
activeBusinessModeKey,
|
||||
imageParams: {
|
||||
aspectRatio: imageParams.aspectRatio,
|
||||
imageSize: imageParams.imageSize,
|
||||
@ -5221,6 +5383,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
workspaceStateSignatureRef.current = JSON.stringify(nextWorkspaceState);
|
||||
onWorkspaceStateChange?.(nextWorkspaceState);
|
||||
}, [
|
||||
activeBusinessModeKey,
|
||||
attachments,
|
||||
imageParams.aspectRatio,
|
||||
imageParams.imageCount,
|
||||
@ -5272,6 +5435,15 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
replacementExecutionCardIds: null,
|
||||
replacementRerunReferenceImages: null,
|
||||
};
|
||||
try {
|
||||
stop();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[AgentGenerationWorkspaceDarkV2] Failed to stop stale generation transport:",
|
||||
error
|
||||
);
|
||||
}
|
||||
transportGenerationStartedAtRef.current = null;
|
||||
setPendingGeneration((current) => {
|
||||
if (
|
||||
!current ||
|
||||
@ -5292,7 +5464,57 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
|
||||
const timerId = window.setTimeout(clearStalePendingGeneration, remainingMs);
|
||||
return () => window.clearTimeout(timerId);
|
||||
}, [pendingGeneration]);
|
||||
}, [pendingGeneration, stop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transportIsGenerating) {
|
||||
transportGenerationStartedAtRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (transportGenerationStartedAtRef.current === null) {
|
||||
transportGenerationStartedAtRef.current =
|
||||
pendingGeneration?.timestamp || Date.now();
|
||||
}
|
||||
|
||||
const startedAt = transportGenerationStartedAtRef.current;
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
const remainingMs = AGENT_PENDING_GENERATION_EXPIRY_MS - elapsedMs;
|
||||
const abortStaleTransport = () => {
|
||||
const currentStartedAt = transportGenerationStartedAtRef.current;
|
||||
if (
|
||||
currentStartedAt === null ||
|
||||
Date.now() - currentStartedAt < AGENT_PENDING_GENERATION_EXPIRY_MS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
generationRequestSnapshotRef.current = {
|
||||
hiddenPrompt: null,
|
||||
replacementExecutionCardIds: null,
|
||||
replacementRerunReferenceImages: null,
|
||||
};
|
||||
try {
|
||||
stop();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[AgentGenerationWorkspaceDarkV2] Failed to abort stale generation transport:",
|
||||
error
|
||||
);
|
||||
}
|
||||
transportGenerationStartedAtRef.current = null;
|
||||
setPendingGeneration(null);
|
||||
toast.warning("生成请求超过 30 分钟未回传,已中断卡住连接,可重新生成未回传结果。");
|
||||
};
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
abortStaleTransport();
|
||||
return;
|
||||
}
|
||||
|
||||
const timerId = window.setTimeout(abortStaleTransport, remainingMs);
|
||||
return () => window.clearTimeout(timerId);
|
||||
}, [pendingGeneration?.timestamp, stop, transportIsGenerating]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingGeneration || transportIsGenerating) {
|
||||
@ -5331,11 +5553,19 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
onResultPreviewUrlsChange?.(
|
||||
currentResultModeType,
|
||||
mergedResultCollections[currentResultModeType] || []
|
||||
);
|
||||
}, [currentResultModeType, mergedResultCollections, onResultPreviewUrlsChange]);
|
||||
if (!onResultPreviewUrlsChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
([
|
||||
"scene_generation",
|
||||
"detail_generation",
|
||||
"detail_replacement",
|
||||
"sku_replacement",
|
||||
] as AgentGenerationResultModeType[]).forEach((modeType) => {
|
||||
onResultPreviewUrlsChange(modeType, mergedResultCollections[modeType] || []);
|
||||
});
|
||||
}, [mergedResultCollections, onResultPreviewUrlsChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeResultModeUrls.length === 0) {
|
||||
@ -5477,6 +5707,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
imageUrls: string[];
|
||||
zipName: string;
|
||||
scope: BatchDownloadScope;
|
||||
modeType?: AgentGenerationResultModeType;
|
||||
}) => {
|
||||
const normalizedUrls = (params.imageUrls || [])
|
||||
.map((item) => String(item || "").trim())
|
||||
@ -5486,6 +5717,19 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
params.modeType === "detail_replacement" ||
|
||||
params.modeType === "sku_replacement"
|
||||
) {
|
||||
syncReplacementResultCollectionsToParent(params.modeType);
|
||||
}
|
||||
const persisted = await Promise.resolve(onPersistProject?.() ?? true).catch(
|
||||
() => false
|
||||
);
|
||||
if (!persisted) {
|
||||
toast.info("结果已保留在当前页面,项目同步稍后会自动重试");
|
||||
}
|
||||
|
||||
setBatchDownloadState(params.scope);
|
||||
setBatchDownloadDialog({
|
||||
scope: params.scope,
|
||||
@ -5565,7 +5809,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}));
|
||||
toast.error(message);
|
||||
}
|
||||
}, []);
|
||||
}, [onPersistProject, syncReplacementResultCollectionsToParent]);
|
||||
|
||||
useEffect(() => {
|
||||
const taskId = batchDownloadDialog?.taskId;
|
||||
@ -5664,6 +5908,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const link = document.createElement("a");
|
||||
link.href = batchDownloadDialog.downloadUrl;
|
||||
link.download = `${batchDownloadDialog.zipName || "batch-results"}.zip`;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
@ -5729,6 +5976,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
setReplacementResultState((current) => {
|
||||
const currentModeState = current[params.modeType];
|
||||
const previousRecord = currentModeState[params.cardId];
|
||||
const referenceCard =
|
||||
params.modeType === "detail_replacement"
|
||||
? detailReplacementReferenceCards.find((card) => card.id === params.cardId)
|
||||
: skuReplacementReferenceCards.find((card) => card.id === params.cardId);
|
||||
const inferredStatus = inferReplacementCardStatusFromText(params.text);
|
||||
const explicitCompareSourceUrl =
|
||||
params.compareSourceUrl === undefined
|
||||
@ -5749,13 +6000,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
explicitCompareSourceUrl === undefined
|
||||
? previousRecord?.compareSourceUrl || null
|
||||
: explicitCompareSourceUrl,
|
||||
referenceImageUrl: referenceCard?.imageUrl || previousRecord?.referenceImageUrl || null,
|
||||
referencePreviewUrl:
|
||||
referenceCard?.previewUrl || previousRecord?.referencePreviewUrl || null,
|
||||
label: referenceCard?.label || previousRecord?.label || null,
|
||||
};
|
||||
|
||||
if (
|
||||
previousRecord?.url === nextRecord.url &&
|
||||
previousRecord?.text === nextRecord.text &&
|
||||
previousRecord?.status === nextRecord.status &&
|
||||
previousRecord?.compareSourceUrl === nextRecord.compareSourceUrl
|
||||
previousRecord?.compareSourceUrl === nextRecord.compareSourceUrl &&
|
||||
previousRecord?.referenceImageUrl === nextRecord.referenceImageUrl &&
|
||||
previousRecord?.referencePreviewUrl === nextRecord.referencePreviewUrl &&
|
||||
previousRecord?.label === nextRecord.label
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
@ -5768,7 +6026,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
},
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
}, [detailReplacementReferenceCards, skuReplacementReferenceCards]);
|
||||
const handleOpenReplacementTextEdit = useCallback((params: {
|
||||
modeType: ReplacementModeType;
|
||||
cardId: string;
|
||||
@ -6712,15 +6970,19 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
: selectedModelId !== "auto"
|
||||
? selectedModelId
|
||||
: effectiveDefaultModelId || undefined;
|
||||
const requestedImageSize: DetailResultLocalEditImageSize =
|
||||
params.imageSize === "2K" ? "2K" : "1K";
|
||||
|
||||
setDetailLocalEditSubmittingId(cardId);
|
||||
setDetailLocalEditSubmittingMode(editMode);
|
||||
toast.info(isScaleAdjust ? "正在提交比例调整..." : "正在提交局部编辑...");
|
||||
toast.info(
|
||||
`${isScaleAdjust ? "正在提交比例调整" : "正在提交局部编辑"}(${requestedImageSize})...`
|
||||
);
|
||||
|
||||
try {
|
||||
const submitResult = await imageGenerationService.localEdit({
|
||||
aspectRatio: "auto",
|
||||
imageSize: "2K",
|
||||
imageSize: requestedImageSize,
|
||||
model:
|
||||
resolvedLocalEditModelId && resolvedLocalEditModelId !== "auto"
|
||||
? resolvedLocalEditModelId
|
||||
@ -6938,6 +7200,23 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
delete next[normalizedUrl];
|
||||
return next;
|
||||
});
|
||||
if (modeType === "detail_replacement" || modeType === "sku_replacement") {
|
||||
setReplacementResultState((current) => {
|
||||
const currentModeState = current[modeType];
|
||||
const nextModeState = Object.fromEntries(
|
||||
Object.entries(currentModeState).filter(
|
||||
([, record]) => record.url !== normalizedUrl
|
||||
)
|
||||
);
|
||||
if (Object.keys(nextModeState).length === Object.keys(currentModeState).length) {
|
||||
return current;
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
[modeType]: nextModeState,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteResultImage(modeType, normalizedUrl);
|
||||
toast.success("结果图已删除");
|
||||
@ -9416,6 +9695,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
imageUrls: detailReplacementOrderedDownloadUrls,
|
||||
zipName: `${selectedAgentPreset?.name || "detail-replacement"}-results`,
|
||||
scope: "detail",
|
||||
modeType: "detail_replacement",
|
||||
})
|
||||
}
|
||||
disabled={batchDownloadState === "detail"}
|
||||
@ -9433,6 +9713,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
imageUrls: mergedResultCollections.detail_generation,
|
||||
zipName: `${selectedAgentPreset?.name || "ai-detail"}-results`,
|
||||
scope: "detail",
|
||||
modeType: "detail_generation",
|
||||
})
|
||||
}
|
||||
disabled={batchDownloadState === "detail"}
|
||||
@ -9450,6 +9731,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
imageUrls: skuReplacementOrderedDownloadUrls,
|
||||
zipName: `${selectedAgentPreset?.name || "sku-replacement"}-results`,
|
||||
scope: "sku",
|
||||
modeType: "sku_replacement",
|
||||
})
|
||||
}
|
||||
disabled={batchDownloadState === "sku"}
|
||||
@ -9466,6 +9748,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
imageUrls: activeResultModeUrls,
|
||||
zipName: `${selectedAgentPreset?.name || "scene-generation"}-results`,
|
||||
scope: "scene",
|
||||
modeType: "scene_generation",
|
||||
})
|
||||
}
|
||||
disabled={batchDownloadState === "scene"}
|
||||
|
||||
@ -8,9 +8,12 @@ import {
|
||||
} from "@/lib/agents/workflow-material-config";
|
||||
import type {
|
||||
AgentGenerationProjectPersistedState,
|
||||
AgentGenerationProjectReplacementResultState,
|
||||
AgentGenerationProjectResultCollections,
|
||||
AgentGenerationResultModeType,
|
||||
AgentGenerationReplacementResultStatus,
|
||||
AgentGenerationProjectWorkspaceState,
|
||||
AgentWorkflowBusinessModeKey,
|
||||
AgentWorkflowPersistedState,
|
||||
} from "@/types/agent";
|
||||
|
||||
@ -19,6 +22,7 @@ export function createEmptyAgentGenerationProjectWorkspaceState(): AgentGenerati
|
||||
input: "",
|
||||
attachments: [],
|
||||
selectedModelId: "auto",
|
||||
activeBusinessModeKey: "main_sku",
|
||||
imageParams: {
|
||||
aspectRatio: "auto",
|
||||
imageSize: "2K",
|
||||
@ -58,6 +62,7 @@ export function createEmptyAgentGenerationProjectPersistedState(): AgentGenerati
|
||||
resultImageUrls: [],
|
||||
resultCollections: {},
|
||||
deletedResultCollections: {},
|
||||
replacementResultState: {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -86,6 +91,129 @@ function normalizeResultCollections(value: unknown): AgentGenerationProjectResul
|
||||
};
|
||||
}
|
||||
|
||||
const REPLACEMENT_RESULT_STATUSES = new Set<AgentGenerationReplacementResultStatus>([
|
||||
"completed",
|
||||
"passthrough",
|
||||
"fallback",
|
||||
]);
|
||||
|
||||
function normalizeOptionalHttpUrl(value: unknown) {
|
||||
const raw = String(value || "").trim();
|
||||
return /^https?:\/\//i.test(raw) ? raw : null;
|
||||
}
|
||||
|
||||
function normalizeReplacementResultStatus(value: unknown): AgentGenerationReplacementResultStatus {
|
||||
return REPLACEMENT_RESULT_STATUSES.has(value as AgentGenerationReplacementResultStatus)
|
||||
? (value as AgentGenerationReplacementResultStatus)
|
||||
: "completed";
|
||||
}
|
||||
|
||||
export function normalizeAgentGenerationProjectReplacementResultState(
|
||||
value: unknown
|
||||
): AgentGenerationProjectReplacementResultState {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const normalizeModeState = (input: unknown) => {
|
||||
const records =
|
||||
input && typeof input === "object" && !Array.isArray(input)
|
||||
? (input as Record<string, unknown>)
|
||||
: {};
|
||||
const entries: Array<
|
||||
readonly [
|
||||
string,
|
||||
NonNullable<AgentGenerationProjectReplacementResultState["detail_replacement"]>[string],
|
||||
]
|
||||
> = [];
|
||||
|
||||
Object.entries(records).forEach(([rawCardId, rawRecord]) => {
|
||||
const cardId = String(rawCardId || "").trim();
|
||||
if (!cardId || !rawRecord || typeof rawRecord !== "object" || Array.isArray(rawRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = rawRecord as Record<string, unknown>;
|
||||
const resultUrl = normalizeOptionalHttpUrl(record.url);
|
||||
const text = String(record.text || "").trim();
|
||||
const compareSourceUrl = normalizeOptionalHttpUrl(record.compareSourceUrl);
|
||||
const referenceImageUrl = normalizeOptionalHttpUrl(record.referenceImageUrl);
|
||||
const referencePreviewUrl = normalizeOptionalHttpUrl(record.referencePreviewUrl);
|
||||
const label = String(record.label || "").trim() || null;
|
||||
const updatedAt = Number(record.updatedAt);
|
||||
|
||||
if (!resultUrl && !text && !compareSourceUrl && !referenceImageUrl && !referencePreviewUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.push([
|
||||
cardId.slice(0, 160),
|
||||
{
|
||||
url: resultUrl,
|
||||
text,
|
||||
status: normalizeReplacementResultStatus(record.status),
|
||||
updatedAt: Number.isFinite(updatedAt) && updatedAt > 0 ? Math.round(updatedAt) : 0,
|
||||
compareSourceUrl,
|
||||
referenceImageUrl,
|
||||
referencePreviewUrl,
|
||||
label,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
return Object.fromEntries(entries.slice(0, 256));
|
||||
};
|
||||
|
||||
return {
|
||||
detail_replacement: normalizeModeState(raw.detail_replacement),
|
||||
sku_replacement: normalizeModeState(raw.sku_replacement),
|
||||
};
|
||||
}
|
||||
|
||||
function getReplacementResultUrls(
|
||||
value:
|
||||
| NonNullable<AgentGenerationProjectReplacementResultState["detail_replacement"]>
|
||||
| undefined
|
||||
| null
|
||||
) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
Object.values(value || {})
|
||||
.map((record) => String(record?.url || "").trim())
|
||||
.filter((url) => /^https?:\/\//i.test(url))
|
||||
)
|
||||
).slice(0, 128);
|
||||
}
|
||||
|
||||
function reconcileReplacementResultCollections(params: {
|
||||
resultCollections: AgentGenerationProjectResultCollections;
|
||||
replacementResultState: AgentGenerationProjectReplacementResultState;
|
||||
}) {
|
||||
const detailReplacementUrls = getReplacementResultUrls(
|
||||
params.replacementResultState.detail_replacement
|
||||
);
|
||||
const skuReplacementUrls = getReplacementResultUrls(
|
||||
params.replacementResultState.sku_replacement
|
||||
);
|
||||
|
||||
if (detailReplacementUrls.length === 0 && skuReplacementUrls.length === 0) {
|
||||
return params.resultCollections;
|
||||
}
|
||||
|
||||
return {
|
||||
...params.resultCollections,
|
||||
detail_replacement:
|
||||
detailReplacementUrls.length > 0
|
||||
? detailReplacementUrls
|
||||
: params.resultCollections.detail_replacement,
|
||||
sku_replacement:
|
||||
skuReplacementUrls.length > 0
|
||||
? skuReplacementUrls
|
||||
: params.resultCollections.sku_replacement,
|
||||
} satisfies AgentGenerationProjectResultCollections;
|
||||
}
|
||||
|
||||
function inferLegacyResultModeType(
|
||||
workflowState: AgentWorkflowPersistedState,
|
||||
resultUrls: string[]
|
||||
@ -210,11 +338,19 @@ function normalizeWorkspaceState(value: unknown): AgentGenerationProjectWorkspac
|
||||
: {};
|
||||
|
||||
const imageCount = Number(rawImageParams.imageCount);
|
||||
const activeBusinessModeKey = String(raw.activeBusinessModeKey || "").trim();
|
||||
const normalizedBusinessModeKey: AgentWorkflowBusinessModeKey =
|
||||
activeBusinessModeKey === "detail" ||
|
||||
activeBusinessModeKey === "buyer_show" ||
|
||||
activeBusinessModeKey === "main_sku"
|
||||
? activeBusinessModeKey
|
||||
: "main_sku";
|
||||
|
||||
return {
|
||||
input: String(raw.input || "").trim(),
|
||||
attachments,
|
||||
selectedModelId: String(raw.selectedModelId || "").trim() || "auto",
|
||||
activeBusinessModeKey: normalizedBusinessModeKey,
|
||||
imageParams: {
|
||||
aspectRatio: String(rawImageParams.aspectRatio || "").trim() || "auto",
|
||||
imageSize: String(rawImageParams.imageSize || "").trim() || "2K",
|
||||
@ -223,6 +359,36 @@ function normalizeWorkspaceState(value: unknown): AgentGenerationProjectWorkspac
|
||||
};
|
||||
}
|
||||
|
||||
function hasExplicitWorkspaceBusinessModeKey(value: unknown) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(String((value as Record<string, unknown>).activeBusinessModeKey || "").trim());
|
||||
}
|
||||
|
||||
function inferWorkspaceBusinessModeKey(params: {
|
||||
workflowState: AgentWorkflowPersistedState;
|
||||
resultCollections: AgentGenerationProjectResultCollections;
|
||||
replacementResultState: AgentGenerationProjectReplacementResultState;
|
||||
}): AgentWorkflowBusinessModeKey {
|
||||
const detailReferenceCount =
|
||||
params.workflowState.agentModeState?.detailReplacement?.references?.length || 0;
|
||||
const skuReferenceCount =
|
||||
params.workflowState.agentModeState?.skuReplacement?.references?.length || 0;
|
||||
const normalizedResultCollections = normalizeResultCollections(params.resultCollections);
|
||||
const hasDetailResults =
|
||||
(normalizedResultCollections.detail_generation || []).length > 0 ||
|
||||
(normalizedResultCollections.detail_replacement || []).length > 0 ||
|
||||
(normalizedResultCollections.sku_replacement || []).length > 0 ||
|
||||
getReplacementResultUrls(params.replacementResultState.detail_replacement).length > 0 ||
|
||||
getReplacementResultUrls(params.replacementResultState.sku_replacement).length > 0;
|
||||
|
||||
return detailReferenceCount > 0 || skuReferenceCount > 0 || hasDetailResults
|
||||
? "detail"
|
||||
: "main_sku";
|
||||
}
|
||||
|
||||
function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
@ -287,18 +453,33 @@ export function normalizeAgentGenerationProjectPersistedState(
|
||||
).slice(0, 64)
|
||||
: [];
|
||||
const workflowState = normalizeWorkflowState(raw.workflowState);
|
||||
const resultCollections = normalizeMigratedResultCollections({
|
||||
workflowState,
|
||||
resultImageUrls,
|
||||
resultCollections: normalizeResultCollections(raw.resultCollections),
|
||||
});
|
||||
const deletedResultCollections = normalizeResultCollections(raw.deletedResultCollections);
|
||||
const replacementResultState = normalizeAgentGenerationProjectReplacementResultState(
|
||||
raw.replacementResultState
|
||||
);
|
||||
const resultCollections = reconcileReplacementResultCollections({
|
||||
replacementResultState,
|
||||
resultCollections: normalizeMigratedResultCollections({
|
||||
workflowState,
|
||||
resultImageUrls,
|
||||
resultCollections: normalizeResultCollections(raw.resultCollections),
|
||||
}),
|
||||
});
|
||||
const workspaceState = normalizeWorkspaceState(raw.workspaceState);
|
||||
if (!hasExplicitWorkspaceBusinessModeKey(raw.workspaceState)) {
|
||||
workspaceState.activeBusinessModeKey = inferWorkspaceBusinessModeKey({
|
||||
workflowState,
|
||||
resultCollections,
|
||||
replacementResultState,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
workflowState,
|
||||
workspaceState: normalizeWorkspaceState(raw.workspaceState),
|
||||
workspaceState,
|
||||
resultImageUrls,
|
||||
resultCollections,
|
||||
deletedResultCollections,
|
||||
replacementResultState,
|
||||
};
|
||||
}
|
||||
|
||||
@ -495,10 +495,39 @@ export interface AgentGenerationProjectResultCollections {
|
||||
sku_replacement?: string[];
|
||||
}
|
||||
|
||||
export type AgentGenerationReplacementModeType = Extract<
|
||||
AgentGenerationResultModeType,
|
||||
"detail_replacement" | "sku_replacement"
|
||||
>;
|
||||
|
||||
export type AgentGenerationReplacementResultStatus =
|
||||
| "completed"
|
||||
| "passthrough"
|
||||
| "fallback";
|
||||
|
||||
export interface AgentGenerationReplacementResultRecord {
|
||||
url: string | null;
|
||||
text: string;
|
||||
status: AgentGenerationReplacementResultStatus;
|
||||
updatedAt: number;
|
||||
compareSourceUrl?: string | null;
|
||||
referenceImageUrl?: string | null;
|
||||
referencePreviewUrl?: string | null;
|
||||
label?: string | null;
|
||||
}
|
||||
|
||||
export type AgentGenerationProjectReplacementResultState = Partial<
|
||||
Record<
|
||||
AgentGenerationReplacementModeType,
|
||||
Record<string, AgentGenerationReplacementResultRecord>
|
||||
>
|
||||
>;
|
||||
|
||||
export interface AgentGenerationProjectWorkspaceState {
|
||||
input: string;
|
||||
attachments: Array<AgentGenerationProjectWorkspaceAttachment | null>;
|
||||
selectedModelId?: string | null;
|
||||
activeBusinessModeKey?: AgentWorkflowBusinessModeKey;
|
||||
imageParams?: AgentGenerationProjectImageParams;
|
||||
}
|
||||
|
||||
@ -530,6 +559,7 @@ export interface AgentGenerationProjectPersistedState {
|
||||
resultImageUrls?: string[];
|
||||
resultCollections?: AgentGenerationProjectResultCollections;
|
||||
deletedResultCollections?: AgentGenerationProjectResultCollections;
|
||||
replacementResultState?: AgentGenerationProjectReplacementResultState;
|
||||
}
|
||||
|
||||
export interface AgentGenerationProject {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user