feat: refine canvas shortcuts and image candidate selection

This commit is contained in:
134 2026-06-26 19:24:53 +08:00
parent d8af4bb48d
commit e3f5b9720e
3 changed files with 96 additions and 6 deletions

View File

@ -1395,7 +1395,7 @@ export async function POST(request: NextRequest) {
sourceWidth,
},
displayPrompt,
imageCount: 1,
imageCount: useFalScaleAdjustMockup ? 4 : 1,
imageSize:
typeof body.imageSize === "string" && body.imageSize.trim()
? body.imageSize.trim()

View File

@ -1873,6 +1873,7 @@ export function CanvasChatPanel({
const [isSessionMenuOpen, setIsSessionMenuOpen] = useState(false);
const [isComposerModeMenuOpen, setIsComposerModeMenuOpen] = useState(false);
const [selectedSkillTagId, setSelectedSkillTagId] = useState<string | null>(null);
const [areCanvasSkillTagsExpanded, setAreCanvasSkillTagsExpanded] = useState(false);
const [messageViewportKey, setMessageViewportKey] = useState(0);
const messagesEndRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
@ -3794,6 +3795,7 @@ export function CanvasChatPanel({
() => canvasSkillTags.find((tag) => tag.id === selectedSkillTagId) || null,
[canvasSkillTags, selectedSkillTagId]
);
const shouldShowCanvasSkillTagsToggle = canvasSkillTags.length > 4;
const activeChatSession = useMemo(
() => chatSessions.find((session) => session.id === activeChatId) || null,
[activeChatId, chatSessions]
@ -12138,9 +12140,16 @@ export function CanvasChatPanel({
{canvasSkillTags.length > 0 ? (
<div className="mt-8 rounded-[26px] border border-[#e4e7ec] bg-white px-5 py-6 text-center dark:border-white/10 dark:bg-zinc-950/70">
<h3 className="text-[16px] font-semibold leading-none text-[#1f2328] dark:text-zinc-100">
预设快捷功能
{canvasSkillTagsTitle}
</h3>
<div className="mt-6 flex flex-wrap justify-center gap-2">
<div
className={cn(
"mt-6 flex flex-wrap justify-center gap-2 transition-[max-height] duration-200 ease-out",
areCanvasSkillTagsExpanded
? "max-h-none overflow-visible"
: "max-h-[76px] overflow-hidden"
)}
>
{canvasSkillTags.map((tag) => {
const isActive = selectedSkillTag?.id === tag.id;
const TagIcon = getCanvasSkillIconComponent(tag.iconName);
@ -12177,6 +12186,22 @@ export function CanvasChatPanel({
);
})}
</div>
{shouldShowCanvasSkillTagsToggle ? (
<button
type="button"
className="mt-4 inline-flex h-7 items-center justify-center gap-1 rounded-full px-3 text-[12px] font-medium text-[#7b8494] transition-colors hover:bg-[#f4f5f7] hover:text-[#1f2328] dark:text-zinc-400 dark:hover:bg-white/10 dark:hover:text-zinc-100"
onClick={() => setAreCanvasSkillTagsExpanded((expanded) => !expanded)}
aria-expanded={areCanvasSkillTagsExpanded}
>
{areCanvasSkillTagsExpanded ? "收起" : "展开全部"}
<ChevronDown
className={cn(
"h-3.5 w-3.5 transition-transform duration-200",
areCanvasSkillTagsExpanded ? "rotate-180" : ""
)}
/>
</button>
) : null}
</div>
) : null}
</div>

View File

@ -973,7 +973,18 @@ async function applyScaleAdjustVerificationToImages(params: {
return images;
}
const processedImages = [...images];
const evaluatedImages: Array<{
changedInside: boolean;
cleanedImage?: ParsedImage;
diffScores: number[];
image: ParsedImage;
index: number;
outsideChanged: boolean;
outsideDiffScores: number[];
outsidePixelStats: Awaited<
ReturnType<typeof getScaleAdjustOutsidePixelChangeStats>
>;
}> = [];
for (const [index, image] of images.entries()) {
const generatedUrl = image.originalUrl || image.thumbnailUrl;
const generatedBuffer = await fetchImageBufferFromUrl(generatedUrl);
@ -1052,6 +1063,7 @@ async function applyScaleAdjustVerificationToImages(params: {
});
}
let cleanedImage: ParsedImage | undefined;
try {
const cleanedImage = await removeScaleAdjustAnnotationArtifacts({
debugLabel,
@ -1063,7 +1075,17 @@ async function applyScaleAdjustVerificationToImages(params: {
verification,
});
if (cleanedImage) {
processedImages[index] = cleanedImage;
evaluatedImages.push({
changedInside,
cleanedImage,
diffScores,
image,
index,
outsideChanged,
outsideDiffScores,
outsidePixelStats,
});
continue;
}
} catch (cleanupError) {
console.warn("[Canvas Generate Image] Failed to clean scale adjust annotation artifacts:", {
@ -1072,9 +1094,52 @@ async function applyScaleAdjustVerificationToImages(params: {
imageIndex: index,
});
}
evaluatedImages.push({
changedInside,
cleanedImage,
diffScores,
image,
index,
outsideChanged,
outsideDiffScores,
outsidePixelStats,
});
}
return processedImages;
if (evaluatedImages.length === 0) return images;
const scoreEvaluation = (item: (typeof evaluatedImages)[number]) => {
const insideScore = Math.max(...item.diffScores, 0);
const outsideMean = item.outsidePixelStats.meanDiff;
const outsidePenalty = item.outsideChanged ? 100 : 0;
const noChangePenalty = item.changedInside ? 0 : 60;
return insideScore - outsideMean * 0.35 - outsidePenalty - noChangePenalty;
};
const rankedImages = [...evaluatedImages].sort((left, right) => {
const leftPassed = left.changedInside && !left.outsideChanged;
const rightPassed = right.changedInside && !right.outsideChanged;
if (leftPassed !== rightPassed) return leftPassed ? -1 : 1;
return scoreEvaluation(right) - scoreEvaluation(left);
});
const selected = rankedImages[0];
console.info("[Canvas Generate Image] Scale adjust candidate selected:", {
debugLabel,
candidateCount: evaluatedImages.length,
imageIndex: selected.index,
passed: selected.changedInside && !selected.outsideChanged,
changedInside: selected.changedInside,
outsideChanged: selected.outsideChanged,
regionDiffScores: selected.diffScores.map((score) => Number(score.toFixed(2))),
outsidePixelStats: {
changedRatio: Number(selected.outsidePixelStats.changedRatio.toFixed(4)),
meanDiff: Number(selected.outsidePixelStats.meanDiff.toFixed(2)),
},
score: Number(scoreEvaluation(selected).toFixed(2)),
});
return [selected.cleanedImage || selected.image];
}
function buildLocalEditCompositeMaskSvg(params: {