feat: Phase 3 — 前端交互功能完成
This commit is contained in:
@@ -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, '&')
|
||||
.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(/\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);
|
||||
|
||||
Reference in New Issue
Block a user