feat: 持久化 AI 审查结果,文件级内联展示与页面集成
This commit is contained in:
+166
-1
@@ -9,6 +9,7 @@
|
||||
<script src="/static/js/sse.js"></script>
|
||||
<script src="/static/js/graph.js"></script>
|
||||
<script src="/static/js/diff-viewer.js"></script>
|
||||
<script src="/static/js/review-inline.js"></script>
|
||||
<style>
|
||||
.graph-container {
|
||||
background: #f8fafc;
|
||||
@@ -98,9 +99,12 @@
|
||||
<button onclick="loadDiff()" class="px-4 py-1.5 bg-blue-600 text-white rounded text-sm hover:bg-blue-700">
|
||||
查看 Diff
|
||||
</button>
|
||||
<div class="flex gap-1 ml-auto">
|
||||
<div class="flex gap-1 ml-auto items-center">
|
||||
<button onclick="setDiffView('side-by-side')" id="btn-side-by-side" class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded font-medium">并排</button>
|
||||
<button onclick="setDiffView('unified')" id="btn-unified" class="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded hover:bg-gray-200">统一</button>
|
||||
<button onclick="startInlineReview()" id="btn-inline-review" class="px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded font-medium hover:bg-purple-200 ml-2">
|
||||
🤖 AI 评审
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-3">
|
||||
@@ -136,6 +140,28 @@
|
||||
<!-- Diff Tab -->
|
||||
<div id="panel-diff" class="p-4 hidden">
|
||||
<div id="diff-container"></div>
|
||||
<!-- Inline review progress -->
|
||||
<div id="review-progress" class="hidden mt-4 bg-white rounded-lg border p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium text-gray-700">🤖 AI 代码审查</span>
|
||||
<button onclick="cancelInlineReview()" class="text-xs text-gray-400 hover:text-gray-600">取消</button>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 mb-2">
|
||||
<div id="review-progress-bar" class="bg-purple-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<p id="review-progress-text" class="text-xs text-gray-500">准备中...</p>
|
||||
</div>
|
||||
<!-- Review summary (shown after completion) -->
|
||||
<div id="review-summary" class="hidden mt-4 bg-white rounded-lg border p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium text-gray-700">审查总结</span>
|
||||
<div class="flex gap-2">
|
||||
<a id="btn-view-full-review" href="#" class="text-xs text-purple-600 hover:underline">查看完整评审</a>
|
||||
<button onclick="clearInlineReview()" class="text-xs text-gray-400 hover:text-gray-600">清除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="review-summary-content" class="text-sm text-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,6 +426,145 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inline AI Review ────────────────────────────────────────
|
||||
|
||||
let inlineReviewSSE = null;
|
||||
let inlineSuggestions = [];
|
||||
|
||||
function startInlineReview() {
|
||||
const base = document.getElementById('select-base').value;
|
||||
const head = document.getElementById('select-head').value;
|
||||
if (!base || !head) {
|
||||
alert('请先选择 Base 和 Head 并查看 Diff');
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel previous if running
|
||||
if (inlineReviewSSE) {
|
||||
inlineReviewSSE.abort();
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btn-inline-review');
|
||||
const progress = document.getElementById('review-progress');
|
||||
const summary = document.getElementById('review-summary');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ 审查中...';
|
||||
progress.classList.remove('hidden');
|
||||
summary.classList.add('hidden');
|
||||
|
||||
inlineSuggestions = [];
|
||||
let totalFiles = 0;
|
||||
let completedFiles = 0;
|
||||
|
||||
inlineReviewSSE = SSE.post(`/api/repos/${repoId}/review`, {
|
||||
base, head, top_n: 0,
|
||||
}, {
|
||||
start(data) {
|
||||
totalFiles = data.total_files || 0;
|
||||
document.getElementById('review-progress-text').textContent = `准备审查 ${totalFiles} 个文件...`;
|
||||
document.getElementById('review-progress-bar').style.width = '5%';
|
||||
},
|
||||
file_start(data) {
|
||||
completedFiles++;
|
||||
const pct = totalFiles > 0 ? Math.round((completedFiles / totalFiles) * 80 + 10) : 50;
|
||||
document.getElementById('review-progress-bar').style.width = pct + '%';
|
||||
document.getElementById('review-progress-text').textContent = `正在审查: ${data.file || ''} (${completedFiles}/${totalFiles})`;
|
||||
},
|
||||
suggestion(data) {
|
||||
inlineSuggestions.push(data);
|
||||
},
|
||||
summary(data) {
|
||||
document.getElementById('review-summary-content').innerHTML = renderInlineMarkdown(data.content || '');
|
||||
document.getElementById('review-progress-bar').style.width = '90%';
|
||||
},
|
||||
done() {
|
||||
document.getElementById('review-progress-bar').style.width = '100%';
|
||||
document.getElementById('review-progress-text').textContent = '审查完成!';
|
||||
|
||||
setTimeout(() => {
|
||||
progress.classList.add('hidden');
|
||||
summary.classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🤖 AI 评审';
|
||||
}, 500);
|
||||
|
||||
// Insert suggestions into the existing diff
|
||||
insertInlineSuggestions();
|
||||
inlineReviewSSE = null;
|
||||
},
|
||||
analysis_saved(data) {
|
||||
const viewLink = document.getElementById('btn-view-full-review');
|
||||
viewLink.href = `/repo/${repoId}/review?base=${encodeURIComponent(base)}&head=${encodeURIComponent(head)}`;
|
||||
},
|
||||
error(data) {
|
||||
alert('审查失败: ' + (data.message || '未知错误'));
|
||||
progress.classList.add('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🤖 AI 评审';
|
||||
inlineReviewSSE = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function cancelInlineReview() {
|
||||
if (inlineReviewSSE) {
|
||||
inlineReviewSSE.abort();
|
||||
inlineReviewSSE = null;
|
||||
}
|
||||
document.getElementById('review-progress').classList.add('hidden');
|
||||
const btn = document.getElementById('btn-inline-review');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🤖 AI 评审';
|
||||
}
|
||||
|
||||
function insertInlineSuggestions() {
|
||||
const fileSeverities = {};
|
||||
const severityOrder = { critical: 3, warning: 2, info: 1 };
|
||||
|
||||
inlineSuggestions.forEach((s, i) => {
|
||||
if (s.file) {
|
||||
DiffViewer.insertFileSuggestion(s.file, s.severity, s.content, 'inline-sug-' + i);
|
||||
const cur = fileSeverities[s.file] || 'info';
|
||||
if ((severityOrder[s.severity] || 0) > (severityOrder[cur] || 0)) {
|
||||
fileSeverities[s.file] = s.severity;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(fileSeverities).forEach(([file, sev]) => {
|
||||
DiffViewer.markFileSeverity(file, sev);
|
||||
});
|
||||
|
||||
// Re-initialize note editor for any new placeholders
|
||||
if (typeof NoteEditor !== 'undefined') {
|
||||
NoteEditor._observeNewPlaceholders && NoteEditor._observeNewPlaceholders();
|
||||
}
|
||||
}
|
||||
|
||||
function clearInlineReview() {
|
||||
// Remove all suggestion elements from the diff
|
||||
const container = document.getElementById('diff-container');
|
||||
container.querySelectorAll('.file-suggestions-container').forEach(el => el.remove());
|
||||
container.querySelectorAll('.file-tree-severity').forEach(el => el.remove());
|
||||
document.getElementById('review-summary').classList.add('hidden');
|
||||
inlineSuggestions = [];
|
||||
}
|
||||
|
||||
function renderInlineMarkdown(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/^### (.+)$/gm, '<h3 class="font-semibold text-sm mt-2">$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2 class="font-semibold mt-2">$1</h2>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/`(.+?)`/g, '<code class="bg-gray-100 px-1 rounded text-xs">$1</code>')
|
||||
.replace(/^- (.+)$/gm, '<li class="ml-4">$1</li>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user