Просмотр исходного кода

feat: add one-click AI fix button for continuity check issues

nonvioce 6 месяцев назад
Родитель
Сommit
2a8eb6a764

+ 117 - 9
src/app/features/novel-writer/components/pipeline/Step6ContinuityCheck.tsx

@@ -40,10 +40,68 @@ export default function Step6ContinuityCheck({
   const [checking, setChecking] = useState(false);
   const [results, setResults] = useState<CheckResult[] | null>(null);
   const [error, setError] = useState<string | null>(null);
+  const [fixing, setFixing] = useState(false);
+  const [fixResult, setFixResult] = useState<string | null>(null);
+  const [fixApplied, setFixApplied] = useState(false);
 
   const issueCount = results ? results.filter(r => r.status === 'issue').length : 0;
   const hasIssues = issueCount > 0;
 
+  const handleAutoFix = async () => {
+    if (!apiSettings || !results || !chapterContent) return;
+    setFixing(true);
+    setError(null);
+
+    const issues = results.filter(r => r.status === 'issue');
+    const issueText = issues.map(r => `- ${r.item}: ${r.detail}`).join('\n');
+
+    try {
+      const prompt = `你是资深小说编辑。请修复以下章节中存在的问题。
+
+## 原始章节
+${chapterContent}
+
+## 需要修复的问题
+${issueText}
+
+## 修复要求
+- 只修改有问题的地方,不要重写整章
+- 保持原有文风和叙事节奏
+- 确保修复后逻辑连贯、人物一致
+- 如果是设定漂移问题,修正设定描述
+- 如果是人物失真问题,调整人物语言和行为
+- 如果是伏笔问题,补充必要的铺垫或回收
+
+请直接输出修复后的完整章节内容,不要加解释说明。`;
+
+      const res = await fetch('/api/proxy', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          targetUrl: apiSettings.apiUrl,
+          headers: { Authorization: `Bearer ${apiSettings.apiKey}` },
+          body: {
+            model: apiSettings.model,
+            messages: [{ role: 'user', content: prompt }],
+            max_tokens: 4000,
+          },
+          isOllama: apiSettings.apiUrl.includes('11434'),
+        }),
+      });
+
+      if (!res.ok) throw new Error(`API 请求失败: HTTP ${res.status}`);
+      const data = await res.json();
+      const fixedContent = data.choices?.[0]?.message?.content || '';
+      if (!fixedContent) throw new Error('AI 返回了空内容');
+
+      setFixResult(fixedContent);
+    } catch (err) {
+      setError(err instanceof Error ? err.message : '修复失败');
+    } finally {
+      setFixing(false);
+    }
+  };
+
   const toggle = (id: string) => {
     const next = new Set(selected);
     if (next.has(id)) next.delete(id);
@@ -51,7 +109,8 @@ export default function Step6ContinuityCheck({
     setSelected(next);
   };
 
-  const handleCheck = async () => {
+  const handleCheck = async (contentOverride?: string) => {
+    const effectiveContent = contentOverride || chapterContent;
     if (!apiSettings || selected.size === 0) return;
     setChecking(true);
     setError(null);
@@ -79,7 +138,7 @@ export default function Step6ContinuityCheck({
           }
         } catch { /* skip */ }
       }
-      contextParts.push(`### 本章内容 (第${chapterIndex}章)\n${chapterContent}`);
+      contextParts.push(`### 本章内容 (第${chapterIndex}章)\n${effectiveContent}`);
 
       if (chapterEvents && chapterEvents.length > 0) {
         contextParts.push('### 本章事件节点\n' + chapterEvents.map(e =>
@@ -171,7 +230,7 @@ export default function Step6ContinuityCheck({
               ))}
             </div>
             <div className="flex justify-center">
-              <button onClick={handleCheck} disabled={checking || selected.size === 0}
+              <button onClick={() => handleCheck()} disabled={checking || selected.size === 0}
                 className="flex items-center gap-2 px-5 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50">
                 <Shield className="h-4 w-4" />
                 {checking ? '检查中...' : '运行连续性检查'}
@@ -214,6 +273,45 @@ export default function Step6ContinuityCheck({
           </>
         )}
 
+        {/* Fix result preview */}
+        {fixResult && !fixApplied && (
+          <div className="mt-4 p-4 border border-green-200 bg-green-50 rounded-xl">
+            <h3 className="text-sm font-semibold text-green-800 mb-2">AI 已修复以下问题:</h3>
+            <ul className="text-xs text-green-700 mb-3 space-y-1">
+              {results?.filter(r => r.status === 'issue').map((r, i) => (
+                <li key={i}>• {r.item}: {r.detail}</li>
+              ))}
+            </ul>
+            <details className="mb-3">
+              <summary className="text-xs text-green-600 cursor-pointer hover:underline">查看修复后的内容</summary>
+              <pre className="mt-2 p-3 bg-white border border-green-200 rounded-lg text-xs text-gray-700 whitespace-pre-wrap max-h-60 overflow-y-auto">
+                {fixResult}
+              </pre>
+            </details>
+            <div className="flex gap-2">
+              <button
+                onClick={() => {
+                  // Apply fix and re-run checks with the fixed content
+                  setFixApplied(true);
+                  setResults(null);
+                  setFixResult(null);
+                  // Trigger re-check with fixed content
+                  setTimeout(() => handleCheck(fixResult), 100);
+                }}
+                className="flex-1 px-3 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700"
+              >
+                应用修复并重新检查
+              </button>
+              <button
+                onClick={() => setFixResult(null)}
+                className="px-3 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50"
+              >
+                放弃
+              </button>
+            </div>
+          </div>
+        )}
+
         {/* Actions */}
         <div className="flex justify-between pt-4 border-t">
           <button onClick={onBack}
@@ -221,12 +319,22 @@ export default function Step6ContinuityCheck({
             <ArrowLeft className="h-4 w-4" /> 返回
           </button>
           {hasIssues ? (
-            <div className="flex flex-col items-end gap-1">
-              <button onClick={onComplete}
-                className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-amber-600 rounded-lg hover:bg-amber-700">
-                <AlertTriangle className="h-4 w-4" /> 忽略并继续
-              </button>
-              <span className="text-xs text-red-500">有 {issueCount} 项未通过,建议返回修改后再提交</span>
+            <div className="flex flex-col items-end gap-2">
+              <div className="flex gap-2">
+                <button onClick={handleAutoFix} disabled={fixing || !apiSettings}
+                  className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50">
+                  {fixing ? (
+                    <><span className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full" /> 修复中...</>
+                  ) : (
+                    <><svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg> 一键修复 ({issueCount} 项)</>
+                  )}
+                </button>
+                <button onClick={onComplete}
+                  className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-amber-600 rounded-lg hover:bg-amber-700">
+                  <AlertTriangle className="h-4 w-4" /> 忽略
+                </button>
+              </div>
+              <span className="text-xs text-red-500">有 {issueCount} 项未通过,可一键修复或返回手动修改</span>
             </div>
           ) : (
             <button onClick={onComplete}