fix: enable text edit after ai copy suggestion

This commit is contained in:
Your Name 2026-06-01 19:45:04 +08:00
parent 5461cff1db
commit c00b9e333d
2 changed files with 93 additions and 58 deletions

View File

@ -70,9 +70,14 @@ function uniqueStrings(values: string[]) {
return Array.from(new Set(values.map((item) => normalizeText(item, 240)).filter(Boolean)));
}
function sanitizeSuggestions(items: unknown[], originalText: string): SuggestionItem[] {
function sanitizeSuggestions(
items: unknown[],
originalText: string,
currentText: string
): SuggestionItem[] {
const seen = new Set<string>();
const normalizedOriginal = normalizeText(originalText, 240);
const normalizedCurrent = normalizeText(currentText, 240);
const suggestions: SuggestionItem[] = [];
for (const entry of items) {
@ -81,7 +86,12 @@ function sanitizeSuggestions(items: unknown[], originalText: string): Suggestion
? (entry as Record<string, unknown>)
: null;
const text = normalizeText(raw?.text as string, 120);
if (!text || seen.has(text)) {
if (
!text ||
text === normalizedOriginal ||
text === normalizedCurrent ||
seen.has(text)
) {
continue;
}
seen.add(text);
@ -94,10 +104,6 @@ function sanitizeSuggestions(items: unknown[], originalText: string): Suggestion
}
}
if (suggestions.length === 0 && normalizedOriginal) {
return [{ text: normalizedOriginal, reason: "原文保留" }];
}
return suggestions;
}
@ -131,8 +137,9 @@ function buildSuggestionPrompt(params: {
"4. 风格要让用户向往和代入,少一些直接叫卖式表达,少用专业术语,多用大白话,多写体感和生活感。",
"5. 要结合给定的人群、场景、核心需求和整页语境,但不要脱离原文所在层级。",
"6. 如果原文看起来更像品牌名、型号、编号、价格或固定信息,就不要强行写成夸张广告词,应给更稳妥、接近原意的版本。",
"7. 不要输出解释性长文,不要加引号、编号、emoji。",
"8. 只返回 JSON,格式必须是:{\"suggestions\":[{\"text\":\"...\",\"reason\":\"...\"}]}。",
"7. 不要返回与待改原文或当前输入框内容完全相同的文案,必须给出可触发替换的不同版本。",
"8. 不要输出解释性长文,不要加引号、编号、emoji。",
"9. 只返回 JSON,格式必须是:{\"suggestions\":[{\"text\":\"...\",\"reason\":\"...\"}]}。",
"",
`当前模块:${params.presetName || "详情替换结果图改字"}`,
`图片区域标题:${params.title || "未命名结果图"}`,
@ -206,7 +213,11 @@ export async function POST(request: Request) {
});
const parsedJson = parseJsonText(result.text || "");
const suggestions = sanitizeSuggestions(parsedJson?.suggestions || [], originalText);
const suggestions = sanitizeSuggestions(
parsedJson?.suggestions || [],
originalText,
currentText
);
if (suggestions.length === 0) {
return NextResponse.json(

View File

@ -55,6 +55,42 @@ interface SuggestionStateItem {
model?: string | null;
}
function buildTextEditRequestItems(
textEditItems: AgentImageTextEditItem[],
deletedTextItems: DeletedTextItem[]
) {
const changedItems: CanvasTextEditPayloadItem[] = textEditItems
.map((item) => {
const editedText = item.editedText.trim();
const action: "replace" | "delete" =
editedText.length === 0 ? "delete" : "replace";
return {
id: item.id,
originalText: item.originalText.trim(),
editedText,
action,
};
})
.filter(
(item) =>
item.originalText.length > 0 &&
(item.action === "delete" ||
(item.editedText &&
item.editedText.length > 0 &&
item.originalText !== item.editedText))
);
const removedItems: CanvasTextEditPayloadItem[] = deletedTextItems
.map((item) => ({
id: item.id,
originalText: item.originalText.trim(),
action: "delete" as const,
}))
.filter((item) => item.originalText.length > 0);
return [...changedItems, ...removedItems];
}
function isHttpImageUrl(value?: string | null) {
if (!value) return false;
try {
@ -204,19 +240,11 @@ export function AgentImageTextEditDialog({
});
}, []);
const hasTextEditChanges = useMemo(
() =>
textEditItems.some((item) => {
const originalText = item.originalText.trim();
const editedText = item.editedText.trim();
return (
originalText.length > 0 &&
(editedText.length === 0 || editedText !== originalText)
);
}) ||
deletedTextItems.some((item) => item.originalText.trim().length > 0),
const textEditRequestItems = useMemo(
() => buildTextEditRequestItems(textEditItems, deletedTextItems),
[deletedTextItems, textEditItems]
);
const hasTextEditChanges = textEditRequestItems.length > 0;
const handleGenerateSuggestions = useCallback(
async (item: AgentImageTextEditItem) => {
@ -257,7 +285,18 @@ export function AgentImageTextEditDialog({
presetName: copySuggestionContext?.presetName || undefined,
});
if (!result.success || !result.data?.suggestions?.length) {
const currentItem = textEditItems.find((entry) => entry.id === item.id);
const blockedTexts = new Set(
[item.originalText, currentItem?.editedText]
.map((entry) => String(entry || "").trim())
.filter(Boolean)
);
const suggestions = (result.data?.suggestions || []).filter((suggestion) => {
const text = String(suggestion.text || "").trim();
return Boolean(text) && !blockedTexts.has(text);
});
if (!result.success || suggestions.length === 0) {
throw new Error(result.error || "未生成可用文案建议");
}
@ -265,7 +304,7 @@ export function AgentImageTextEditDialog({
...current,
[item.id]: {
isLoading: false,
suggestions: result.data?.suggestions || [],
suggestions,
error: null,
model: result.data?.model || null,
},
@ -289,9 +328,24 @@ export function AgentImageTextEditDialog({
);
const handleApplySuggestion = useCallback((id: string, value: string) => {
handleTextItemChange(id, value);
toast.success("已套用推荐文案");
}, [handleTextItemChange]);
const nextText = String(value || "").trim();
const target = textEditItems.find((item) => item.id === id);
if (!nextText) {
toast.info("这条推荐文案为空,请换一条");
return;
}
if (target && nextText === target.originalText.trim()) {
toast.info("这条和原文一样,不会触发生成,请换一条推荐文案");
return;
}
if (target && nextText === target.editedText.trim()) {
toast.info("这条已经在输入框里了");
return;
}
handleTextItemChange(id, nextText);
toast.success("已套用推荐文案,可以点击确定生图");
}, [handleTextItemChange, textEditItems]);
const handleApplyTextEdit = useCallback(async () => {
if (!normalizedImageUrl) {
@ -308,36 +362,7 @@ export function AgentImageTextEditDialog({
return;
}
const changedItems: CanvasTextEditPayloadItem[] = textEditItems
.map((item) => {
const editedText = item.editedText.trim();
const action: "replace" | "delete" = editedText.length === 0 ? "delete" : "replace";
return {
id: item.id,
originalText: item.originalText.trim(),
editedText,
action,
};
})
.filter(
(item) =>
item.originalText.length > 0 &&
(item.action === "delete" ||
(item.editedText &&
item.editedText.length > 0 &&
item.originalText !== item.editedText))
);
const removedItems: CanvasTextEditPayloadItem[] = deletedTextItems
.map((item) => ({
id: item.id,
originalText: item.originalText.trim(),
action: "delete" as const,
}))
.filter((item) => item.originalText.length > 0);
const requestItems = [...changedItems, ...removedItems];
if (requestItems.length === 0) {
if (textEditRequestItems.length === 0) {
toast.info("请先把要替换的文字改成新文案,或点击垃圾桶删除该文字");
return;
}
@ -348,7 +373,7 @@ export function AgentImageTextEditDialog({
try {
const result = await imageGenerationService.editCanvasText({
imageUrl: normalizedImageUrl,
items: requestItems,
items: textEditRequestItems,
});
if (!result.success || !result.data?.originalUrl) {
@ -372,12 +397,11 @@ export function AgentImageTextEditDialog({
setIsApplyingTextEdit(false);
}
}, [
deletedTextItems,
isApplyingTextEdit,
normalizedImageUrl,
onApplySuccess,
onOpenChange,
textEditItems,
textEditRequestItems,
]);
return (