8e8ea6a07d
Deploy PR-Helper / deploy (push) Successful in 2m9s
- 新增 PR 描述 tab:流式生成 + 双栏实时预览(渲染效果 + Markdown 源码) - 新增 AI 代码审查 tab:Top-N/并发设置、历史记录、进度条、总结、备注、PDF 导出、内联 Diff - 保留 Diff 视图中的 AI 评审快捷内联按钮 - Repo handler 传递 TopN/Concurrency 设置 - 删除独立的 generate.html 和 review.html 页面及路由
1173 lines
55 KiB
HTML
1173 lines
55 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
{{template "head" .}}
|
|
<link rel="stylesheet" href="/static/lib/diff2html.min.css">
|
|
<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/lib/marked.min.js"></script>
|
|
<script src="/static/js/markdown.js"></script>
|
|
<script src="/static/js/diff-viewer.js"></script>
|
|
<script src="/static/js/review-inline.js"></script>
|
|
<script src="/static/js/note-editor.js"></script>
|
|
<style>
|
|
.graph-container {
|
|
background: #f8fafc;
|
|
border-radius: 8px;
|
|
min-height: 300px;
|
|
overflow: auto;
|
|
}
|
|
.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;
|
|
}
|
|
@media print {
|
|
nav, footer, #review-actions, #btn-review, #btn-toggle-diff,
|
|
.note-save-indicator, select, input, #review-progress,
|
|
#diff-review-container, #history-select,
|
|
label[for="base-ref"], label[for="head-ref"],
|
|
label[for="top-n"], label[for="concurrency"] { display: none !important; }
|
|
|
|
#review-results { display: block !important; }
|
|
.print-header { display: block !important; }
|
|
|
|
body { background: white !important; padding: 0 !important; font-size: 12px; }
|
|
main { max-width: 100% !important; padding: 16px !important; }
|
|
|
|
.bg-white { box-shadow: none !important; border: 1px solid #e5e7eb; }
|
|
.bg-white.rounded-lg { break-inside: avoid; }
|
|
|
|
#file-reviews > div { break-inside: avoid; margin-bottom: 12px; }
|
|
|
|
.note-textarea {
|
|
border: none !important;
|
|
padding: 0 !important;
|
|
resize: none !important;
|
|
height: auto !important;
|
|
overflow: visible !important;
|
|
white-space: pre-wrap;
|
|
background: #f0f9ff !important;
|
|
padding: 8px !important;
|
|
border-left: 3px solid #3b82f6 !important;
|
|
}
|
|
|
|
.suggestion-card { break-inside: avoid; }
|
|
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
|
|
|
.rounded-full { border: 1px solid currentColor; }
|
|
|
|
#top-n, #concurrency {
|
|
display: inline !important;
|
|
border: none !important;
|
|
padding: 0 !important;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
{{template "nav" .}}
|
|
|
|
<main class="max-w-7xl mx-auto px-4 py-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900">仓库详情</h1>
|
|
<p id="repo-url" class="text-gray-500 text-sm mt-1"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loading" class="bg-white rounded-lg shadow-md p-8 text-center">
|
|
<div class="spinner inline-block"></div>
|
|
<p class="mt-2 text-gray-500">加载仓库数据...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="error" class="bg-red-50 border border-red-200 rounded-lg p-4 hidden">
|
|
<p class="text-red-600" id="error-message"></p>
|
|
</div>
|
|
|
|
<!-- 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 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">
|
|
<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">
|
|
<nav class="flex -mb-px">
|
|
<button onclick="showTab('graph')" id="tab-graph" class="px-6 py-3 text-sm font-medium border-b-2 border-blue-500 text-blue-600">
|
|
Git Graph
|
|
</button>
|
|
<button onclick="showTab('diff')" id="tab-diff" class="px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
|
Diff 视图
|
|
</button>
|
|
<button onclick="showTab('generate')" id="tab-generate" class="px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
|
PR 描述
|
|
</button>
|
|
<button onclick="showTab('review')" id="tab-review" class="px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
|
AI 代码审查
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Graph Tab -->
|
|
<div id="panel-graph" class="p-4">
|
|
<div id="git-graph" class="graph-container p-4"></div>
|
|
</div>
|
|
|
|
<!-- 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">
|
|
<button onclick="showTab('review'); startReview()" class="text-xs text-purple-600 hover:underline">查看完整审查</button>
|
|
<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>
|
|
|
|
<!-- PR Description Tab -->
|
|
<div id="panel-generate" class="p-4 hidden">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-2">
|
|
<h2 class="text-lg font-semibold text-gray-900">PR 描述</h2>
|
|
<div id="generating-indicator" class="hidden flex items-center gap-2">
|
|
<div class="spinner inline-block"></div>
|
|
<span id="streaming-status" class="text-xs text-gray-400"></span>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="generatePR()" id="btn-generate" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
|
|
生成 PR 描述
|
|
</button>
|
|
<button onclick="copyMarkdown()" class="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 text-sm">
|
|
复制 Markdown
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="generate-output" class="hidden">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<div class="text-xs text-gray-400 mb-1">渲染效果</div>
|
|
<div id="pr-rendered" class="text-gray-700 prose text-sm p-4 border rounded-md bg-white min-h-[200px]"></div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs text-gray-400 mb-1">Markdown 源码</div>
|
|
<pre id="pr-markdown" class="whitespace-pre-wrap text-sm text-gray-800 font-mono p-4 border rounded-md bg-gray-50 min-h-[200px] overflow-auto"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Code Review Tab -->
|
|
<div id="panel-review" class="p-4 hidden">
|
|
<!-- Review Settings -->
|
|
<div class="mb-6">
|
|
<div class="grid grid-cols-3 gap-4 mb-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Top-N 文件数</label>
|
|
<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>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">并发分析数</label>
|
|
<input type="number" id="concurrency" value="{{.Concurrency}}" min="1" max="20" class="w-full border rounded-md px-3 py-2" placeholder="默认 5">
|
|
<p class="text-xs text-gray-400 mt-1">同时分析的文件数 (1-20)</p>
|
|
</div>
|
|
<div class="flex items-end">
|
|
<button onclick="startReview()" id="btn-review" class="w-full bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700">
|
|
开始审查
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- History -->
|
|
<div class="pt-4 border-t border-gray-200">
|
|
<div class="flex items-center gap-3">
|
|
<label class="text-sm font-medium text-gray-600">📋 历史评审:</label>
|
|
<select id="history-select" class="flex-1 border rounded-md px-3 py-1.5 text-sm" onchange="loadHistoryReview(this.value)">
|
|
<option value="">加载中...</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress -->
|
|
<div id="review-progress-full" class="mb-6 hidden">
|
|
<h3 class="font-semibold text-gray-700 mb-2">审查进度</h3>
|
|
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-2">
|
|
<div id="review-progress-bar-full" class="bg-purple-600 h-2.5 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
</div>
|
|
<p id="review-progress-text-full" class="text-sm text-gray-500">准备中...</p>
|
|
</div>
|
|
|
|
<!-- Results -->
|
|
<div id="review-results" class="hidden">
|
|
<!-- Print-only header -->
|
|
<div class="print-header hidden mb-6">
|
|
<h1 style="font-size:20px;font-weight:700;color:#111827;margin-bottom:8px;">PR-Helper 审查报告</h1>
|
|
<p style="color:#6b7280;font-size:12px;">
|
|
📦 仓库: <span id="print-repo-url"></span>
|
|
🌿 分支: <span class="print-base-ref"></span> → <span class="print-head-ref"></span>
|
|
📅 <span class="print-date"></span>
|
|
</p>
|
|
<hr style="margin-top:12px;border:none;border-top:1px solid #e5e7eb;">
|
|
</div>
|
|
|
|
<!-- 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 prose text-sm"></div>
|
|
</div>
|
|
|
|
<!-- Overall Note Editor -->
|
|
<div id="overall-note-section" class="bg-white rounded-lg shadow-md p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-3">📝 整体备注</h2>
|
|
<div class="note-editor" data-note-key="overall:" data-scope="overall" data-scope-key="">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-xs text-gray-400">💬 备注 (Markdown)</span>
|
|
<span class="note-save-indicator text-xs"></span>
|
|
</div>
|
|
<textarea class="note-textarea" placeholder="添加整体备注..." rows="2"
|
|
oninput="NoteEditor.onInput(this, 'overall', '')"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions Bar -->
|
|
<div id="review-actions" class="flex items-center gap-3 mb-6 hidden">
|
|
<button onclick="NoteEditor.exportPDF()" id="btn-export-pdf"
|
|
class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 text-sm">
|
|
📄 导出 PDF 报告
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
{{template "footer" .}}
|
|
|
|
<script>
|
|
const repoId = '{{.ID}}';
|
|
let currentRefs = [];
|
|
|
|
// ── Init ────────────────────────────────────────────────────────
|
|
async function init() {
|
|
try {
|
|
// Load repo info
|
|
const reposResp = await fetch('/api/repos', { credentials: 'same-origin' });
|
|
if (reposResp.ok) {
|
|
const repos = await reposResp.json();
|
|
const repo = repos.find(r => r.id == repoId || r.id === parseInt(repoId));
|
|
if (repo) {
|
|
document.getElementById('repo-url').textContent = repo.url;
|
|
document.getElementById('print-repo-url').textContent = repo.url;
|
|
} else {
|
|
showError('仓库未找到 (ID: ' + repoId + ')');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Load refs
|
|
const refsResp = await fetch(`/api/repos/${repoId}/refs`, { credentials: 'same-origin' });
|
|
if (refsResp.ok) {
|
|
currentRefs = await refsResp.json();
|
|
|
|
// Fetch recent commits independently
|
|
let commits = [];
|
|
try {
|
|
const graphResp = await fetch(`/api/repos/${repoId}/graph?max_commits=20`, { credentials: 'same-origin' });
|
|
if (graphResp.ok) { const gd = await graphResp.json(); commits = gd.commits || []; }
|
|
} catch (e) { /* ignore */ }
|
|
|
|
populateRefSelects(currentRefs, commits);
|
|
}
|
|
|
|
// Init graph with click-to-select
|
|
GitGraph.init('git-graph', repoId);
|
|
GitGraph.onSelectionChange = onGraphSelectionChange;
|
|
|
|
// Init diff viewer
|
|
DiffViewer.init('diff-container');
|
|
|
|
// Load review history
|
|
loadHistoryList();
|
|
|
|
// Show main content
|
|
document.getElementById('loading').classList.add('hidden');
|
|
document.getElementById('main-content').classList.remove('hidden');
|
|
|
|
} catch (err) {
|
|
console.error('Init error:', err);
|
|
showError('加载失败: ' + err.message);
|
|
}
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById('loading').classList.add('hidden');
|
|
document.getElementById('error').classList.remove('hidden');
|
|
document.getElementById('error-message').textContent = message;
|
|
}
|
|
|
|
// ── Ref Selects ─────────────────────────────────────────────────
|
|
|
|
function populateRefSelects(refs, commits) {
|
|
const selects = ['select-base', 'select-head'];
|
|
const branches = refs.filter(r => !r.is_tag);
|
|
const tags = refs.filter(r => r.is_tag);
|
|
|
|
selects.forEach(selectId => {
|
|
const select = document.getElementById(selectId);
|
|
select.innerHTML = '<option value="">选择分支/标签</option>';
|
|
|
|
if (branches.length > 0) {
|
|
const group = document.createElement('optgroup');
|
|
group.label = '分支';
|
|
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);
|
|
});
|
|
select.appendChild(group);
|
|
}
|
|
|
|
if (tags.length > 0) {
|
|
const group = document.createElement('optgroup');
|
|
group.label = '标签';
|
|
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 (commits && commits.length > 0) {
|
|
const group = document.createElement('optgroup');
|
|
group.label = '最近提交';
|
|
commits.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);
|
|
|
|
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;
|
|
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);
|
|
}
|
|
|
|
// ── Tab Navigation ──────────────────────────────────────────────
|
|
|
|
const TABS = ['graph', 'diff', 'generate', 'review'];
|
|
const TAB_STYLES = {
|
|
active: 'border-blue-500 text-blue-600',
|
|
inactive: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
|
|
};
|
|
|
|
function showTab(tab) {
|
|
TABS.forEach(t => {
|
|
document.getElementById('panel-' + t).classList.toggle('hidden', t !== tab);
|
|
const tabBtn = document.getElementById('tab-' + t);
|
|
tabBtn.className = `px-6 py-3 text-sm font-medium border-b-2 ${t === tab ? TAB_STYLES.active : TAB_STYLES.inactive}`;
|
|
});
|
|
}
|
|
|
|
// ── Diff ────────────────────────────────────────────────────────
|
|
|
|
function loadDiff() {
|
|
const base = document.getElementById('select-base').value;
|
|
const head = document.getElementById('select-head').value;
|
|
if (!base || !head) {
|
|
showToast('请选择 Base 和 Head 分支', 'warning');
|
|
return;
|
|
}
|
|
DiffViewer.loadDiff(repoId, base, head);
|
|
}
|
|
|
|
function setDiffView(mode) {
|
|
DiffViewer.setViewMode(mode);
|
|
|
|
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';
|
|
}
|
|
}
|
|
|
|
// ── Utilities ───────────────────────────────────────────────────
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
return String(text)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function renderSummaryHtml(data) {
|
|
let html = '<div class="space-y-3">';
|
|
const score = data.score || 0;
|
|
const scoreColor = score >= 7 ? 'bg-green-100 text-green-800' : score >= 4 ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800';
|
|
html += `<div class="flex items-center gap-2"><span class="text-sm font-medium text-gray-600">整体评分</span><span class="px-2 py-0.5 rounded-full text-xs font-semibold ${scoreColor}">${score}/10</span></div>`;
|
|
if (data.overall) {
|
|
html += `<div><span class="text-sm font-medium text-gray-600">总体评价</span><p class="mt-1 text-sm text-gray-800">${escapeHtml(data.overall)}</p></div>`;
|
|
}
|
|
if (data.findings) {
|
|
html += `<div><span class="text-sm font-medium text-gray-600">主要发现</span><p class="mt-1 text-sm text-gray-800">${escapeHtml(data.findings)}</p></div>`;
|
|
}
|
|
if (data.recommendations) {
|
|
html += `<div><span class="text-sm font-medium text-gray-600">改进建议</span><p class="mt-1 text-sm text-gray-800">${escapeHtml(data.recommendations)}</p></div>`;
|
|
}
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
function renderMarkdown(text) {
|
|
return Markdown.render(text);
|
|
}
|
|
|
|
// ── Inline AI Review (quick review in Diff tab) ─────────────────
|
|
|
|
let inlineReviewSSE = null;
|
|
let inlineSuggestions = [];
|
|
|
|
function startInlineReview() {
|
|
const base = document.getElementById('select-base').value;
|
|
const head = document.getElementById('select-head').value;
|
|
if (!base || !head) {
|
|
showToast('请先选择 Base 和 Head 并查看 Diff', 'warning');
|
|
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) {
|
|
const el = document.getElementById('review-summary-content');
|
|
if (data.score !== undefined) {
|
|
el.innerHTML = renderSummaryHtml(data);
|
|
} else {
|
|
el.innerHTML = renderMarkdown(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) {
|
|
// Update "查看完整审查" link behavior
|
|
},
|
|
error(data) {
|
|
showToast('审查失败: ' + (data.message || '未知错误'), 'error');
|
|
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() {
|
|
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 = [];
|
|
}
|
|
|
|
// ── Generate PR Description ─────────────────────────────────────
|
|
|
|
let markdownContent = '';
|
|
let generateSSE = null;
|
|
|
|
function generatePR() {
|
|
const base = document.getElementById('select-base').value;
|
|
const head = document.getElementById('select-head').value;
|
|
|
|
if (!base || !head) {
|
|
showToast('请选择 Base 和 Head 分支', 'warning');
|
|
return;
|
|
}
|
|
|
|
// Cancel previous if running
|
|
if (generateSSE) {
|
|
generateSSE.abort();
|
|
}
|
|
|
|
const btn = document.getElementById('btn-generate');
|
|
const output = document.getElementById('generate-output');
|
|
const indicator = document.getElementById('generating-indicator');
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = '生成中...';
|
|
btn.classList.add('opacity-70', 'cursor-not-allowed');
|
|
output.classList.remove('hidden');
|
|
indicator.classList.remove('hidden');
|
|
|
|
// Clear previous results
|
|
document.getElementById('pr-rendered').innerHTML = '';
|
|
document.getElementById('pr-markdown').textContent = '';
|
|
markdownContent = '';
|
|
|
|
generateSSE = SSE.post(`/api/repos/${repoId}/generate`, {
|
|
base, head,
|
|
}, {
|
|
content(data) {
|
|
markdownContent += data.content || '';
|
|
document.getElementById('pr-rendered').innerHTML = Markdown.render(markdownContent);
|
|
document.getElementById('pr-markdown').textContent = markdownContent;
|
|
},
|
|
done() {
|
|
indicator.classList.add('hidden');
|
|
btn.disabled = false;
|
|
btn.textContent = '生成 PR 描述';
|
|
btn.classList.remove('opacity-70', 'cursor-not-allowed');
|
|
generateSSE = null;
|
|
},
|
|
error(data) {
|
|
showToast('生成失败: ' + (data.message || '未知错误'), 'error');
|
|
indicator.classList.add('hidden');
|
|
btn.disabled = false;
|
|
btn.textContent = '生成 PR 描述';
|
|
btn.classList.remove('opacity-70', 'cursor-not-allowed');
|
|
generateSSE = null;
|
|
},
|
|
});
|
|
}
|
|
|
|
function copyMarkdown() {
|
|
const markdown = markdownContent || document.getElementById('pr-markdown').textContent;
|
|
if (!markdown.trim()) {
|
|
showToast('没有可复制的内容,请先生成 PR 描述', 'warning');
|
|
return;
|
|
}
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(markdown).then(() => {
|
|
showToast('已复制到剪贴板', 'success');
|
|
}).catch(() => {
|
|
fallbackCopy(markdown);
|
|
});
|
|
} else {
|
|
fallbackCopy(markdown);
|
|
}
|
|
}
|
|
|
|
function fallbackCopy(text) {
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = text;
|
|
textarea.style.cssText = 'position:fixed;left:0;top:0;opacity:0;';
|
|
document.body.appendChild(textarea);
|
|
textarea.focus();
|
|
textarea.select();
|
|
try {
|
|
const ok = document.execCommand('copy');
|
|
showToast(ok ? '已复制到剪贴板' : '复制失败,请手动选择文本复制', ok ? 'success' : 'error');
|
|
} catch (e) {
|
|
showToast('复制失败,请手动选择文本复制', 'error');
|
|
}
|
|
document.body.removeChild(textarea);
|
|
}
|
|
|
|
// ── Full AI Code Review ─────────────────────────────────────────
|
|
|
|
let currentSSE = null;
|
|
let diffVisible = true;
|
|
let allSuggestions = [];
|
|
let fileReviews = {};
|
|
let currentAnalysisId = null;
|
|
|
|
function startReview() {
|
|
const base = document.getElementById('select-base').value;
|
|
const head = document.getElementById('select-head').value;
|
|
const topN = document.getElementById('top-n').value;
|
|
|
|
if (!base || !head) {
|
|
showToast('请选择 Base 和 Head 分支', 'warning');
|
|
return;
|
|
}
|
|
|
|
// Abort any previous stream
|
|
if (currentSSE) {
|
|
currentSSE.abort();
|
|
}
|
|
|
|
const btn = document.getElementById('btn-review');
|
|
const progress = document.getElementById('review-progress-full');
|
|
const results = document.getElementById('review-results');
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = '审查中...';
|
|
progress.classList.remove('hidden');
|
|
results.classList.add('hidden');
|
|
|
|
// Reset state
|
|
allSuggestions = [];
|
|
fileReviews = {};
|
|
currentAnalysisId = null;
|
|
document.getElementById('summary-content').innerHTML = '';
|
|
document.getElementById('file-reviews').innerHTML = '';
|
|
document.getElementById('review-actions').classList.add('hidden');
|
|
|
|
// Progress tracking
|
|
let totalFiles = 0;
|
|
let completedFiles = 0;
|
|
let startedFiles = 0;
|
|
|
|
function updateProgress() {
|
|
const pct = totalFiles > 0 ? Math.round((completedFiles / totalFiles) * 80 + 10) : 50;
|
|
document.getElementById('review-progress-bar-full').style.width = pct + '%';
|
|
}
|
|
|
|
const concurrency = document.getElementById('concurrency').value;
|
|
|
|
currentSSE = SSE.post(`/api/repos/${repoId}/review`, {
|
|
base, head,
|
|
top_n: parseInt(topN) || 0,
|
|
concurrency: parseInt(concurrency) || 5,
|
|
}, {
|
|
start(data) {
|
|
totalFiles = data.total_files || 0;
|
|
document.getElementById('review-progress-text-full').textContent = `准备审查 ${totalFiles} 个文件...`;
|
|
document.getElementById('review-progress-bar-full').style.width = '5%';
|
|
},
|
|
file_start(data) {
|
|
startedFiles++;
|
|
const currentFile = data.file || '';
|
|
document.getElementById('review-progress-text-full').textContent = `正在审查: ${currentFile} (${startedFiles}/${totalFiles})`;
|
|
|
|
if (!fileReviews[currentFile]) {
|
|
fileReviews[currentFile] = { filename: currentFile, suggestions: [], summary: '' };
|
|
}
|
|
},
|
|
suggestion(data) {
|
|
allSuggestions.push(data);
|
|
|
|
if (data.file && fileReviews[data.file]) {
|
|
fileReviews[data.file].suggestions.push(data);
|
|
}
|
|
|
|
appendSuggestionCard(data);
|
|
},
|
|
file_end(data) {
|
|
completedFiles++;
|
|
updateProgress();
|
|
document.getElementById('review-progress-text-full').textContent = `已完成 ${completedFiles}/${totalFiles} 个文件`;
|
|
},
|
|
file_summary(data) {
|
|
if (data.file && fileReviews[data.file]) {
|
|
fileReviews[data.file].summary = data.content || '';
|
|
}
|
|
},
|
|
diff(data) {
|
|
if (data.diff) {
|
|
window._reviewDiff = data.diff;
|
|
}
|
|
},
|
|
summary(data) {
|
|
const el = document.getElementById('summary-content');
|
|
if (data.score !== undefined) {
|
|
el.innerHTML = renderSummaryHtml(data);
|
|
} else {
|
|
el.innerHTML = renderMarkdown(data.content || '');
|
|
}
|
|
document.getElementById('review-progress-bar-full').style.width = '90%';
|
|
document.getElementById('review-progress-text-full').textContent = '生成总结...';
|
|
},
|
|
done() {
|
|
document.getElementById('review-progress-bar-full').style.width = '100%';
|
|
document.getElementById('review-progress-text-full').textContent = '审查完成!';
|
|
|
|
setTimeout(() => {
|
|
progress.classList.add('hidden');
|
|
results.classList.remove('hidden');
|
|
document.getElementById('review-actions').classList.remove('hidden');
|
|
btn.disabled = false;
|
|
btn.textContent = '开始审查';
|
|
updatePrintHeader();
|
|
}, 500);
|
|
|
|
// Render inline suggestions if diff is available
|
|
if (window._reviewDiff) {
|
|
renderInlineReview(window._reviewDiff);
|
|
}
|
|
|
|
currentSSE = null;
|
|
},
|
|
analysis_saved(data) {
|
|
currentAnalysisId = data.analysis_id;
|
|
NoteEditor.init(repoId, currentAnalysisId);
|
|
NoteEditor.loadNotes();
|
|
// Refresh history list
|
|
loadHistoryList();
|
|
},
|
|
error(data) {
|
|
showToast('审查失败: ' + (data.message || '未知错误'), 'error');
|
|
progress.classList.add('hidden');
|
|
btn.disabled = false;
|
|
btn.textContent = '开始审查';
|
|
currentSSE = null;
|
|
},
|
|
});
|
|
}
|
|
|
|
function appendSuggestionCard(suggestion) {
|
|
const container = document.getElementById('file-reviews');
|
|
|
|
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>
|
|
<div class="p-4 space-y-2 suggestions-container"></div>`;
|
|
container.appendChild(fileSection);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
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, '-');
|
|
}
|
|
|
|
function renderInlineReview(diffString) {
|
|
const container = document.getElementById('review-diff-container');
|
|
if (!container) return;
|
|
|
|
DiffViewer.init('review-diff-container');
|
|
DiffViewer.renderDiff(diffString, {
|
|
onComplete() {
|
|
const fileSeverities = {};
|
|
const severityOrder = { critical: 3, warning: 2, info: 1 };
|
|
|
|
allSuggestions.forEach((s, i) => {
|
|
if (s.file) {
|
|
DiffViewer.insertFileSuggestion(s.file, s.severity, s.content, '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);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
function updatePrintHeader() {
|
|
const baseEl = document.getElementById('select-base');
|
|
const headEl = document.getElementById('select-head');
|
|
const baseText = baseEl?.options[baseEl.selectedIndex]?.text || baseEl?.value || '';
|
|
const headText = headEl?.options[headEl.selectedIndex]?.text || headEl?.value || '';
|
|
document.querySelectorAll('.print-base-ref').forEach(el => el.textContent = baseText);
|
|
document.querySelectorAll('.print-head-ref').forEach(el => el.textContent = headText);
|
|
document.querySelectorAll('.print-date').forEach(el => el.textContent = new Date().toLocaleString('zh-CN'));
|
|
}
|
|
|
|
// ── History ─────────────────────────────────────────────────────
|
|
|
|
async function loadHistoryList() {
|
|
try {
|
|
const resp = await fetch(`/api/repos/${repoId}/review/analyses`, { credentials: 'same-origin' });
|
|
if (!resp.ok) return;
|
|
const analyses = await resp.json();
|
|
|
|
const select = document.getElementById('history-select');
|
|
select.innerHTML = '<option value="">选择历史评审...</option>';
|
|
|
|
analyses.forEach(a => {
|
|
const opt = document.createElement('option');
|
|
opt.value = a.id;
|
|
const date = (a.created_at || '').substring(0, 16).replace('T', ' ');
|
|
opt.textContent = `${a.base_ref} → ${a.head_ref} (${date})`;
|
|
select.appendChild(opt);
|
|
});
|
|
|
|
if (analyses.length === 0) {
|
|
select.innerHTML = '<option value="">暂无历史评审</option>';
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load history:', e);
|
|
}
|
|
}
|
|
|
|
async function loadHistoryReview(analysisId) {
|
|
if (!analysisId) return;
|
|
|
|
try {
|
|
const resp = await fetch(`/api/repos/${repoId}/review/analyses/${analysisId}`, { credentials: 'same-origin' });
|
|
if (!resp.ok) throw new Error('加载评审数据失败');
|
|
const data = await resp.json();
|
|
|
|
const result = data.result;
|
|
if (!result || !result.file_reviews) {
|
|
showToast('该历史记录没有完整的评审数据', 'warning');
|
|
return;
|
|
}
|
|
|
|
currentAnalysisId = parseInt(analysisId);
|
|
|
|
// Populate suggestions from stored data
|
|
allSuggestions = [];
|
|
fileReviews = {};
|
|
|
|
result.file_reviews.forEach(fr => {
|
|
fileReviews[fr.file_name] = { filename: fr.file_name, suggestions: [], summary: '' };
|
|
(fr.suggestions || []).forEach(s => {
|
|
const content = s.description || '';
|
|
const fullContent = content +
|
|
(s.suggestion ? '\n\n**建议修改:** ' + s.suggestion : '') +
|
|
(s.code_example ? '\n\n```\n' + s.code_example + '\n```' : '');
|
|
const sug = { file: fr.file_name, severity: s.severity, content: fullContent };
|
|
allSuggestions.push(sug);
|
|
fileReviews[fr.file_name].suggestions.push(sug);
|
|
});
|
|
});
|
|
|
|
// Render summary
|
|
const summary = result.summary || {};
|
|
document.getElementById('summary-content').innerHTML = renderSummaryHtml(summary);
|
|
|
|
// Clear and rebuild card view
|
|
document.getElementById('file-reviews').innerHTML = '';
|
|
allSuggestions.forEach(s => appendSuggestionCard(s));
|
|
|
|
// Show results
|
|
document.getElementById('review-results').classList.remove('hidden');
|
|
document.getElementById('review-actions').classList.remove('hidden');
|
|
updatePrintHeader();
|
|
|
|
// Load diff and render inline
|
|
const base = data.base_ref;
|
|
const head = data.head_ref;
|
|
|
|
const diffResp = await fetch(`/api/repos/${repoId}/diff?base=${encodeURIComponent(base)}&head=${encodeURIComponent(head)}`, { credentials: 'same-origin' });
|
|
if (diffResp.ok) {
|
|
const diffData = await diffResp.json();
|
|
const diffString = diffData.diff || '';
|
|
if (diffString) {
|
|
renderInlineReview(diffString);
|
|
}
|
|
}
|
|
|
|
// Init note editor
|
|
NoteEditor.init(repoId, currentAnalysisId);
|
|
NoteEditor.loadNotes();
|
|
|
|
} catch (e) {
|
|
console.error('Load history error:', e);
|
|
showToast('加载历史评审失败: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|