diff --git a/app/api/canvas/text-copy-suggestions/route.ts b/app/api/canvas/text-copy-suggestions/route.ts index 8e0624685..af627c036 100644 --- a/app/api/canvas/text-copy-suggestions/route.ts +++ b/app/api/canvas/text-copy-suggestions/route.ts @@ -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(); 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) : 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( diff --git a/components/agents/AgentImageTextEditDialog.tsx b/components/agents/AgentImageTextEditDialog.tsx index 12fabe320..482162f04 100644 --- a/components/agents/AgentImageTextEditDialog.tsx +++ b/components/agents/AgentImageTextEditDialog.tsx @@ -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 (