feat: Phase 3 — 前端交互功能完成
This commit is contained in:
+185
-68
@@ -5,7 +5,9 @@
|
||||
<link rel="stylesheet" href="/static/lib/diff2html.min.css">
|
||||
<script src="/static/lib/diff2html.min.js"></script>
|
||||
<script src="/static/lib/highlight.min.js"></script>
|
||||
<script src="/static/js/sse.js"></script>
|
||||
<script src="/static/js/diff-viewer.js"></script>
|
||||
<script src="/static/js/review-inline.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
{{template "nav" .}}
|
||||
@@ -29,7 +31,9 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Top-N 文件数</label>
|
||||
<input type="number" id="top-n" value="20" min="1" max="100" class="w-full border rounded-md px-3 py-2">
|
||||
<input type="number" id="top-n" value="{{.TopN}}" min="0" max="100" class="w-full border rounded-md px-3 py-2"
|
||||
placeholder="留空=全部">
|
||||
<p class="text-xs text-gray-400 mt-1">设为 0 分析全部文件</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="startReview()" id="btn-review" class="w-full bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700">
|
||||
@@ -51,13 +55,24 @@
|
||||
<!-- Summary -->
|
||||
<div id="summary" class="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">审查总结</h2>
|
||||
<div id="summary-content" class="text-gray-700"></div>
|
||||
<div id="summary-content" class="text-gray-700 prose text-sm"></div>
|
||||
</div>
|
||||
|
||||
<!-- File Reviews -->
|
||||
<div id="file-reviews" class="space-y-4">
|
||||
<!-- Dynamically populated -->
|
||||
<!-- Inline Diff + Suggestions View -->
|
||||
<div id="diff-review-container" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold text-gray-900">代码变更 & 审查建议</h2>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="toggleDiffView()" id="btn-toggle-diff" class="px-3 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">
|
||||
隐藏 Diff
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="review-diff-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- File Reviews (card-based fallback) -->
|
||||
<div id="file-reviews" class="space-y-4"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -65,6 +80,10 @@
|
||||
|
||||
<script>
|
||||
const repoId = {{.ID}};
|
||||
let currentSSE = null;
|
||||
let diffVisible = true;
|
||||
let allSuggestions = [];
|
||||
let fileReviews = {};
|
||||
|
||||
// Load refs on page load
|
||||
async function loadRefs() {
|
||||
@@ -92,7 +111,6 @@
|
||||
headSelect.appendChild(opt2);
|
||||
});
|
||||
|
||||
// Auto-select main/master as base, HEAD as head
|
||||
const mainBranch = branches.find(b => b.name === 'main' || b.name === 'master');
|
||||
const headBranch = branches.find(r => r.is_head);
|
||||
|
||||
@@ -103,7 +121,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function startReview() {
|
||||
function startReview() {
|
||||
const baseRef = document.getElementById('base-ref').value;
|
||||
const headRef = document.getElementById('head-ref').value;
|
||||
const topN = document.getElementById('top-n').value;
|
||||
@@ -113,6 +131,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort any previous stream
|
||||
if (currentSSE) {
|
||||
currentSSE.abort();
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btn-review');
|
||||
const progress = document.getElementById('progress');
|
||||
const results = document.getElementById('results');
|
||||
@@ -122,80 +145,174 @@
|
||||
progress.classList.remove('hidden');
|
||||
results.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/repos/${repoId}/review`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ base_ref: baseRef, head_ref: headRef, top_n: parseInt(topN) })
|
||||
});
|
||||
// Reset state
|
||||
allSuggestions = [];
|
||||
fileReviews = {};
|
||||
document.getElementById('summary-content').innerHTML = '';
|
||||
document.getElementById('file-reviews').innerHTML = '';
|
||||
|
||||
if (!resp.ok) throw new Error('审查失败');
|
||||
// Progress tracking
|
||||
let totalFiles = 0;
|
||||
let completedFiles = 0;
|
||||
let currentFile = '';
|
||||
|
||||
const data = await resp.json();
|
||||
currentSSE = SSE.post(`/api/repos/${repoId}/review`, {
|
||||
base_ref: baseRef,
|
||||
head_ref: headRef,
|
||||
top_n: parseInt(topN) || 0,
|
||||
}, {
|
||||
start(data) {
|
||||
totalFiles = data.total_files || 0;
|
||||
document.getElementById('progress-text').textContent = `准备审查 ${totalFiles} 个文件...`;
|
||||
document.getElementById('progress-bar').style.width = '5%';
|
||||
},
|
||||
file_start(data) {
|
||||
currentFile = data.file || '';
|
||||
completedFiles++;
|
||||
const pct = totalFiles > 0 ? Math.round((completedFiles / totalFiles) * 80 + 10) : 50;
|
||||
document.getElementById('progress-bar').style.width = pct + '%';
|
||||
document.getElementById('progress-text').textContent = `正在审查: ${currentFile} (${completedFiles}/${totalFiles})`;
|
||||
|
||||
// Display summary
|
||||
document.getElementById('summary-content').innerHTML = data.summary || '无总结';
|
||||
// Initialize file review entry
|
||||
if (!fileReviews[currentFile]) {
|
||||
fileReviews[currentFile] = { filename: currentFile, suggestions: [], summary: '' };
|
||||
}
|
||||
},
|
||||
suggestion(data) {
|
||||
allSuggestions.push(data);
|
||||
|
||||
// Display file reviews
|
||||
const container = document.getElementById('file-reviews');
|
||||
container.innerHTML = '';
|
||||
if (data.file && fileReviews[data.file]) {
|
||||
fileReviews[data.file].suggestions.push(data);
|
||||
}
|
||||
|
||||
if (data.files && Array.isArray(data.files)) {
|
||||
data.files.forEach(file => {
|
||||
const card = createFileCard(file);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
// Add to card view incrementally
|
||||
appendSuggestionCard(data);
|
||||
},
|
||||
file_end(data) {
|
||||
// File done
|
||||
},
|
||||
file_summary(data) {
|
||||
if (data.file && fileReviews[data.file]) {
|
||||
fileReviews[data.file].summary = data.content || '';
|
||||
}
|
||||
},
|
||||
diff(data) {
|
||||
// Store diff for inline rendering
|
||||
if (data.diff) {
|
||||
window._reviewDiff = data.diff;
|
||||
}
|
||||
},
|
||||
summary(data) {
|
||||
document.getElementById('summary-content').innerHTML = renderMarkdown(data.content || '');
|
||||
document.getElementById('progress-bar').style.width = '90%';
|
||||
document.getElementById('progress-text').textContent = '生成总结...';
|
||||
},
|
||||
done() {
|
||||
document.getElementById('progress-bar').style.width = '100%';
|
||||
document.getElementById('progress-text').textContent = '审查完成!';
|
||||
|
||||
results.classList.remove('hidden');
|
||||
} catch (err) {
|
||||
alert('审查失败: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '开始审查';
|
||||
progress.classList.add('hidden');
|
||||
}
|
||||
setTimeout(() => {
|
||||
progress.classList.add('hidden');
|
||||
results.classList.remove('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '开始审查';
|
||||
}, 500);
|
||||
|
||||
// Render inline suggestions if diff is available
|
||||
if (window._reviewDiff) {
|
||||
renderInlineReview(window._reviewDiff);
|
||||
}
|
||||
|
||||
currentSSE = null;
|
||||
},
|
||||
error(data) {
|
||||
alert('审查失败: ' + (data.message || '未知错误'));
|
||||
progress.classList.add('hidden');
|
||||
btn.disabled = false;
|
||||
btn.textContent = '开始审查';
|
||||
currentSSE = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createFileCard(file) {
|
||||
const severityColors = {
|
||||
critical: 'bg-red-100 text-red-800 border-red-200',
|
||||
warning: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
||||
info: 'bg-green-100 text-green-800 border-green-200'
|
||||
};
|
||||
// Append a suggestion card incrementally
|
||||
function appendSuggestionCard(suggestion) {
|
||||
const container = document.getElementById('file-reviews');
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bg-white rounded-lg shadow-md border-l-4 ' + (severityColors[file.max_severity] || severityColors.info);
|
||||
|
||||
let suggestionsHtml = '';
|
||||
if (file.suggestions && Array.isArray(file.suggestions)) {
|
||||
suggestionsHtml = file.suggestions.map(s => `
|
||||
<div class="mt-2 p-2 bg-gray-50 rounded">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium ${severityColors[s.severity] || severityColors.info}">
|
||||
${s.severity || 'info'}
|
||||
</span>
|
||||
${s.line ? `<span class="text-xs text-gray-500">行 ${s.line}</span>` : ''}
|
||||
</div>
|
||||
<p class="text-sm text-gray-700">${s.content || ''}</p>
|
||||
// Find or create file section
|
||||
let fileSection = document.getElementById('file-review-' + cssId(suggestion.file));
|
||||
if (!fileSection) {
|
||||
fileSection = document.createElement('div');
|
||||
fileSection.id = 'file-review-' + cssId(suggestion.file);
|
||||
fileSection.className = 'bg-white rounded-lg shadow-md border-l-4 border-gray-200 mb-4';
|
||||
fileSection.innerHTML = `
|
||||
<div class="p-4 border-b border-gray-100">
|
||||
<h3 class="font-medium text-gray-900 font-mono text-sm">${suggestion.file || ''}</h3>
|
||||
<p class="file-summary text-sm text-gray-600 mt-1"></p>
|
||||
</div>
|
||||
`).join('');
|
||||
<div class="p-4 space-y-2 suggestions-container"></div>`;
|
||||
container.appendChild(fileSection);
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium text-gray-900">${file.filename || ''}</h3>
|
||||
<span class="px-2 py-1 rounded text-xs font-medium ${severityColors[file.max_severity] || severityColors.info}">
|
||||
${file.max_severity || 'info'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-600">${file.summary || ''}</p>
|
||||
${suggestionsHtml}
|
||||
</div>
|
||||
`;
|
||||
// Update border color based on max severity
|
||||
const severities = { critical: 0, warning: 1, info: 2 };
|
||||
const currentMax = fileSection.getAttribute('data-max-severity') || 'info';
|
||||
if ((severities[suggestion.severity] || 2) < (severities[currentMax] || 2)) {
|
||||
fileSection.setAttribute('data-max-severity', suggestion.severity);
|
||||
const colors = { critical: 'border-red-500', warning: 'border-yellow-500', info: 'border-green-500' };
|
||||
fileSection.className = fileSection.className.replace(/border-\w+-500/, '') + ' ' + (colors[suggestion.severity] || colors.info);
|
||||
}
|
||||
|
||||
return card;
|
||||
// Add suggestion card
|
||||
const sugContainer = fileSection.querySelector('.suggestions-container');
|
||||
const card = document.createElement('div');
|
||||
card.innerHTML = ReviewInline.renderSuggestionCard(suggestion);
|
||||
sugContainer.appendChild(card.firstElementChild);
|
||||
}
|
||||
|
||||
function cssId(str) {
|
||||
return (str || '').replace(/[^a-zA-Z0-9]/g, '-');
|
||||
}
|
||||
|
||||
// Render diff with inline suggestions
|
||||
function renderInlineReview(diffString) {
|
||||
const container = document.getElementById('review-diff-container');
|
||||
if (!container) return;
|
||||
|
||||
DiffViewer.init('review-diff-container');
|
||||
DiffViewer.renderDiff(diffString);
|
||||
|
||||
// Insert suggestions inline
|
||||
allSuggestions.forEach((s, i) => {
|
||||
if (s.file && s.line) {
|
||||
DiffViewer.insertSuggestion(s.file, s.line, s.side || 'right', s.severity, s.content, 'sug-' + i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDiffView() {
|
||||
const container = document.getElementById('review-diff-container');
|
||||
const btn = document.getElementById('btn-toggle-diff');
|
||||
diffVisible = !diffVisible;
|
||||
container.style.display = diffVisible ? '' : 'none';
|
||||
btn.textContent = diffVisible ? '隐藏 Diff' : '显示 Diff';
|
||||
}
|
||||
|
||||
// Simple markdown rendering
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/`(.+?)`/g, '<code class="bg-gray-100 px-1 rounded">$1</code>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(\d+)\. (.+)$/gm, '<li>$1. $2</li>')
|
||||
.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
loadRefs();
|
||||
|
||||
Reference in New Issue
Block a user