feat: Phase 3 — 前端交互功能完成

This commit is contained in:
2026-06-18 23:37:19 +08:00
parent 886e0d598d
commit 7ac3a3726b
10 changed files with 1387 additions and 261 deletions
+87 -43
View File
@@ -2,6 +2,7 @@
<html lang="zh-CN">
<head>
{{template "head" .}}
<script src="/static/js/sse.js"></script>
</head>
<body class="bg-gray-50 min-h-screen">
{{template "nav" .}}
@@ -56,7 +57,7 @@
<div class="mb-4">
<label class="block text-sm font-medium text-gray-500">详细说明</label>
<ul id="pr-details" class="list-disc list-inside text-gray-700 space-y-1"></ul>
<div id="pr-details" class="text-gray-700 prose text-sm"></div>
</div>
<div class="mb-4">
@@ -70,10 +71,11 @@
</div>
</div>
<!-- Loading indicator -->
<div id="loading" class="text-center py-8 hidden">
<!-- Streaming indicator -->
<div id="streaming" class="text-center py-8 hidden">
<div class="spinner inline-block"></div>
<p class="mt-2 text-gray-500">正在生成 PR 描述...</p>
<p id="streaming-status" class="mt-1 text-xs text-gray-400"></p>
</div>
</div>
</main>
@@ -82,6 +84,7 @@
<script>
const repoId = {{.ID}};
let markdownContent = '';
// Load refs on page load
async function loadRefs() {
@@ -120,7 +123,7 @@
}
}
async function generate() {
function generate() {
const baseRef = document.getElementById('base-ref').value;
const headRef = document.getElementById('head-ref').value;
@@ -131,59 +134,100 @@
const btn = document.getElementById('btn-generate');
const output = document.getElementById('output');
const loading = document.getElementById('loading');
const streaming = document.getElementById('streaming');
const outputContent = document.getElementById('output-content');
btn.disabled = true;
btn.textContent = '生成中...';
output.classList.remove('hidden');
document.getElementById('output-content').classList.add('hidden');
loading.classList.remove('hidden');
outputContent.classList.add('hidden');
streaming.classList.remove('hidden');
try {
const resp = await fetch(`/api/repos/${repoId}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ base_ref: baseRef, head_ref: headRef })
});
// Clear previous results
document.getElementById('pr-title').textContent = '';
document.getElementById('pr-type').textContent = '';
document.getElementById('pr-summary').textContent = '';
document.getElementById('pr-details').innerHTML = '';
document.getElementById('pr-impact').textContent = '';
document.getElementById('pr-markdown').textContent = '';
markdownContent = '';
if (!resp.ok) throw new Error('生成失败');
// Accumulate streaming content
const fields = { title: '', type: '', summary: '', details: '', impact: '' };
const data = await resp.json();
SSE.post(`/api/repos/${repoId}/generate`, {
base_ref: baseRef,
head_ref: headRef,
}, {
title(data) {
fields.title += data.content || '';
document.getElementById('pr-title').textContent = fields.title;
document.getElementById('streaming-status').textContent = '生成标题...';
},
type(data) {
fields.type += data.content || '';
document.getElementById('pr-type').textContent = fields.type;
document.getElementById('streaming-status').textContent = '生成类型...';
},
summary(data) {
fields.summary += data.content || '';
document.getElementById('pr-summary').textContent = fields.summary;
document.getElementById('streaming-status').textContent = '生成摘要...';
},
detail(data) {
fields.details += data.content || '';
document.getElementById('pr-details').innerHTML = renderMarkdown(fields.details);
document.getElementById('streaming-status').textContent = '生成详细说明...';
},
impact(data) {
fields.impact += data.content || '';
document.getElementById('pr-impact').textContent = fields.impact;
document.getElementById('streaming-status').textContent = '生成影响范围...';
},
markdown(data) {
markdownContent += data.content || '';
document.getElementById('pr-markdown').textContent = markdownContent;
},
done() {
streaming.classList.add('hidden');
outputContent.classList.remove('hidden');
btn.disabled = false;
btn.textContent = '生成 PR 描述';
},
error(data) {
alert('生成失败: ' + (data.message || '未知错误'));
streaming.classList.add('hidden');
output.classList.add('hidden');
btn.disabled = false;
btn.textContent = '生成 PR 描述';
},
});
}
document.getElementById('pr-title').textContent = data.title || '';
document.getElementById('pr-type').textContent = data.type || '';
document.getElementById('pr-summary').textContent = data.summary || '';
document.getElementById('pr-impact').textContent = data.impact || '';
const detailsList = document.getElementById('pr-details');
detailsList.innerHTML = '';
if (data.details && Array.isArray(data.details)) {
data.details.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
detailsList.appendChild(li);
});
}
document.getElementById('pr-markdown').textContent = data.markdown || '';
loading.classList.add('hidden');
document.getElementById('output-content').classList.remove('hidden');
} catch (err) {
alert('生成失败: ' + err.message);
output.classList.add('hidden');
} finally {
btn.disabled = false;
btn.textContent = '生成 PR 描述';
}
// Simple markdown rendering (bold, headers, lists, code)
function renderMarkdown(text) {
if (!text) return '';
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.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(/\n/g, '<br>');
// Wrap consecutive <li> in <ul>
html = html.replace(/(<li>.*?<\/li>)(\s*<br>\s*<li>)/g, '$1$2');
return html;
}
function copyMarkdown() {
const markdown = document.getElementById('pr-markdown').textContent;
const markdown = markdownContent || document.getElementById('pr-markdown').textContent;
navigator.clipboard.writeText(markdown).then(() => {
alert('已复制到剪贴板');
}).catch(() => {
// Fallback
const textarea = document.createElement('textarea');
textarea.value = markdown;
document.body.appendChild(textarea);
+188 -95
View File
@@ -6,6 +6,7 @@
<script src="/static/lib/d3.min.js"></script>
<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/graph.js"></script>
<script src="/static/js/diff-viewer.js"></script>
<style>
@@ -18,6 +19,33 @@
.graph-container svg {
display: block;
}
.selection-hint {
font-size: 12px;
color: #6b7280;
padding: 6px 12px;
background: #f3f4f6;
border-radius: 6px;
display: inline-block;
}
.selection-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-family: monospace;
}
.badge-base {
background: #d1fae5;
color: #065f46;
border: 1px solid #a7f3d0;
}
.badge-head {
background: #fef3c7;
color: #92400e;
border: 1px solid #fde68a;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
@@ -39,14 +67,6 @@
</div>
</div>
<!-- Debug Info -->
<div id="debug-info" class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4 text-sm hidden">
<p><strong>调试信息:</strong></p>
<p>仓库 ID: <span id="debug-id"></span></p>
<p>Refs: <span id="debug-refs"></span></p>
<p>Graph: <span id="debug-graph"></span></p>
</div>
<!-- Loading State -->
<div id="loading" class="bg-white rounded-lg shadow-md p-8 text-center">
<div class="spinner inline-block"></div>
@@ -60,6 +80,41 @@
<!-- Main Content -->
<div id="main-content" class="hidden">
<!-- Shared Selection Bar -->
<div class="bg-white rounded-lg shadow-md p-4 mb-4">
<div class="flex items-center gap-4 flex-wrap">
<div class="flex items-center gap-2">
<label class="text-sm font-medium text-gray-600">Base:</label>
<select id="select-base" class="border rounded px-3 py-1.5 text-sm min-w-[160px]">
<option value="">选择分支/标签</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm font-medium text-gray-600">Head:</label>
<select id="select-head" class="border rounded px-3 py-1.5 text-sm min-w-[160px]">
<option value="">选择分支/标签</option>
</select>
</div>
<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">
<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>
</div>
</div>
<div class="mt-2 flex items-center gap-3">
<span class="selection-hint" id="selection-hint">
💡 在 Git Graph 中点击节点选择 Base(绿)和 Head(橙)
</span>
<span id="selection-display" class="hidden">
<span id="sel-base-badge" class="selection-badge badge-base"></span>
<span class="text-gray-400 mx-1">→</span>
<span id="sel-head-badge" class="selection-badge badge-head"></span>
</span>
</div>
</div>
<!-- Tabs -->
<div class="bg-white rounded-lg shadow-md">
<div class="border-b border-gray-200">
@@ -75,46 +130,11 @@
<!-- Graph Tab -->
<div id="panel-graph" class="p-4">
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Base:</label>
<select id="graph-base" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Head:</label>
<select id="graph-head" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
</div>
<div id="git-graph" class="graph-container p-4"></div>
</div>
<!-- Diff Tab -->
<div id="panel-diff" class="p-4 hidden">
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Base:</label>
<select id="diff-base" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Head:</label>
<select id="diff-head" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
<button onclick="loadDiff()" class="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700">
查看 Diff
</button>
<div class="flex gap-1 ml-auto">
<button onclick="setDiffView('side-by-side')" class="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">并排</button>
<button onclick="setDiffView('unified')" class="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">统一</button>
</div>
</div>
<div id="diff-container"></div>
</div>
</div>
@@ -127,23 +147,12 @@
const repoId = '{{.ID}}';
let currentRefs = [];
// Show debug info in development
const showDebug = true;
async function init() {
console.log('Initializing repo page, ID:', repoId);
if (showDebug) {
document.getElementById('debug-info').classList.remove('hidden');
document.getElementById('debug-id').textContent = repoId;
}
try {
// Load repo info first
// Load repo info
const reposResp = await fetch('/api/repos');
if (reposResp.ok) {
const repos = await reposResp.json();
console.log('Repos:', repos);
const repo = repos.find(r => r.id == repoId || r.id === parseInt(repoId));
if (repo) {
document.getElementById('repo-url').textContent = repo.url;
@@ -156,42 +165,15 @@
}
// Load refs
console.log('Loading refs...');
const refsResp = await fetch(`/api/repos/${repoId}/refs`);
if (refsResp.ok) {
currentRefs = await refsResp.json();
console.log('Refs loaded:', currentRefs);
if (showDebug) {
document.getElementById('debug-refs').textContent = currentRefs.length + ' refs';
}
populateRefSelects(currentRefs);
} else {
console.error('Failed to load refs:', refsResp.status);
if (showDebug) {
document.getElementById('debug-refs').textContent = 'Error: ' + refsResp.status;
}
}
// Init graph
console.log('Loading graph...');
const graphResp = await fetch(`/api/repos/${repoId}/graph`);
if (graphResp.ok) {
const graphData = await graphResp.json();
console.log('Graph data:', graphData);
if (showDebug) {
document.getElementById('debug-graph').textContent =
(graphData.commits ? graphData.commits.length : 0) + ' commits, ' +
(graphData.refs ? graphData.refs.length : 0) + ' refs';
}
// Render graph
GitGraph.init('git-graph', repoId);
} else {
console.error('Failed to load graph:', graphResp.status);
if (showDebug) {
document.getElementById('debug-graph').textContent = 'Error: ' + graphResp.status;
}
}
// Init graph with click-to-select
GitGraph.init('git-graph', repoId);
GitGraph.onSelectionChange = onGraphSelectionChange;
// Init diff viewer
DiffViewer.init('diff-container');
@@ -213,7 +195,7 @@
}
function populateRefSelects(refs) {
const selects = ['graph-base', 'graph-head', 'diff-base', 'diff-head'];
const selects = ['select-base', 'select-head'];
const branches = refs.filter(r => !r.is_tag);
const tags = refs.filter(r => r.is_tag);
@@ -227,6 +209,7 @@
branches.forEach(ref => {
const opt = document.createElement('option');
opt.value = ref.name;
opt.dataset.hash = ref.hash;
opt.textContent = ref.name + (ref.is_head ? ' (HEAD)' : '');
group.appendChild(opt);
});
@@ -239,25 +222,119 @@
tags.forEach(ref => {
const opt = document.createElement('option');
opt.value = ref.name;
opt.dataset.hash = ref.hash;
opt.textContent = ref.name;
group.appendChild(opt);
});
select.appendChild(group);
}
// Add "Recent Commits" group
if (GitGraph.commits && GitGraph.commits.length > 0) {
const group = document.createElement('optgroup');
group.label = '最近提交';
GitGraph.commits.slice(0, 20).forEach(commit => {
const opt = document.createElement('option');
opt.value = commit.hash;
opt.dataset.hash = commit.hash;
opt.textContent = `${commit.short_hash} ${commit.message.substring(0, 40)}`;
group.appendChild(opt);
});
select.appendChild(group);
}
});
// Auto-select main/master as base, HEAD as head
const mainBranch = branches.find(b => b.name === 'main' || b.name === 'master');
const headBranch = refs.find(r => r.is_head);
selects.forEach(selectId => {
const select = document.getElementById(selectId);
if (selectId.includes('base') && mainBranch) {
select.value = mainBranch.name;
} else if (selectId.includes('head') && headBranch) {
select.value = headBranch.name;
if (mainBranch) {
document.getElementById('select-base').value = mainBranch.name;
}
if (headBranch) {
document.getElementById('select-head').value = headBranch.name;
}
// Sync dropdowns to graph
syncDropdownsToGraph();
}
// Called when user clicks a commit node in the graph
function onGraphSelectionChange(baseHash, headHash) {
const baseSelect = document.getElementById('select-base');
const headSelect = document.getElementById('select-head');
const selectionDisplay = document.getElementById('selection-display');
const selectionHint = document.getElementById('selection-hint');
// Try to match hash to a ref name
const baseRef = currentRefs.find(r => r.hash === baseHash);
const headRef = currentRefs.find(r => r.hash === headHash);
if (baseHash) {
const val = baseRef ? baseRef.name : baseHash;
// Check if value exists as an option, add if not
ensureOption(baseSelect, val, baseRef ? baseRef.name : GitGraph.getRefName(baseHash));
baseSelect.value = val;
document.getElementById('sel-base-badge').textContent = baseRef ? baseRef.name : GitGraph.getRefName(baseHash);
} else {
baseSelect.value = '';
}
if (headHash) {
const val = headRef ? headRef.name : headHash;
ensureOption(headSelect, val, headRef ? headRef.name : GitGraph.getRefName(headHash));
headSelect.value = val;
document.getElementById('sel-head-badge').textContent = headRef ? headRef.name : GitGraph.getRefName(headHash);
} else {
headSelect.value = '';
}
// Update display
if (baseHash || headHash) {
selectionDisplay.classList.remove('hidden');
selectionHint.classList.add('hidden');
} else {
selectionDisplay.classList.add('hidden');
selectionHint.classList.remove('hidden');
}
// If both selected, auto-load diff
if (baseHash && headHash) {
showTab('diff');
loadDiff();
}
}
// Ensure a select has an option with given value
function ensureOption(select, value, label) {
for (const opt of select.options) {
if (opt.value === value) return;
}
const opt = document.createElement('option');
opt.value = value;
opt.textContent = label || value;
select.appendChild(opt);
}
// Sync dropdown selections to graph highlights
function syncDropdownsToGraph() {
const baseSelect = document.getElementById('select-base');
const headSelect = document.getElementById('select-head');
const sync = () => {
const baseOpt = baseSelect.options[baseSelect.selectedIndex];
const headOpt = headSelect.options[headSelect.selectedIndex];
const baseHash = baseOpt ? (baseOpt.dataset.hash || '') : '';
const headHash = headOpt ? (headOpt.dataset.hash || '') : '';
if (baseHash || headHash) {
GitGraph.setSelection(baseHash || null, headHash || null);
}
});
};
baseSelect.addEventListener('change', sync);
headSelect.addEventListener('change', sync);
}
function showTab(tab) {
@@ -265,13 +342,17 @@
document.getElementById('panel-diff').classList.toggle('hidden', tab !== 'diff');
document.getElementById('tab-graph').classList.toggle('border-blue-500', tab === 'graph');
document.getElementById('tab-graph').classList.toggle('text-blue-600', tab === 'graph');
document.getElementById('tab-graph').classList.toggle('border-transparent', tab !== 'graph');
document.getElementById('tab-graph').classList.toggle('text-gray-500', tab !== 'graph');
document.getElementById('tab-diff').classList.toggle('border-blue-500', tab === 'diff');
document.getElementById('tab-diff').classList.toggle('text-blue-600', tab === 'diff');
document.getElementById('tab-diff').classList.toggle('border-transparent', tab !== 'diff');
document.getElementById('tab-diff').classList.toggle('text-gray-500', tab !== 'diff');
}
function loadDiff() {
const base = document.getElementById('diff-base').value;
const head = document.getElementById('diff-head').value;
const base = document.getElementById('select-base').value;
const head = document.getElementById('select-head').value;
if (!base || !head) {
alert('请选择 Base 和 Head 分支');
return;
@@ -281,6 +362,18 @@
function setDiffView(mode) {
DiffViewer.setViewMode(mode);
// Update button styles
const btnSide = document.getElementById('btn-side-by-side');
const btnUnified = document.getElementById('btn-unified');
if (mode === 'side-by-side') {
btnSide.className = 'px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded font-medium';
btnUnified.className = 'px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded hover:bg-gray-200';
} else {
btnUnified.className = 'px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded font-medium';
btnSide.className = 'px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded hover:bg-gray-200';
}
}
init();
+185 -68
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.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();