fix: 结构化渲染审查总结,替换原始JSON显示
- 后端发送结构化数据(score/overall/findings/recommendations)而非格式化文本 - 增加灵活JSON解析,兼容LLM返回recommendations为数组的情况 - 前端渲染带样式的审查总结:评分徽章(颜色分级)、分区展示 - 两个页面统一处理:repo页面的内联审查和review页面的完整审查
This commit is contained in:
+43
-11
@@ -287,9 +287,9 @@ func GenerateReview(db *sql.DB, repoPath, base, head string, topN, concurrency i
|
|||||||
"score": 7,
|
"score": 7,
|
||||||
"overall": "总体评价(2-3 句话)",
|
"overall": "总体评价(2-3 句话)",
|
||||||
"findings": "按严重程度排序的主要发现汇总",
|
"findings": "按严重程度排序的主要发现汇总",
|
||||||
"recommendations": "改进建议优先级列表"
|
"recommendations": "改进建议,用换行分隔多条建议"
|
||||||
}
|
}
|
||||||
请用中文回复。`, strings.Join(reviewParts, "\n\n"))
|
注意:所有字段必须是字符串类型,不要使用数组。请用中文回复。`, strings.Join(reviewParts, "\n\n"))
|
||||||
|
|
||||||
messages := []goopenai.ChatCompletionMessage{
|
messages := []goopenai.ChatCompletionMessage{
|
||||||
{Role: goopenai.ChatMessageRoleUser, Content: summaryPrompt},
|
{Role: goopenai.ChatMessageRoleUser, Content: summaryPrompt},
|
||||||
@@ -299,22 +299,54 @@ func GenerateReview(db *sql.DB, repoPath, base, head string, topN, concurrency i
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
jsonStr := extractJSON(summaryResponse)
|
jsonStr := extractJSON(summaryResponse)
|
||||||
if json.Unmarshal([]byte(jsonStr), &summary) == nil {
|
if json.Unmarshal([]byte(jsonStr), &summary) == nil {
|
||||||
summaryText := fmt.Sprintf("整体评分: %d/10\n\n%s", summary.Score, summary.Overall)
|
|
||||||
if summary.Findings != "" {
|
|
||||||
summaryText += "\n\n**主要发现:**\n" + summary.Findings
|
|
||||||
}
|
|
||||||
if summary.Recommendations != "" {
|
|
||||||
summaryText += "\n\n**改进建议:**\n" + summary.Recommendations
|
|
||||||
}
|
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback("summary", map[string]interface{}{"content": summaryText})
|
callback("summary", map[string]interface{}{
|
||||||
|
"score": summary.Score,
|
||||||
|
"overall": summary.Overall,
|
||||||
|
"findings": summary.Findings,
|
||||||
|
"recommendations": summary.Recommendations,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If parsing fails, send raw response as summary
|
// Try flexible parsing (handle recommendations as array)
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if json.Unmarshal([]byte(jsonStr), &raw) == nil {
|
||||||
|
var flexible struct {
|
||||||
|
Score int `json:"score"`
|
||||||
|
Overall string `json:"overall"`
|
||||||
|
Findings string `json:"findings"`
|
||||||
|
}
|
||||||
|
json.Unmarshal(raw["score"], &flexible.Score)
|
||||||
|
json.Unmarshal(raw["overall"], &flexible.Overall)
|
||||||
|
json.Unmarshal(raw["findings"], &flexible.Findings)
|
||||||
|
|
||||||
|
recommendations := ""
|
||||||
|
if rec, ok := raw["recommendations"]; ok {
|
||||||
|
var arr []string
|
||||||
|
if json.Unmarshal(rec, &arr) == nil {
|
||||||
|
recommendations = strings.Join(arr, "\n")
|
||||||
|
} else {
|
||||||
|
var s string
|
||||||
|
json.Unmarshal(rec, &s)
|
||||||
|
recommendations = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
callback("summary", map[string]interface{}{
|
||||||
|
"score": flexible.Score,
|
||||||
|
"overall": flexible.Overall,
|
||||||
|
"findings": flexible.Findings,
|
||||||
|
"recommendations": recommendations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If all parsing fails, send raw response as summary
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback("summary", map[string]interface{}{"content": summaryResponse})
|
callback("summary", map[string]interface{}{"content": summaryResponse})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback("error", map[string]interface{}{"message": "生成汇总失败: " + err.Error()})
|
callback("error", map[string]interface{}{"message": "生成汇总失败: " + err.Error()})
|
||||||
|
|||||||
@@ -427,6 +427,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Inline AI Review ────────────────────────────────────────
|
// ── Inline AI Review ────────────────────────────────────────
|
||||||
|
|
||||||
let inlineReviewSSE = null;
|
let inlineReviewSSE = null;
|
||||||
@@ -476,7 +504,12 @@
|
|||||||
inlineSuggestions.push(data);
|
inlineSuggestions.push(data);
|
||||||
},
|
},
|
||||||
summary(data) {
|
summary(data) {
|
||||||
document.getElementById('review-summary-content').innerHTML = renderInlineMarkdown(data.content || '');
|
const el = document.getElementById('review-summary-content');
|
||||||
|
if (data.score !== undefined) {
|
||||||
|
el.innerHTML = renderSummaryHtml(data);
|
||||||
|
} else {
|
||||||
|
el.innerHTML = renderInlineMarkdown(data.content || '');
|
||||||
|
}
|
||||||
document.getElementById('review-progress-bar').style.width = '90%';
|
document.getElementById('review-progress-bar').style.width = '90%';
|
||||||
},
|
},
|
||||||
done() {
|
done() {
|
||||||
|
|||||||
@@ -126,6 +126,34 @@
|
|||||||
let fileReviews = {};
|
let fileReviews = {};
|
||||||
let currentAnalysisId = null;
|
let currentAnalysisId = null;
|
||||||
|
|
||||||
|
// ── 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;
|
||||||
|
}
|
||||||
|
|
||||||
// Load refs on page load
|
// Load refs on page load
|
||||||
async function loadRefs() {
|
async function loadRefs() {
|
||||||
try {
|
try {
|
||||||
@@ -310,7 +338,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
summary(data) {
|
summary(data) {
|
||||||
document.getElementById('summary-content').innerHTML = renderMarkdown(data.content || '');
|
const el = document.getElementById('summary-content');
|
||||||
|
if (data.score !== undefined) {
|
||||||
|
el.innerHTML = renderSummaryHtml(data);
|
||||||
|
} else {
|
||||||
|
el.innerHTML = renderMarkdown(data.content || '');
|
||||||
|
}
|
||||||
document.getElementById('progress-bar').style.width = '90%';
|
document.getElementById('progress-bar').style.width = '90%';
|
||||||
document.getElementById('progress-text').textContent = '生成总结...';
|
document.getElementById('progress-text').textContent = '生成总结...';
|
||||||
},
|
},
|
||||||
@@ -497,13 +530,7 @@
|
|||||||
|
|
||||||
// Render summary
|
// Render summary
|
||||||
const summary = result.summary || {};
|
const summary = result.summary || {};
|
||||||
let summaryText = '';
|
document.getElementById('summary-content').innerHTML = renderSummaryHtml(summary);
|
||||||
if (summary.score) summaryText += `整体评分: ${summary.score}/10\n\n`;
|
|
||||||
if (summary.overall) summaryText += summary.overall;
|
|
||||||
if (summary.findings) summaryText += '\n\n**主要发现:**\n' + summary.findings;
|
|
||||||
if (summary.recommendations) summaryText += '\n\n**改进建议:**\n' + summary.recommendations;
|
|
||||||
|
|
||||||
document.getElementById('summary-content').innerHTML = renderMarkdown(summaryText);
|
|
||||||
|
|
||||||
// Clear and rebuild card view
|
// Clear and rebuild card view
|
||||||
document.getElementById('file-reviews').innerHTML = '';
|
document.getElementById('file-reviews').innerHTML = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user