|
|
@@ -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}
|