feat: support deleting agent result cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-24 17:48:02 +08:00
parent 0a33d99c28
commit 83102c559f
4 changed files with 222 additions and 27 deletions

View File

@ -334,6 +334,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
const [agentProjectThumbnail, setAgentProjectThumbnail] = useState<string | null>(null); const [agentProjectThumbnail, setAgentProjectThumbnail] = useState<string | null>(null);
const [agentProjectResultCollections, setAgentProjectResultCollections] = const [agentProjectResultCollections, setAgentProjectResultCollections] =
useState<AgentGenerationProjectResultCollections>({}); useState<AgentGenerationProjectResultCollections>({});
const [agentProjectDeletedResultCollections, setAgentProjectDeletedResultCollections] =
useState<AgentGenerationProjectResultCollections>({});
const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false); const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false);
const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false); const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false);
const [isAgentProjectSheetOpen, setIsAgentProjectSheetOpen] = useState(false); const [isAgentProjectSheetOpen, setIsAgentProjectSheetOpen] = useState(false);
@ -509,6 +511,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
setAgentProjectTitle("未命名生图项目"); setAgentProjectTitle("未命名生图项目");
setAgentProjectThumbnail(null); setAgentProjectThumbnail(null);
setAgentProjectResultCollections({}); setAgentProjectResultCollections({});
setAgentProjectDeletedResultCollections({});
// 🆕 先清空当前画布状态,确保切换时不会显示旧数据 // 🆕 先清空当前画布状态,确保切换时不会显示旧数据
useCanvasStore.getState().clearCanvas(); useCanvasStore.getState().clearCanvas();
@ -658,6 +661,17 @@ export default function CanvasEditorPage({ params }: PageProps) {
agentProjectResultCollections.sku_replacement 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( resultImageUrls: Array.from(
new Set([ new Set([
...normalizeResultCollectionUrls(agentProjectResultCollections.scene_generation), ...normalizeResultCollectionUrls(agentProjectResultCollections.scene_generation),
@ -668,6 +682,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
}; };
}, [ }, [
agentModeState, agentModeState,
agentProjectDeletedResultCollections,
agentProjectResultCollections, agentProjectResultCollections,
agentWorkspaceState, agentWorkspaceState,
currentAgentStage, currentAgentStage,
@ -885,6 +900,9 @@ export default function CanvasEditorPage({ params }: PageProps) {
setAgentProjectTitle(result.data.title || "未命名生图项目"); setAgentProjectTitle(result.data.title || "未命名生图项目");
setAgentProjectThumbnail(result.data.thumbnail || null); setAgentProjectThumbnail(result.data.thumbnail || null);
setAgentProjectResultCollections(persistedState.resultCollections || {}); setAgentProjectResultCollections(persistedState.resultCollections || {});
setAgentProjectDeletedResultCollections(
persistedState.deletedResultCollections || {}
);
} catch (error) { } catch (error) {
if (!cancelled) { if (!cancelled) {
toast.error(error instanceof Error ? error.message : "加载生图项目失败"); toast.error(error instanceof Error ? error.message : "加载生图项目失败");
@ -1117,6 +1135,48 @@ export default function CanvasEditorPage({ params }: PageProps) {
[agentProjectResultCollections] [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(() => { useEffect(() => {
const normalizedBrand = workflowInputs.brand.trim().toLowerCase(); const normalizedBrand = workflowInputs.brand.trim().toLowerCase();
if (!normalizedBrand) { if (!normalizedBrand) {
@ -1795,8 +1855,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
onAgentModeStateChange={setAgentModeState} onAgentModeStateChange={setAgentModeState}
workspaceState={agentWorkspaceState} workspaceState={agentWorkspaceState}
resultCollections={agentProjectResultCollections} resultCollections={agentProjectResultCollections}
deletedResultCollections={agentProjectDeletedResultCollections}
onWorkspaceStateChange={setAgentWorkspaceState} onWorkspaceStateChange={setAgentWorkspaceState}
onResultPreviewUrlsChange={handleAgentResultPreviewUrlsChange} onResultPreviewUrlsChange={handleAgentResultPreviewUrlsChange}
onDeleteResultImage={handleAgentResultImageDelete}
siteName={siteSettingsData?.siteName} siteName={siteSettingsData?.siteName}
siteLogo={siteSettingsData?.siteLogo} siteLogo={siteSettingsData?.siteLogo}
siteLogoDark={siteSettingsData?.siteLogoDark} siteLogoDark={siteSettingsData?.siteLogoDark}

View File

@ -16,6 +16,7 @@ import {
Sparkles, Sparkles,
Upload, Upload,
UserRound, UserRound,
Trash2,
X, X,
Wand2, Wand2,
Settings2, Settings2,
@ -93,10 +94,15 @@ interface AgentGenerationWorkspaceDarkV2Props {
workspaceState?: AgentGenerationProjectWorkspaceState | null; workspaceState?: AgentGenerationProjectWorkspaceState | null;
onWorkspaceStateChange?: (next: AgentGenerationProjectWorkspaceState) => void; onWorkspaceStateChange?: (next: AgentGenerationProjectWorkspaceState) => void;
resultCollections?: AgentGenerationProjectResultCollections | null; resultCollections?: AgentGenerationProjectResultCollections | null;
deletedResultCollections?: AgentGenerationProjectResultCollections | null;
onResultPreviewUrlsChange?: ( onResultPreviewUrlsChange?: (
modeType: AgentGenerationResultModeType, modeType: AgentGenerationResultModeType,
urls: string[] urls: string[]
) => void; ) => void;
onDeleteResultImage?: (
modeType: AgentGenerationResultModeType,
imageUrl: string
) => void;
topNavRightSlot?: ReactNode; topNavRightSlot?: ReactNode;
showcaseConfig?: AgentWorkflowShowcaseConfig | null; showcaseConfig?: AgentWorkflowShowcaseConfig | null;
workflowStylePanel?: ReactNode; 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) { function getResultModeLabel(modeType: AgentGenerationResultModeType) {
return modeType === "detail_replacement" return modeType === "detail_replacement"
? "详情替换" ? "详情替换"
@ -677,8 +691,10 @@ export function AgentGenerationWorkspaceDarkV2({
onOpenCanvasEditor, onOpenCanvasEditor,
workspaceState = null, workspaceState = null,
resultCollections = null, resultCollections = null,
deletedResultCollections = null,
onWorkspaceStateChange, onWorkspaceStateChange,
onResultPreviewUrlsChange, onResultPreviewUrlsChange,
onDeleteResultImage,
topNavRightSlot, topNavRightSlot,
showcaseConfig, showcaseConfig,
workflowStylePanel, workflowStylePanel,
@ -759,6 +775,20 @@ export function AgentGenerationWorkspaceDarkV2({
}); });
const [pendingGeneration, setPendingGeneration] = const [pendingGeneration, setPendingGeneration] =
useState<LocalGenerationPendingState | null>(null); 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(() => { useEffect(() => {
const restored = getStoredAgentPendingGenerations().find( const restored = getStoredAgentPendingGenerations().find(
@ -1310,8 +1340,20 @@ export function AgentGenerationWorkspaceDarkV2({
}, [assistantMessagesAfterLastUser, generatedGroups]); }, [assistantMessagesAfterLastUser, generatedGroups]);
const allGeneratedImages = useMemo( 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( const currentSceneGeneratedUrls = useMemo(
() => Array.from(new Set(generatedGroupsAfterLastUser.flatMap((group) => group.imageUrls))), () => Array.from(new Set(generatedGroupsAfterLastUser.flatMap((group) => group.imageUrls))),
@ -1357,8 +1399,11 @@ export function AgentGenerationWorkspaceDarkV2({
() => () =>
latestReplacementResults latestReplacementResults
.map((item) => item.url) .map((item) => item.url)
.filter((url): url is string => Boolean(url)), .filter((url): url is string => Boolean(url))
[latestReplacementResults] .filter(
(url) => !normalizedDeletedResultCollections[currentResultModeType].includes(url)
),
[currentResultModeType, latestReplacementResults, normalizedDeletedResultCollections]
); );
const currentLiveResultUrls = isReplacementPreset const currentLiveResultUrls = isReplacementPreset
? latestReplacementResultUrls ? latestReplacementResultUrls
@ -1400,9 +1445,18 @@ export function AgentGenerationWorkspaceDarkV2({
const mergedResultCollections = useMemo( const mergedResultCollections = useMemo(
() => { () => {
const baseCollections = { const baseCollections = {
scene_generation: normalizeResultCollectionUrls(resultCollections?.scene_generation), scene_generation: excludeDeletedResultUrls(
detail_replacement: normalizeResultCollectionUrls(resultCollections?.detail_replacement), resultCollections?.scene_generation,
sku_replacement: normalizeResultCollectionUrls(resultCollections?.sku_replacement), normalizedDeletedResultCollections.scene_generation
),
detail_replacement: excludeDeletedResultUrls(
resultCollections?.detail_replacement,
normalizedDeletedResultCollections.detail_replacement
),
sku_replacement: excludeDeletedResultUrls(
resultCollections?.sku_replacement,
normalizedDeletedResultCollections.sku_replacement
),
}; };
return { return {
@ -1410,12 +1464,20 @@ export function AgentGenerationWorkspaceDarkV2({
[currentResultModeType]: Array.from( [currentResultModeType]: Array.from(
new Set([ new Set([
...baseCollections[currentResultModeType], ...baseCollections[currentResultModeType],
...normalizeResultCollectionUrls(currentLiveResultUrls), ...excludeDeletedResultUrls(
currentLiveResultUrls,
normalizedDeletedResultCollections[currentResultModeType]
),
]) ])
), ),
} as Record<AgentGenerationResultModeType, string[]>; } as Record<AgentGenerationResultModeType, string[]>;
}, },
[currentLiveResultUrls, currentResultModeType, resultCollections] [
currentLiveResultUrls,
currentResultModeType,
normalizedDeletedResultCollections,
resultCollections,
]
); );
const availableResultModeTypes = useMemo( const availableResultModeTypes = useMemo(
() => () =>
@ -1434,43 +1496,61 @@ export function AgentGenerationWorkspaceDarkV2({
() => () =>
detailReplacementReferenceCards.map((card, index) => { detailReplacementReferenceCards.map((card, index) => {
const resultMeta = latestReplacementResults[index]; const resultMeta = latestReplacementResults[index];
const resultUrl = resultMeta?.url || null; const resultUrl =
resultMeta?.url &&
!normalizedDeletedResultCollections.detail_replacement.includes(resultMeta.url)
? resultMeta.url
: null;
return { return {
...card, ...card,
index, index,
resultUrl, resultUrl,
resultText: resultMeta?.text || "", resultText: resultUrl ? resultMeta?.text || "" : "",
status: getReplacementCardStatus({ status: getReplacementCardStatus({
index, index,
resultUrl, resultUrl,
resultText: resultMeta?.text || "", resultText: resultUrl ? resultMeta?.text || "" : "",
generatedCount: latestReplacementResults.length, generatedCount: latestReplacementResults.length,
isGenerating: isDetailResultModeGenerating, isGenerating: isDetailResultModeGenerating,
}), }),
}; };
}), }),
[detailReplacementReferenceCards, isDetailResultModeGenerating, latestReplacementResults] [
detailReplacementReferenceCards,
isDetailResultModeGenerating,
latestReplacementResults,
normalizedDeletedResultCollections.detail_replacement,
]
); );
const skuResultCards = useMemo( const skuResultCards = useMemo(
() => () =>
skuReplacementReferenceCards.map((card, index) => { skuReplacementReferenceCards.map((card, index) => {
const resultMeta = latestReplacementResults[index]; const resultMeta = latestReplacementResults[index];
const resultUrl = resultMeta?.url || null; const resultUrl =
resultMeta?.url &&
!normalizedDeletedResultCollections.sku_replacement.includes(resultMeta.url)
? resultMeta.url
: null;
return { return {
...card, ...card,
index, index,
resultUrl, resultUrl,
resultText: resultMeta?.text || "", resultText: resultUrl ? resultMeta?.text || "" : "",
status: getReplacementCardStatus({ status: getReplacementCardStatus({
index, index,
resultUrl, resultUrl,
resultText: resultMeta?.text || "", resultText: resultUrl ? resultMeta?.text || "" : "",
generatedCount: latestReplacementResults.length, generatedCount: latestReplacementResults.length,
isGenerating: isSkuResultModeGenerating, isGenerating: isSkuResultModeGenerating,
}), }),
}; };
}), }),
[isSkuResultModeGenerating, latestReplacementResults, skuReplacementReferenceCards] [
isSkuResultModeGenerating,
latestReplacementResults,
normalizedDeletedResultCollections.sku_replacement,
skuReplacementReferenceCards,
]
); );
const detailProcessedCount = useMemo( const detailProcessedCount = useMemo(
() => () =>
@ -1719,10 +1799,7 @@ export function AgentGenerationWorkspaceDarkV2({
return; return;
} }
const hasProgress = const hasProgress = latestReplacementResults.length > 0;
pendingGeneration.modeType === "scene_generation"
? currentSceneGeneratedUrls.length > 0
: latestReplacementResults.length > 0;
if (!hasProgress) { if (!hasProgress) {
setPendingGeneration(null); setPendingGeneration(null);
@ -2090,6 +2167,25 @@ export function AgentGenerationWorkspaceDarkV2({
toast.message("已打开原图,可右键另存为"); 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( const handleToolStepChange = useCallback(
(nextKey: ToolStepKey) => { (nextKey: ToolStepKey) => {
@ -3370,6 +3466,16 @@ export function AgentGenerationWorkspaceDarkV2({
当前联动 当前联动
</div> </div>
) : null} ) : 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 <button
type="button" type="button"
onClick={() => { 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)}> <div className={cn("absolute left-3 top-3 rounded-full border px-2.5 py-1 text-[11px]", statusMeta.className)}>
{statusMeta.label} {statusMeta.label}
</div> </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 <button
type="button" type="button"
onClick={() => { onClick={() => {
@ -3926,12 +4042,25 @@ export function AgentGenerationWorkspaceDarkV2({
> >
<div className="relative aspect-[4/5] overflow-hidden bg-[linear-gradient(180deg,#090b0c_0%,#111417_100%)]"> <div className="relative aspect-[4/5] overflow-hidden bg-[linear-gradient(180deg,#090b0c_0%,#111417_100%)]">
{card.url ? ( {card.url ? (
<img <>
src={card.url} <img
alt={`${getResultModeLabel(activeResultModeType)} 结果图 ${card.index + 1}`} src={card.url}
className="h-full w-full object-contain p-2" alt={`${getResultModeLabel(activeResultModeType)} 结果图 ${card.index + 1}`}
loading="lazy" 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 <AnimatedGenerationPlaceholderCard
badge={card.status === "generating" ? "生成中" : card.status === "queued" ? "排队中" : "待生成"} badge={card.status === "generating" ? "生成中" : card.status === "queued" ? "排队中" : "待生成"}

View File

@ -51,6 +51,7 @@ export function createEmptyAgentGenerationProjectPersistedState(): AgentGenerati
workspaceState: createEmptyAgentGenerationProjectWorkspaceState(), workspaceState: createEmptyAgentGenerationProjectWorkspaceState(),
resultImageUrls: [], resultImageUrls: [],
resultCollections: {}, resultCollections: {},
deletedResultCollections: {},
}; };
} }
@ -274,11 +275,13 @@ export function normalizeAgentGenerationProjectPersistedState(
resultImageUrls, resultImageUrls,
resultCollections: normalizeResultCollections(raw.resultCollections), resultCollections: normalizeResultCollections(raw.resultCollections),
}); });
const deletedResultCollections = normalizeResultCollections(raw.deletedResultCollections);
return { return {
workflowState, workflowState,
workspaceState: normalizeWorkspaceState(raw.workspaceState), workspaceState: normalizeWorkspaceState(raw.workspaceState),
resultImageUrls, resultImageUrls,
resultCollections, resultCollections,
deletedResultCollections,
}; };
} }

View File

@ -349,6 +349,7 @@ export interface AgentGenerationProjectPersistedState {
workspaceState?: AgentGenerationProjectWorkspaceState; workspaceState?: AgentGenerationProjectWorkspaceState;
resultImageUrls?: string[]; resultImageUrls?: string[];
resultCollections?: AgentGenerationProjectResultCollections; resultCollections?: AgentGenerationProjectResultCollections;
deletedResultCollections?: AgentGenerationProjectResultCollections;
} }
export interface AgentGenerationProject { export interface AgentGenerationProject {