- 新增 content 事件处理器,实时显示 LLM 流式原始输出 - 按钮增加 opacity-70 和 cursor-not-allowed 视觉反馈 - 详细说明区域改为左右两栏:左侧渲染效果、右侧 Markdown 源码 - 流式阶段显示带自动滚动的内容区域,完成后切换为结构化展示 - 精简布局:移除独立 Markdown 预览区域,合并到两栏中
This commit is contained in:
@@ -41,7 +41,21 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Streaming: show raw LLM output as it arrives -->
|
||||||
|
<div id="streaming" class="hidden">
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<div class="spinner inline-block"></div>
|
||||||
|
<p class="text-gray-500 text-sm">正在生成 PR 描述...</p>
|
||||||
|
<p id="streaming-status" class="text-xs text-gray-400"></p>
|
||||||
|
</div>
|
||||||
|
<div id="streaming-content" class="p-4 bg-gray-50 rounded-md max-h-96 overflow-y-auto">
|
||||||
|
<pre id="streaming-text" class="whitespace-pre-wrap text-sm text-gray-700 font-mono"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Structured output (two-column after done) -->
|
||||||
<div id="output-content">
|
<div id="output-content">
|
||||||
|
<div id="structured-fields">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-500">标题</label>
|
<label class="block text-sm font-medium text-gray-500">标题</label>
|
||||||
<p id="pr-title" class="text-lg font-semibold text-gray-900"></p>
|
<p id="pr-title" class="text-lg font-semibold text-gray-900"></p>
|
||||||
@@ -57,27 +71,26 @@
|
|||||||
<p id="pr-summary" class="text-gray-700"></p>
|
<p id="pr-summary" class="text-gray-700"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block text-sm font-medium text-gray-500">详细说明</label>
|
|
||||||
<div id="pr-details" class="text-gray-700 prose text-sm"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-500">影响</label>
|
<label class="block text-sm font-medium text-gray-500">影响</label>
|
||||||
<p id="pr-impact" class="text-gray-700"></p>
|
<p id="pr-impact" class="text-gray-700"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="markdown-preview" class="mt-6 p-4 bg-gray-50 rounded-md hidden">
|
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-2">Markdown 预览</label>
|
|
||||||
<pre id="pr-markdown" class="whitespace-pre-wrap text-sm text-gray-800 font-mono"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Streaming indicator -->
|
<!-- Two-column: rendered markdown (left) + raw markdown (right) -->
|
||||||
<div id="streaming" class="text-center py-8 hidden">
|
<div class="mb-4">
|
||||||
<div class="spinner inline-block"></div>
|
<label class="block text-sm font-medium text-gray-500 mb-2">详细说明</label>
|
||||||
<p class="mt-2 text-gray-500">正在生成 PR 描述...</p>
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<p id="streaming-status" class="mt-1 text-xs text-gray-400"></p>
|
<div>
|
||||||
|
<div class="text-xs text-gray-400 mb-1">渲染效果</div>
|
||||||
|
<div id="pr-details" class="text-gray-700 prose text-sm p-4 border rounded-md bg-white min-h-[120px]"></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-[120px] overflow-auto"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -202,9 +215,12 @@
|
|||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '生成中...';
|
btn.textContent = '生成中...';
|
||||||
|
btn.classList.add('opacity-70', 'cursor-not-allowed');
|
||||||
output.classList.remove('hidden');
|
output.classList.remove('hidden');
|
||||||
outputContent.classList.add('hidden');
|
outputContent.classList.add('hidden');
|
||||||
streaming.classList.remove('hidden');
|
streaming.classList.remove('hidden');
|
||||||
|
document.getElementById('streaming-text').textContent = '';
|
||||||
|
document.getElementById('streaming-status').textContent = '';
|
||||||
|
|
||||||
// Clear previous results
|
// Clear previous results
|
||||||
document.getElementById('pr-title').textContent = '';
|
document.getElementById('pr-title').textContent = '';
|
||||||
@@ -213,54 +229,64 @@
|
|||||||
document.getElementById('pr-details').innerHTML = '';
|
document.getElementById('pr-details').innerHTML = '';
|
||||||
document.getElementById('pr-impact').textContent = '';
|
document.getElementById('pr-impact').textContent = '';
|
||||||
document.getElementById('pr-markdown').textContent = '';
|
document.getElementById('pr-markdown').textContent = '';
|
||||||
document.getElementById('markdown-preview').classList.add('hidden');
|
|
||||||
markdownContent = '';
|
markdownContent = '';
|
||||||
|
|
||||||
// Accumulate streaming content
|
// Accumulate streaming content
|
||||||
const fields = { title: '', type: '', summary: '', details: '', impact: '' };
|
const fields = { title: '', type: '', summary: '', details: '', impact: '' };
|
||||||
|
let rawStreamText = '';
|
||||||
|
|
||||||
SSE.post(`/api/repos/${repoId}/generate`, {
|
SSE.post(`/api/repos/${repoId}/generate`, {
|
||||||
base: baseRef,
|
base: baseRef,
|
||||||
head: headRef,
|
head: headRef,
|
||||||
}, {
|
}, {
|
||||||
|
// Raw streaming content from LLM — show in real-time
|
||||||
|
content(data) {
|
||||||
|
const chunk = data.content || '';
|
||||||
|
rawStreamText += chunk;
|
||||||
|
document.getElementById('streaming-text').textContent = rawStreamText;
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
const container = document.getElementById('streaming-content');
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
},
|
||||||
title(data) {
|
title(data) {
|
||||||
fields.title += data.content || '';
|
fields.title += data.content || '';
|
||||||
document.getElementById('pr-title').textContent = fields.title;
|
document.getElementById('pr-title').textContent = fields.title;
|
||||||
document.getElementById('streaming-status').textContent = '生成标题...';
|
document.getElementById('streaming-status').textContent = '解析标题...';
|
||||||
},
|
},
|
||||||
type(data) {
|
type(data) {
|
||||||
fields.type += data.content || '';
|
fields.type += data.content || '';
|
||||||
document.getElementById('pr-type').textContent = fields.type;
|
document.getElementById('pr-type').textContent = fields.type;
|
||||||
document.getElementById('streaming-status').textContent = '生成类型...';
|
document.getElementById('streaming-status').textContent = '解析类型...';
|
||||||
},
|
},
|
||||||
summary(data) {
|
summary(data) {
|
||||||
fields.summary += data.content || '';
|
fields.summary += data.content || '';
|
||||||
document.getElementById('pr-summary').textContent = fields.summary;
|
document.getElementById('pr-summary').textContent = fields.summary;
|
||||||
document.getElementById('streaming-status').textContent = '生成摘要...';
|
document.getElementById('streaming-status').textContent = '解析摘要...';
|
||||||
},
|
},
|
||||||
detail(data) {
|
detail(data) {
|
||||||
fields.details += data.content || '';
|
fields.details += data.content || '';
|
||||||
document.getElementById('pr-details').innerHTML = renderMarkdown(fields.details);
|
document.getElementById('pr-details').innerHTML = renderMarkdown(fields.details);
|
||||||
document.getElementById('streaming-status').textContent = '生成详细说明...';
|
document.getElementById('streaming-status').textContent = '解析详细说明...';
|
||||||
},
|
},
|
||||||
impact(data) {
|
impact(data) {
|
||||||
fields.impact += data.content || '';
|
fields.impact += data.content || '';
|
||||||
document.getElementById('pr-impact').textContent = fields.impact;
|
document.getElementById('pr-impact').textContent = fields.impact;
|
||||||
document.getElementById('streaming-status').textContent = '生成影响范围...';
|
document.getElementById('streaming-status').textContent = '解析影响范围...';
|
||||||
},
|
},
|
||||||
markdown(data) {
|
markdown(data) {
|
||||||
markdownContent += data.content || '';
|
markdownContent += data.content || '';
|
||||||
document.getElementById('pr-markdown').textContent = markdownContent;
|
document.getElementById('pr-markdown').textContent = markdownContent;
|
||||||
},
|
},
|
||||||
done() {
|
done() {
|
||||||
// Assemble full markdown from structured fields for copy
|
// Assemble full markdown from structured fields
|
||||||
markdownContent = buildMarkdown(fields);
|
markdownContent = buildMarkdown(fields);
|
||||||
document.getElementById('pr-markdown').textContent = markdownContent;
|
document.getElementById('pr-markdown').textContent = markdownContent;
|
||||||
document.getElementById('markdown-preview').classList.remove('hidden');
|
// Switch from streaming to structured display
|
||||||
streaming.classList.add('hidden');
|
streaming.classList.add('hidden');
|
||||||
outputContent.classList.remove('hidden');
|
outputContent.classList.remove('hidden');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '生成 PR 描述';
|
btn.textContent = '生成 PR 描述';
|
||||||
|
btn.classList.remove('opacity-70', 'cursor-not-allowed');
|
||||||
},
|
},
|
||||||
error(data) {
|
error(data) {
|
||||||
showToast('生成失败: ' + (data.message || '未知错误'), 'error');
|
showToast('生成失败: ' + (data.message || '未知错误'), 'error');
|
||||||
@@ -268,6 +294,7 @@
|
|||||||
output.classList.add('hidden');
|
output.classList.add('hidden');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '生成 PR 描述';
|
btn.textContent = '生成 PR 描述';
|
||||||
|
btn.classList.remove('opacity-70', 'cursor-not-allowed');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user