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);