diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx index 28250a85c..bf205276a 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx @@ -759,8 +759,15 @@ function ResourceReferenceEditor({ const reminderDisabledRef = useRef(reminderDisabled); reminderDisabledRef.current = reminderDisabled; + /** + * 我们自己回填进草稿的那一份文本,用于区分「润色回填」与「用户手改」: + * 只有后者该把上一轮往返留下的提示(截断 / 与原文相同)收掉, + * 否则刚显示出来的提示会被自己的回填立刻清掉。 + */ + const appliedPromptRef = useRef(null); const applyPromptText = useCallback( (text: string) => { + appliedPromptRef.current = text; flushSync(() => { onChange({ text, references: liveDraftRef.current.references }); }); @@ -782,13 +789,25 @@ function ResourceReferenceEditor({ const { polishing, error: polishError, + notice: polishNotice, originalText: polishedOriginalText, polish: runPolish, restoreOriginal: restoreOriginalPrompt, clearError: clearPolishError, + clearNotice: clearPolishNotice, reset: resetPromptPolish, } = promptPolishState; + useEffect(() => { + if ( + appliedPromptRef.current !== null && + value === appliedPromptRef.current + ) { + return; + } + clearPolishNotice(); + }, [clearPolishNotice, value]); + const polishPrompt = useCallback(async () => { await runPolish(); }, [runPolish]); @@ -1083,13 +1102,16 @@ function ResourceReferenceEditor({ ) : null} {/* 提醒面板打开时错误提示只在面板里出现,输入区不重复显示。 */} - {showPolishAction && !reminderOpen && (polishing || polishError) ? ( + {/* 「与原文相同 / 已截断」这类提示也要可见:只报失败会让「润色没变化」看起来像按钮坏了。 */} + {showPolishAction && + !reminderOpen && + (polishing || polishError || polishNotice) ? ( - {polishing ? '润色中…' : polishError} + {polishing ? '润色中…' : (polishError ?? polishNotice)} ) : null} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/usePromptPolish.ts b/apps/ai-game-creator-shell/src/features/project-workspace/usePromptPolish.ts index 76b02b421..e8506823b 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/usePromptPolish.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/usePromptPolish.ts @@ -2,6 +2,15 @@ import { useCallback, useRef, useState } from 'react'; import { requestChatPromptPolish } from './chatPromptPolish'; +/** + * 回包与原文一字不差时的提示语。 + * + * 平台侧有时会把同一句话原样还回来(AGC-004:快速编辑里点「AI 润色」后文案毫无变化, + * 用户以为按钮没反应或已经改过)。这种情况必须给一条说得清的提示,而且**不能**落下 + * 原文快照——没有可回退的变化,就不该冒出「恢复原文」这种假入口。 + */ +export const PROMPT_POLISH_UNCHANGED_NOTICE = 'AI 润色结果与原文相同,未做修改'; + /** * 润色回填前的规范化结果。 * @@ -41,14 +50,19 @@ export type UsePromptPolishOptions = { export type UsePromptPolishResult = { polishing: boolean; error: string | null; - /** 最近一次成功回填的截断提示;没有截断时为 null。 */ + /** 最近一次成功回包的提示:截断说明,或「与原文相同,未做修改」;两者都没有时为 null。 */ notice: string | null; - /** 首次成功润色时落下的原文快照;非空时宿主渲染「恢复原文」。 */ + /** + * 首次**真的改动了文本**的润色时落下的原文快照;非空时宿主渲染「恢复原文」。 + * 回包与原文相同的那些次不落快照(没什么可恢复的)。 + */ originalText: string | null; /** 润色并在成功时回填,返回回填后的文本;失败返回 null 并保留原文。 */ polish: (options?: PromptPolishRunOptions) => Promise; restoreOriginal: () => void; clearError: () => void; + /** 用户自己改了提示词:把上一轮往返留下的提示(截断 / 与原文相同)收掉。 */ + clearNotice: () => void; /** 清掉往返状态(草稿清空、面板换资源时用)。 */ reset: () => void; }; @@ -56,6 +70,10 @@ export type UsePromptPolishResult = { /** * 提示词润色的状态机:失败保留原文、首次成功落原文快照、反复润色只覆盖结果。 * + * 成功但回包与原文逐字相同(规范化前后都没变)时不算「润色出了新东西」:不落原文快照、 + * 不回填宿主状态,只给 {@link PROMPT_POLISH_UNCHANGED_NOTICE} 这条明确反馈, + * 免得用户把「按钮点了没反应」误当成功能坏了、或者以为文案已经被改过。 + * * 聊天输入区与资源侧(生成素材 / 快速编辑)共用这一份;它与界面无关,也不碰共享组件, * 宿主自己决定文本存在哪里、怎么回填。Tauri 调用固定在 AGC 侧 * (`requestChatPromptPolish`),共享 composer 只接一个可选的注入位。 @@ -108,6 +126,18 @@ export function usePromptPolish({ const normalized = normalizeResult ? normalizeResult(polished) : { text: polished }; + if (normalized.text === prompt) { + // 最终要写回宿主的文本与当前文本逐字相同:既没有新内容可回填,也没有可回退的 + // 变化,因此不落原文快照、不写宿主状态——`applyPrompt` 在宿主侧还有「提示词变了 + // 就重铸请求身份」这类副作用,回填一份没变的文本会平白作废一次请求身份。 + // + // 判据必须是**规范化之后**的文本,不能只看回包:润色结果被按长度上限截回原文时 + // (`truncateResourceEditPrompt` 是切片不是 trim),回包与原文不同、写回去却一字 + // 未变,那会渲染出一枚点了等于没点的「恢复原文」,正是要消灭的那种假入口。 + // 截断提示优先——它解释的是「这次为什么没变」。 + setNotice(normalized.notice ?? PROMPT_POLISH_UNCHANGED_NOTICE); + return normalized.text; + } // 原文快照只在第一次成功润色时落下,因此「恢复原文」永远回到最初原文。 setOriginalText((current) => current ?? prompt); setNotice(normalized.notice ?? null); @@ -150,6 +180,10 @@ export function usePromptPolish({ setError(null); }, []); + const clearNotice = useCallback(() => { + setNotice(null); + }, []); + const reset = useCallback(() => { // 作废在飞请求:宿主清空草稿 / 换资源后,迟到的润色结果不许再回填。 requestIdRef.current += 1; @@ -166,6 +200,7 @@ export function usePromptPolish({ polish, restoreOriginal, clearError, + clearNotice, reset, }; } diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView.tsx b/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView.tsx index 50b687831..68886de45 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView.tsx +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView.tsx @@ -414,7 +414,13 @@ export function ResourceCanvasAssetGenerationPanelView({ onChange={(event) => setAssetName(event.currentTarget.value)} /> -