feat: support deleting agent result cards
This commit is contained in:
parent
0a33d99c28
commit
83102c559f
@ -334,6 +334,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
const [agentProjectThumbnail, setAgentProjectThumbnail] = useState<string | null>(null);
|
||||
const [agentProjectResultCollections, setAgentProjectResultCollections] =
|
||||
useState<AgentGenerationProjectResultCollections>({});
|
||||
const [agentProjectDeletedResultCollections, setAgentProjectDeletedResultCollections] =
|
||||
useState<AgentGenerationProjectResultCollections>({});
|
||||
const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false);
|
||||
const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false);
|
||||
const [isAgentProjectSheetOpen, setIsAgentProjectSheetOpen] = useState(false);
|
||||
@ -509,6 +511,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
setAgentProjectTitle("未命名生图项目");
|
||||
setAgentProjectThumbnail(null);
|
||||
setAgentProjectResultCollections({});
|
||||
setAgentProjectDeletedResultCollections({});
|
||||
|
||||
// 🆕 先清空当前画布状态,确保切换时不会显示旧数据
|
||||
useCanvasStore.getState().clearCanvas();
|
||||
@ -658,6 +661,17 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
agentProjectResultCollections.sku_replacement
|
||||
),
|
||||
},
|
||||
deletedResultCollections: {
|
||||
scene_generation: normalizeResultCollectionUrls(
|
||||
agentProjectDeletedResultCollections.scene_generation
|
||||
),
|
||||
detail_replacement: normalizeResultCollectionUrls(
|
||||
agentProjectDeletedResultCollections.detail_replacement
|
||||
),
|
||||
sku_replacement: normalizeResultCollectionUrls(
|
||||
agentProjectDeletedResultCollections.sku_replacement
|
||||
),
|
||||
},
|
||||
resultImageUrls: Array.from(
|
||||
new Set([
|
||||
...normalizeResultCollectionUrls(agentProjectResultCollections.scene_generation),
|
||||
@ -668,6 +682,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
};
|
||||
}, [
|
||||
agentModeState,
|
||||
agentProjectDeletedResultCollections,
|
||||
agentProjectResultCollections,
|
||||
agentWorkspaceState,
|
||||
currentAgentStage,
|
||||
@ -885,6 +900,9 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
setAgentProjectTitle(result.data.title || "未命名生图项目");
|
||||
setAgentProjectThumbnail(result.data.thumbnail || null);
|
||||
setAgentProjectResultCollections(persistedState.resultCollections || {});
|
||||
setAgentProjectDeletedResultCollections(
|
||||
persistedState.deletedResultCollections || {}
|
||||
);
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
toast.error(error instanceof Error ? error.message : "加载生图项目失败");
|
||||
@ -1117,6 +1135,48 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
[agentProjectResultCollections]
|
||||
);
|
||||
|
||||
const handleAgentResultImageDelete = useCallback(
|
||||
(modeType: AgentGenerationResultModeType, imageUrl: string) => {
|
||||
const normalizedUrl = String(imageUrl || "").trim();
|
||||
if (!/^https?:\/\//i.test(normalizedUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCollections: AgentGenerationProjectResultCollections = {
|
||||
...agentProjectResultCollections,
|
||||
[modeType]: normalizeResultCollectionUrls(
|
||||
(agentProjectResultCollections[modeType] || []).filter(
|
||||
(url) => url !== normalizedUrl
|
||||
)
|
||||
),
|
||||
};
|
||||
const nextDeletedCollections: AgentGenerationProjectResultCollections = {
|
||||
...agentProjectDeletedResultCollections,
|
||||
[modeType]: normalizeResultCollectionUrls([
|
||||
...(agentProjectDeletedResultCollections[modeType] || []),
|
||||
normalizedUrl,
|
||||
]),
|
||||
};
|
||||
|
||||
setAgentProjectResultCollections((current) =>
|
||||
areResultCollectionsEqual(current, nextCollections) ? current : nextCollections
|
||||
);
|
||||
setAgentProjectDeletedResultCollections((current) =>
|
||||
areResultCollectionsEqual(current, nextDeletedCollections)
|
||||
? current
|
||||
: nextDeletedCollections
|
||||
);
|
||||
setAgentProjectThumbnail((current) => {
|
||||
const nextThumbnail =
|
||||
current === normalizedUrl
|
||||
? getFirstResultUrlFromCollections(nextCollections)
|
||||
: current;
|
||||
return current === nextThumbnail ? current : nextThumbnail;
|
||||
});
|
||||
},
|
||||
[agentProjectDeletedResultCollections, agentProjectResultCollections]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const normalizedBrand = workflowInputs.brand.trim().toLowerCase();
|
||||
if (!normalizedBrand) {
|
||||
@ -1795,8 +1855,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
onAgentModeStateChange={setAgentModeState}
|
||||
workspaceState={agentWorkspaceState}
|
||||
resultCollections={agentProjectResultCollections}
|
||||
deletedResultCollections={agentProjectDeletedResultCollections}
|
||||
onWorkspaceStateChange={setAgentWorkspaceState}
|
||||
onResultPreviewUrlsChange={handleAgentResultPreviewUrlsChange}
|
||||
onDeleteResultImage={handleAgentResultImageDelete}
|
||||
siteName={siteSettingsData?.siteName}
|
||||
siteLogo={siteSettingsData?.siteLogo}
|
||||
siteLogoDark={siteSettingsData?.siteLogoDark}
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
Sparkles,
|
||||
Upload,
|
||||
UserRound,
|
||||
Trash2,
|
||||
X,
|
||||
Wand2,
|
||||
Settings2,
|
||||
@ -93,10 +94,15 @@ interface AgentGenerationWorkspaceDarkV2Props {
|
||||
workspaceState?: AgentGenerationProjectWorkspaceState | null;
|
||||
onWorkspaceStateChange?: (next: AgentGenerationProjectWorkspaceState) => void;
|
||||
resultCollections?: AgentGenerationProjectResultCollections | null;
|
||||
deletedResultCollections?: AgentGenerationProjectResultCollections | null;
|
||||
onResultPreviewUrlsChange?: (
|
||||
modeType: AgentGenerationResultModeType,
|
||||
urls: string[]
|
||||
) => void;
|
||||
onDeleteResultImage?: (
|
||||
modeType: AgentGenerationResultModeType,
|
||||
imageUrl: string
|
||||
) => void;
|
||||
topNavRightSlot?: ReactNode;
|
||||
showcaseConfig?: AgentWorkflowShowcaseConfig | null;
|
||||
workflowStylePanel?: ReactNode;
|
||||
@ -145,6 +151,14 @@ function normalizeResultCollectionUrls(urls?: string[] | null) {
|
||||
);
|
||||
}
|
||||
|
||||
function excludeDeletedResultUrls(
|
||||
urls: string[] | undefined | null,
|
||||
deletedUrls: string[] | undefined | null
|
||||
) {
|
||||
const deletedUrlSet = new Set(normalizeResultCollectionUrls(deletedUrls));
|
||||
return normalizeResultCollectionUrls(urls).filter((url) => !deletedUrlSet.has(url));
|
||||
}
|
||||
|
||||
function getResultModeLabel(modeType: AgentGenerationResultModeType) {
|
||||
return modeType === "detail_replacement"
|
||||
? "详情替换"
|
||||
@ -677,8 +691,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
onOpenCanvasEditor,
|
||||
workspaceState = null,
|
||||
resultCollections = null,
|
||||
deletedResultCollections = null,
|
||||
onWorkspaceStateChange,
|
||||
onResultPreviewUrlsChange,
|
||||
onDeleteResultImage,
|
||||
topNavRightSlot,
|
||||
showcaseConfig,
|
||||
workflowStylePanel,
|
||||
@ -759,6 +775,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
});
|
||||
const [pendingGeneration, setPendingGeneration] =
|
||||
useState<LocalGenerationPendingState | null>(null);
|
||||
const normalizedDeletedResultCollections = useMemo(
|
||||
() => ({
|
||||
scene_generation: normalizeResultCollectionUrls(
|
||||
deletedResultCollections?.scene_generation
|
||||
),
|
||||
detail_replacement: normalizeResultCollectionUrls(
|
||||
deletedResultCollections?.detail_replacement
|
||||
),
|
||||
sku_replacement: normalizeResultCollectionUrls(
|
||||
deletedResultCollections?.sku_replacement
|
||||
),
|
||||
}),
|
||||
[deletedResultCollections]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const restored = getStoredAgentPendingGenerations().find(
|
||||
@ -1310,8 +1340,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}, [assistantMessagesAfterLastUser, generatedGroups]);
|
||||
|
||||
const allGeneratedImages = useMemo(
|
||||
() => Array.from(new Set(generatedGroups.flatMap((group) => group.imageUrls))),
|
||||
[generatedGroups]
|
||||
() =>
|
||||
Array.from(
|
||||
new Set(
|
||||
generatedGroups
|
||||
.flatMap((group) => group.imageUrls)
|
||||
.filter(
|
||||
(url) =>
|
||||
!normalizedDeletedResultCollections.scene_generation.includes(url) &&
|
||||
!normalizedDeletedResultCollections.detail_replacement.includes(url) &&
|
||||
!normalizedDeletedResultCollections.sku_replacement.includes(url)
|
||||
)
|
||||
)
|
||||
),
|
||||
[generatedGroups, normalizedDeletedResultCollections]
|
||||
);
|
||||
const currentSceneGeneratedUrls = useMemo(
|
||||
() => Array.from(new Set(generatedGroupsAfterLastUser.flatMap((group) => group.imageUrls))),
|
||||
@ -1357,8 +1399,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
() =>
|
||||
latestReplacementResults
|
||||
.map((item) => item.url)
|
||||
.filter((url): url is string => Boolean(url)),
|
||||
[latestReplacementResults]
|
||||
.filter((url): url is string => Boolean(url))
|
||||
.filter(
|
||||
(url) => !normalizedDeletedResultCollections[currentResultModeType].includes(url)
|
||||
),
|
||||
[currentResultModeType, latestReplacementResults, normalizedDeletedResultCollections]
|
||||
);
|
||||
const currentLiveResultUrls = isReplacementPreset
|
||||
? latestReplacementResultUrls
|
||||
@ -1400,9 +1445,18 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const mergedResultCollections = useMemo(
|
||||
() => {
|
||||
const baseCollections = {
|
||||
scene_generation: normalizeResultCollectionUrls(resultCollections?.scene_generation),
|
||||
detail_replacement: normalizeResultCollectionUrls(resultCollections?.detail_replacement),
|
||||
sku_replacement: normalizeResultCollectionUrls(resultCollections?.sku_replacement),
|
||||
scene_generation: excludeDeletedResultUrls(
|
||||
resultCollections?.scene_generation,
|
||||
normalizedDeletedResultCollections.scene_generation
|
||||
),
|
||||
detail_replacement: excludeDeletedResultUrls(
|
||||
resultCollections?.detail_replacement,
|
||||
normalizedDeletedResultCollections.detail_replacement
|
||||
),
|
||||
sku_replacement: excludeDeletedResultUrls(
|
||||
resultCollections?.sku_replacement,
|
||||
normalizedDeletedResultCollections.sku_replacement
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
@ -1410,12 +1464,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
[currentResultModeType]: Array.from(
|
||||
new Set([
|
||||
...baseCollections[currentResultModeType],
|
||||
...normalizeResultCollectionUrls(currentLiveResultUrls),
|
||||
...excludeDeletedResultUrls(
|
||||
currentLiveResultUrls,
|
||||
normalizedDeletedResultCollections[currentResultModeType]
|
||||
),
|
||||
])
|
||||
),
|
||||
} as Record<AgentGenerationResultModeType, string[]>;
|
||||
},
|
||||
[currentLiveResultUrls, currentResultModeType, resultCollections]
|
||||
[
|
||||
currentLiveResultUrls,
|
||||
currentResultModeType,
|
||||
normalizedDeletedResultCollections,
|
||||
resultCollections,
|
||||
]
|
||||
);
|
||||
const availableResultModeTypes = useMemo(
|
||||
() =>
|
||||
@ -1434,43 +1496,61 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
() =>
|
||||
detailReplacementReferenceCards.map((card, index) => {
|
||||
const resultMeta = latestReplacementResults[index];
|
||||
const resultUrl = resultMeta?.url || null;
|
||||
const resultUrl =
|
||||
resultMeta?.url &&
|
||||
!normalizedDeletedResultCollections.detail_replacement.includes(resultMeta.url)
|
||||
? resultMeta.url
|
||||
: null;
|
||||
return {
|
||||
...card,
|
||||
index,
|
||||
resultUrl,
|
||||
resultText: resultMeta?.text || "",
|
||||
resultText: resultUrl ? resultMeta?.text || "" : "",
|
||||
status: getReplacementCardStatus({
|
||||
index,
|
||||
resultUrl,
|
||||
resultText: resultMeta?.text || "",
|
||||
resultText: resultUrl ? resultMeta?.text || "" : "",
|
||||
generatedCount: latestReplacementResults.length,
|
||||
isGenerating: isDetailResultModeGenerating,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
[detailReplacementReferenceCards, isDetailResultModeGenerating, latestReplacementResults]
|
||||
[
|
||||
detailReplacementReferenceCards,
|
||||
isDetailResultModeGenerating,
|
||||
latestReplacementResults,
|
||||
normalizedDeletedResultCollections.detail_replacement,
|
||||
]
|
||||
);
|
||||
const skuResultCards = useMemo(
|
||||
() =>
|
||||
skuReplacementReferenceCards.map((card, index) => {
|
||||
const resultMeta = latestReplacementResults[index];
|
||||
const resultUrl = resultMeta?.url || null;
|
||||
const resultUrl =
|
||||
resultMeta?.url &&
|
||||
!normalizedDeletedResultCollections.sku_replacement.includes(resultMeta.url)
|
||||
? resultMeta.url
|
||||
: null;
|
||||
return {
|
||||
...card,
|
||||
index,
|
||||
resultUrl,
|
||||
resultText: resultMeta?.text || "",
|
||||
resultText: resultUrl ? resultMeta?.text || "" : "",
|
||||
status: getReplacementCardStatus({
|
||||
index,
|
||||
resultUrl,
|
||||
resultText: resultMeta?.text || "",
|
||||
resultText: resultUrl ? resultMeta?.text || "" : "",
|
||||
generatedCount: latestReplacementResults.length,
|
||||
isGenerating: isSkuResultModeGenerating,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
[isSkuResultModeGenerating, latestReplacementResults, skuReplacementReferenceCards]
|
||||
[
|
||||
isSkuResultModeGenerating,
|
||||
latestReplacementResults,
|
||||
normalizedDeletedResultCollections.sku_replacement,
|
||||
skuReplacementReferenceCards,
|
||||
]
|
||||
);
|
||||
const detailProcessedCount = useMemo(
|
||||
() =>
|
||||
@ -1719,10 +1799,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
return;
|
||||
}
|
||||
|
||||
const hasProgress =
|
||||
pendingGeneration.modeType === "scene_generation"
|
||||
? currentSceneGeneratedUrls.length > 0
|
||||
: latestReplacementResults.length > 0;
|
||||
const hasProgress = latestReplacementResults.length > 0;
|
||||
|
||||
if (!hasProgress) {
|
||||
setPendingGeneration(null);
|
||||
@ -2090,6 +2167,25 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
toast.message("已打开原图,可右键另存为");
|
||||
}
|
||||
}, []);
|
||||
const handleDeleteResultImage = useCallback(
|
||||
(modeType: AgentGenerationResultModeType, imageUrl: string) => {
|
||||
const normalizedUrl = String(imageUrl || "").trim();
|
||||
if (!normalizedUrl || !onDeleteResultImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedResultUrl === normalizedUrl) {
|
||||
setSelectedResultUrl(null);
|
||||
}
|
||||
if (fullscreenPreview === normalizedUrl) {
|
||||
setFullscreenPreview(null);
|
||||
}
|
||||
|
||||
onDeleteResultImage(modeType, normalizedUrl);
|
||||
toast.success("结果图已删除");
|
||||
},
|
||||
[fullscreenPreview, onDeleteResultImage, selectedResultUrl]
|
||||
);
|
||||
|
||||
const handleToolStepChange = useCallback(
|
||||
(nextKey: ToolStepKey) => {
|
||||
@ -3370,6 +3466,16 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
当前联动
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDeleteResultImage("detail_replacement", card.resultUrl!)
|
||||
}
|
||||
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border border-rose-400/25 bg-black/60 text-rose-100 transition-colors hover:border-rose-300/45 hover:bg-rose-500/15"
|
||||
aria-label="删除结果图"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@ -3815,6 +3921,16 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className={cn("absolute left-3 top-3 rounded-full border px-2.5 py-1 text-[11px]", statusMeta.className)}>
|
||||
{statusMeta.label}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDeleteResultImage("sku_replacement", card.resultUrl!)
|
||||
}
|
||||
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border border-rose-400/25 bg-black/60 text-rose-100 transition-colors hover:border-rose-300/45 hover:bg-rose-500/15"
|
||||
aria-label="删除结果图"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@ -3926,12 +4042,25 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
>
|
||||
<div className="relative aspect-[4/5] overflow-hidden bg-[linear-gradient(180deg,#090b0c_0%,#111417_100%)]">
|
||||
{card.url ? (
|
||||
<img
|
||||
src={card.url}
|
||||
alt={`${getResultModeLabel(activeResultModeType)} 结果图 ${card.index + 1}`}
|
||||
className="h-full w-full object-contain p-2"
|
||||
loading="lazy"
|
||||
/>
|
||||
<>
|
||||
<img
|
||||
src={card.url}
|
||||
alt={`${getResultModeLabel(activeResultModeType)} 结果图 ${card.index + 1}`}
|
||||
className="h-full w-full object-contain p-2"
|
||||
loading="lazy"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteResultImage(activeResultModeType, card.url!);
|
||||
}}
|
||||
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-full border border-rose-400/25 bg-black/60 text-rose-100 transition-colors hover:border-rose-300/45 hover:bg-rose-500/15"
|
||||
aria-label="删除结果图"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<AnimatedGenerationPlaceholderCard
|
||||
badge={card.status === "generating" ? "生成中" : card.status === "queued" ? "排队中" : "待生成"}
|
||||
|
||||
@ -51,6 +51,7 @@ export function createEmptyAgentGenerationProjectPersistedState(): AgentGenerati
|
||||
workspaceState: createEmptyAgentGenerationProjectWorkspaceState(),
|
||||
resultImageUrls: [],
|
||||
resultCollections: {},
|
||||
deletedResultCollections: {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -274,11 +275,13 @@ export function normalizeAgentGenerationProjectPersistedState(
|
||||
resultImageUrls,
|
||||
resultCollections: normalizeResultCollections(raw.resultCollections),
|
||||
});
|
||||
const deletedResultCollections = normalizeResultCollections(raw.deletedResultCollections);
|
||||
|
||||
return {
|
||||
workflowState,
|
||||
workspaceState: normalizeWorkspaceState(raw.workspaceState),
|
||||
resultImageUrls,
|
||||
resultCollections,
|
||||
deletedResultCollections,
|
||||
};
|
||||
}
|
||||
|
||||
@ -349,6 +349,7 @@ export interface AgentGenerationProjectPersistedState {
|
||||
workspaceState?: AgentGenerationProjectWorkspaceState;
|
||||
resultImageUrls?: string[];
|
||||
resultCollections?: AgentGenerationProjectResultCollections;
|
||||
deletedResultCollections?: AgentGenerationProjectResultCollections;
|
||||
}
|
||||
|
||||
export interface AgentGenerationProject {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user