From 25c55fcb7f48ef14dc1546c6a6af0efb9b714468 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Fri, 11 Sep 2026 16:53:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=E6=8F=90=E7=A4=BA=E8=AF=8D=E6=B6=A6?= =?UTF-8?q?=E8=89=B2=E7=9A=84=E7=8A=B6=E6=80=81=E6=9C=BA=E6=8A=BD=E6=88=90?= =?UTF-8?q?=20usePromptPolish=EF=BC=8C=E8=81=8A=E5=A4=A9=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E5=8C=BA=E5=85=88=E8=BF=81=E8=BF=87=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 features/project-workspace/usePromptPolish.ts:失败保留原文、首次成功落原文快照(反复润色只覆盖结果)、可重入保护、可注入的上下文与规范化回调,Tauri 调用固定在 AGC 侧(默认 requestChatPromptPolish) - 聊天输入区改用它,行为逐字不变:润色按钮 aria-busy/disabled、Loader2/Sparkles、「润色中…」/失败文案、失败保留原文、恢复原文回到最初原文、草稿清空后重置 - 聊天特有的「发送前提醒」留在输入区:提醒偏好、本轮已确认草稿指纹、表单捕获拦截、面板内润色用另一句失败文案 - chatPromptPolish.ts 的 requestChatPromptPolish 及其单测未改动,继续作为这条 LLM 通道的契约测试 - 护栏:chatPromptPolish.test.tsx(16 条)与 resourceReferenceInput.test.tsx(20 条)全绿 --- .../ResourceReferenceInput.tsx | 175 ++++++++---------- .../project-workspace/usePromptPolish.ts | 155 ++++++++++++++++ 2 files changed, 228 insertions(+), 102 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/usePromptPolish.ts 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 44e7780c9..0621835e6 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 @@ -66,7 +66,6 @@ import { import { chatPromptDraftKey, readChatPromptPolishReminderDisabled, - requestChatPromptPolish, shouldRemindChatPromptPolish, writeChatPromptPolishReminderDisabled, } from './chatPromptPolish'; @@ -93,6 +92,7 @@ import { resourceReferenceMatchesQuery, type ResourceReferenceScope, } from './resourceReferences'; +import { usePromptPolish } from './usePromptPolish'; type ResourceReferenceInputProps = { value: string; @@ -255,21 +255,25 @@ function mentionableAssetReferences( function $staleResourceReferenceNodes( assetsById: ReadonlyMap, ) { - const staleNodes: { node: ResourceReferenceNode; reference: ChatReference }[] = - []; - $getRoot().getChildren().forEach((block) => { - if (!$isElementNode(block)) return; - block.getChildren().forEach((child) => { - if (!$isResourceReferenceNode(child)) return; - if (child.__reference.type !== 'resource') return; - const nextReference = refreshResourceReference( - child.__reference, - assetsById, - ); - if (nextReference === child.__reference) return; - staleNodes.push({ node: child, reference: nextReference }); + const staleNodes: { + node: ResourceReferenceNode; + reference: ChatReference; + }[] = []; + $getRoot() + .getChildren() + .forEach((block) => { + if (!$isElementNode(block)) return; + block.getChildren().forEach((child) => { + if (!$isResourceReferenceNode(child)) return; + if (child.__reference.type !== 'resource') return; + const nextReference = refreshResourceReference( + child.__reference, + assetsById, + ); + if (nextReference === child.__reference) return; + staleNodes.push({ node: child, reference: nextReference }); + }); }); - }); return staleNodes; } @@ -460,9 +464,11 @@ function ResourceReferenceEditor({ .read(() => $staleResourceReferenceNodes(assetsById).length > 0); if (!hasStaleReferences) return; editor.update(() => { - $staleResourceReferenceNodes(assetsById).forEach(({ node, reference }) => { - node.replace($createResourceReferenceNode(reference)); - }); + $staleResourceReferenceNodes(assetsById).forEach( + ({ node, reference }) => { + node.replace($createResourceReferenceNode(reference)); + }, + ); }); }, [assetsById, editor]); @@ -533,14 +539,8 @@ function ResourceReferenceEditor({ ); // —— C8 AI 润色与发送前提醒 —— - // 原文快照只在第一次成功润色时落下,之后反复润色只覆盖结果, - // 因此「恢复原文」永远回到最初原文。 - const [promptPolish, setPromptPolish] = useState<{ - originalText: string; - resultText: string; - } | null>(null); - const [polishing, setPolishing] = useState(false); - const [polishError, setPolishError] = useState(null); + // 润色状态机抽到 `usePromptPolish`(资源侧两处入口共用同一份);这里只剩下 + // 聊天特有的「发送前提醒」:提醒偏好、本轮已确认草稿指纹与表单拦截。 const [reminderOpen, setReminderOpen] = useState(false); const [reminderPolishing, setReminderPolishing] = useState(false); const [reminderDisabled, setReminderDisabled] = useState(() => @@ -562,45 +562,35 @@ function ResourceReferenceEditor({ [onChange], ); - const runPromptPolish = useCallback(async () => { - const draft = liveDraftRef.current; - if (!draft.text.trim()) return null; - return requestChatPromptPolish(draft.text, projectPath || null); - }, [projectPath]); + const readPromptText = useCallback(() => liveDraftRef.current.text, []); + const resolvePolishContext = useCallback( + () => projectPath || null, + [projectPath], + ); + + const promptPolishState = usePromptPolish({ + readPrompt: readPromptText, + applyPrompt: applyPromptText, + resolveContext: resolvePolishContext, + }); + const { + polishing, + error: polishError, + originalText: polishedOriginalText, + polish: runPolish, + restoreOriginal: restoreOriginalPrompt, + clearError: clearPolishError, + reset: resetPromptPolish, + } = promptPolishState; const polishPrompt = useCallback(async () => { - if (polishing) return; - setPolishing(true); - setPolishError(null); - try { - const polished = await runPromptPolish(); - if (!polished) { - // 失败 / 超时 / 未配置模型:保留原文,只给出可重试提示。 - setPolishError('AI 润色失败,可重试'); - return; - } - setPromptPolish((current) => ({ - originalText: current?.originalText ?? liveDraftRef.current.text, - resultText: polished, - })); - applyPromptText(polished); - } finally { - setPolishing(false); - } - }, [applyPromptText, polishing, runPromptPolish]); - - const restoreOriginalPrompt = useCallback(() => { - const originalText = promptPolish?.originalText; - if (originalText === undefined) return; - setPromptPolish(null); - setPolishError(null); - applyPromptText(originalText); - }, [applyPromptText, promptPolish]); + await runPolish(); + }, [runPolish]); const closeReminder = useCallback(() => { setReminderOpen(false); - setPolishError(null); - }, []); + clearPolishError(); + }, [clearPolishError]); // 用户已在提醒面板里做出选择:记下本轮草稿指纹,再真正提交表单。 // 输入区不在表单里时(例如单独渲染的单元测试)没有可提交的表单事件,仅取消提醒状态。 @@ -611,46 +601,29 @@ function ResourceReferenceEditor({ }, []); const useOriginalAndSubmit = useCallback(() => { - setPolishError(null); + clearPolishError(); submitCurrentDraft(); - }, [submitCurrentDraft]); + }, [clearPolishError, submitCurrentDraft]); const polishAndSubmitFromReminder = useCallback(async () => { if (reminderPolishing) return; setReminderPolishing(true); - setPolishError(null); try { - const polished = await runPromptPolish(); - if (!polished) { - // 保留原文并留在提醒面板里,用户可以选择重试或直接使用原文提交。 - setPolishError('AI 润色失败,可重试或使用原文提交'); - return; - } - const nextPromptPolish = { - originalText: promptPolish?.originalText ?? liveDraftRef.current.text, - resultText: polished, - }; - setPromptPolish(nextPromptPolish); - applyPromptText(polished); + const polished = await runPolish({ + failureMessage: 'AI 润色失败,可重试或使用原文提交', + }); + // 失败时留在提醒面板里,用户可以选择重试或直接使用原文提交。 + if (!polished) return; submitCurrentDraft(); } finally { setReminderPolishing(false); } - }, [ - applyPromptText, - promptPolish, - reminderPolishing, - runPromptPolish, - submitCurrentDraft, - ]); + }, [reminderPolishing, runPolish, submitCurrentDraft]); - const setPromptPolishReminderDisabled = useCallback( - (disabled: boolean) => { - writeChatPromptPolishReminderDisabled(disabled); - setReminderDisabled(disabled); - }, - [], - ); + const setPromptPolishReminderDisabled = useCallback((disabled: boolean) => { + writeChatPromptPolishReminderDisabled(disabled); + setReminderDisabled(disabled); + }, []); // 发送前提醒拦截:在捕获阶段拦下 form 的 submit,阻止 React 的表单提交处理器执行, // 等用户在独立面板里做出选择后再用 requestSubmit() 真正提交。 @@ -658,11 +631,13 @@ function ResourceReferenceEditor({ const form = rootRef.current?.closest('form'); if (!form) return; const handleFormSubmit = (event: Event) => { - if (!shouldRemindChatPromptPolish({ - draft: liveDraftRef.current, - acknowledgedDraftKey: acknowledgedDraftKeyRef.current, - reminderDisabled: reminderDisabledRef.current, - })) { + if ( + !shouldRemindChatPromptPolish({ + draft: liveDraftRef.current, + acknowledgedDraftKey: acknowledgedDraftKeyRef.current, + reminderDisabled: reminderDisabledRef.current, + }) + ) { return; } event.preventDefault(); @@ -676,10 +651,9 @@ function ResourceReferenceEditor({ // 草稿发出去或被清空后重新开始一轮:清掉润色结果与「本轮已确认」标记。 useEffect(() => { if (value.trim() !== '' || references.length > 0) return; - setPromptPolish(null); - setPolishError(null); + resetPromptPolish(); acknowledgedDraftKeyRef.current = null; - }, [references, value]); + }, [references, resetPromptPolish, value]); const renderMentionMenu: MenuRenderFn = useCallback( (_anchorElementRef, itemProps) => { @@ -847,7 +821,7 @@ function ResourceReferenceEditor({