Files
hustoj/web/template/syzoj/reinfo.php
klarkxy 90010113dd feat(ui): 判题结果页 AI 点评区支持折叠
- 为 LLM 点评标题增加点击折叠/展开交互,减少页面占用空间
- 新增标题悬停反馈与图标旋转动画样式
- 移除快捷操作区中的"下载测试数据"按钮入口
2026-07-08 21:07:56 +08:00

578 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php $show_title=$id." - $MSG_ERROR_INFO - $OJ_NAME"; ?>
<?php include("template/$OJ_TEMPLATE/header.php");?>
<script src="include/vditor-adapter.js"></script>
<script src="template/<?php echo $OJ_TEMPLATE?>/js/textFit.min.js"></script>
<style>
.single-subtask {
box-shadow: none !important;
}
.single-subtask > .title {
display: none;
}
.single-subtask > .content {
padding: 0 !important;
}
.accordion > .content > .accordion {
margin-top: 0;
margin-bottom: 0;
}
.accordion > .content > .accordion > .content {
margin-top: 0;
margin-bottom: 14px;
}
.accordion > .content > .accordion > .content > :last-child {
margin-bottom: -10px !important;
}
/* === reinfo 友好化样式 === */
#result-banner {
margin-bottom: 18px;
border-left: 5px solid;
}
#result-banner.green { border-left-color: #21ba45; }
#result-banner.red { border-left-color: #db2828; }
#result-banner.yellow{ border-left-color: #fbbd08; }
#result-banner.blue { border-left-color: #2185d0; }
#result-banner.grey { border-left-color: #767676; }
#result-banner h2.ui.header {
margin: 0;
}
#result-banner .verdict-stats {
margin-top: 6px;
font-size: 0.95em;
color: rgba(0,0,0,0.6);
}
#result-banner .verdict-stats .stat {
margin-right: 16px;
}
#result-banner .verdict-stats i.icon {
margin-right: 4px;
}
#verdict-tip {
margin-bottom: 18px;
font-size: 1.05em;
padding: 8px 12px;
background: rgba(33,150,243,0.08);
border-radius: 6px;
border-left: 3px solid #2185d0;
}
#raw-info-accordion {
margin-bottom: 18px;
}
#raw-info-accordion > .content {
padding: 8px 14px;
}
#raw-info-accordion pre#errtxt {
margin: 0;
padding: 12px;
background: #f7f7f9;
border-radius: 4px;
font-family: 'Fira Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.9em;
white-space: pre-wrap;
word-break: break-all;
}
#raw-info-accordion .title .dedup-label {
margin-left: 8px;
}
#raw-info-accordion .empty-notice {
color: #999;
font-style: italic;
}
.quick-actions {
text-align: center;
margin-top: 24px;
margin-bottom: 12px;
}
.quick-actions .ui.buttons {
box-shadow: none;
}
/* AI 点评区 */
#llm-section {
margin-top: 20px;
margin-bottom: 18px;
}
#llm-section .ui.header {
margin-top: 0;
}
#llm-section .llm-section-header {
transition: background-color 0.15s;
padding: 4px 8px;
margin-left: -8px;
margin-right: -8px;
border-radius: 4px;
}
#llm-section .llm-section-header:hover {
background-color: rgba(33,150,243,0.06);
}
#llm-section .llm-toggle-icon {
transition: transform 0.2s;
color: #888;
}
#llm-review-result {
word-break: break-word;
}
/* 错误解释列表(由 JS explain() 填充) - 已弃用,由 AI 点评替代 */
/* 自动刷新状态 */
i.icon.spinning {
animation: spin 1.2s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
#auto-refresh-status {
color: #2185d0;
font-weight: 600;
}
</style>
<div class="padding">
<?php
// 兼容老变量:$row 是 solution 行reinfo.php 已查询过)
$verdict_name = isset($judge_result[$row['result']]) ? $judge_result[$row['result']] : (isset($MSG_UNKNOWN) ? $MSG_UNKNOWN : 'Unknown');
$verdict_lang = isset($language_name[$lang]) ? $language_name[$lang] : '?';
$verdict_memory = isset($row['memory']) ? intval($row['memory']) : 0;
$verdict_time = isset($row['time']) ? intval($row['time']) : 0;
$verdict_mark = isset($mark) ? $mark : (isset($row['pass_rate']) ? round($row['pass_rate']*100, 1) : 0);
?>
<!-- 1. 顶部判题结果横幅 -->
<div class="ui segment <?php echo isset($verdict_color_class) ? $verdict_color_class : 'blue'; ?>" id="result-banner">
<h2 class="ui header">
<i class="<?php echo isset($result_icon) ? $result_icon : 'info circle' ?> icon" style="color: inherit;"></i>
<div class="content">
<?php echo htmlspecialchars($verdict_name, ENT_QUOTES, 'UTF-8') ?>
<div class="verdict-stats">
<span class="stat"><i class="chart bar icon"></i><?php echo $MSG_MARK ?>: <?php echo $verdict_mark ?>%</span>
<span class="stat"><i class="microchip icon"></i><?php echo $verdict_memory ?> KB</span>
<span class="stat"><i class="clock icon"></i><?php echo $verdict_time ?> ms</span>
<span class="stat"><i class="code icon"></i><?php echo htmlspecialchars($verdict_lang, ENT_QUOTES, 'UTF-8') ?></span>
<span class="stat" id="auto-refresh-status" style="display:none;">
<i class="refresh icon spinning"></i> <span id="auto-refresh-text"></span>
</span>
</div>
</div>
</h2>
</div>
<!-- 2. 判题结果标题提示(仅一句话,具体修复建议交给 AI -->
<div class="ui info message" id="verdict-tip">
<i class="info circle icon"></i>
<?php echo isset($verdict_tip) ? htmlspecialchars($verdict_tip, ENT_QUOTES, 'UTF-8') : ''; ?>
</div>
<!-- 3. AI 点评区(标题可点击折叠/展开) -->
<?php if (isset($OJ_LLM_ENABLED) && $OJ_LLM_ENABLED) { ?>
<div class="ui raised segment" id="llm-section">
<h3 class="ui header llm-section-header" id="llm-toggle" style="cursor: pointer; user-select: none;">
<i class="magic icon"></i>
<div class="content">
AI 智能点评
<div class="sub header"><?php echo isset($MSG_AI_REVIEW_DESC) ? $MSG_AI_REVIEW_DESC : '基于你的代码和判题结果分析'; ?></div>
</div>
<i class="dropdown icon llm-toggle-icon" style="margin-left: auto; font-size: 1em;"></i>
</h3>
<div id="llm-section-body">
<button class="ui labeled icon primary button" id="llm-review-btn" onclick="fetchLLMReview()">
<i class="magic icon"></i> 获取 AI 点评
</button>
<div id="llm-review-loading" class="ui active inline text loader" style="display:none; margin-left: 1em;">
AI 正在分析你的代码,请稍候...
</div>
<div id="llm-review-result" style="display:none; margin-top: 15px;">
<div class="ui existing segment" id="llm-review-content"></div>
</div>
<div id="llm-review-error" style="display:none; margin-top: 15px;">
<div class="ui negative message">
<p id="llm-review-error-msg"></p>
</div>
</div>
</div>
</div>
<script>
// 折叠/展开 AI 点评区
(function(){
var header = document.getElementById('llm-toggle');
var body = document.getElementById('llm-section-body');
var icon = header ? header.querySelector('.llm-toggle-icon') : null;
if (!header || !body) return;
header.addEventListener('click', function(e){
// 如果用户点击的是按钮本身,不要触发展开(按钮事件自己处理)
if (e.target.closest('button')) return;
var collapsed = body.style.display === 'none';
body.style.display = collapsed ? '' : 'none';
if (icon) {
icon.className = collapsed
? 'dropdown icon llm-toggle-icon'
: 'dropdown icon llm-toggle-icon active';
// active class 让 Semantic UI 的箭头自动旋转 90°但我们手写也行
icon.style.transform = collapsed ? 'rotate(0deg)' : 'rotate(-90deg)';
}
});
})();
function fetchLLMReview(){
var btn = $('#llm-review-btn');
btn.prop('disabled', true).addClass('loading');
$('#llm-review-loading').show();
$('#llm-review-error').hide();
$('#llm-review-result').hide();
var fullText = "";
var container = document.getElementById('llm-review-content');
var resultDiv = document.getElementById('llm-review-result');
var reader = null;
var decoder = new TextDecoder();
var sseBuffer = "";
function processSSE(text) {
sseBuffer += text;
var lines = sseBuffer.split('\n');
sseBuffer = lines.pop();
var currentEvent = "";
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.indexOf('event: ') === 0) {
currentEvent = line.substring(7);
} else if (line.indexOf('data: ') === 0) {
var dataStr = line.substring(6);
try {
var data = JSON.parse(dataStr);
if (currentEvent === 'chunk') {
fullText += data.text;
container.textContent = fullText;
resultDiv.style.display = 'block';
} else if (currentEvent === 'cached') {
fullText = data.text;
container.textContent = fullText;
resultDiv.style.display = 'block';
} else if (currentEvent === 'done') {
finishReview();
} else if (currentEvent === 'error') {
showError(data.message || '未知错误');
}
} catch(e) {}
currentEvent = "";
}
}
}
function finishReview() {
btn.removeClass('loading').hide();
document.getElementById('llm-review-loading').style.display = 'none';
resultDiv.style.display = 'block';
if (fullText && typeof HustOJVditor !== 'undefined' && HustOJVditor.renderMarkdownBlocks) {
HustOJVditor.renderMarkdownBlocks('#llm-review-content', { useTextContent: true });
}
}
function showError(msg) {
btn.removeClass('loading').prop('disabled', false);
document.getElementById('llm-review-loading').style.display = 'none';
document.getElementById('llm-review-error-msg').textContent = msg;
document.getElementById('llm-review-error').style.display = 'block';
}
if (typeof fetch === 'undefined') {
showError('浏览器不支持,请升级浏览器');
return;
}
fetch('llm-review.php?sid=<?php echo intval($id); ?>')
.then(function(response) {
if (!response.ok) {
return response.text().then(function(text) {
try {
var d = JSON.parse(text);
showError(d.error || '请求失败 (HTTP ' + response.status + ')');
} catch(e) {
showError('请求失败 (HTTP ' + response.status + ')');
}
});
}
reader = response.body.getReader();
function read() {
reader.read().then(function(result) {
if (result.done) {
if (fullText) finishReview();
return;
}
var text = decoder.decode(result.value, { stream: true });
processSSE(text);
read();
}).catch(function(err) {
if (fullText) {
finishReview();
} else {
showError('读取响应失败: ' + err.message);
}
});
}
read();
})
.catch(function(err) {
showError('网络请求失败: ' + err.message);
});
}
</script>
<?php } ?>
<!-- 4. 原始错误折叠(默认收起) -->
<div class="ui styled accordion" id="raw-info-accordion">
<div class="title">
<i class="dropdown icon"></i>
<?php echo isset($MSG_RAW_ERROR) ? $MSG_RAW_ERROR : '查看原始错误信息'; ?>
<?php if (!empty($view_reinfo_dedup_count)): ?>
<span class="ui tiny teal label dedup-label">
<?php echo sprintf(isset($MSG_DEDUP_HINT) ? $MSG_DEDUP_HINT : '已折叠 %d 个相同测试点', intval($view_reinfo_dedup_count)); ?>
</span>
<?php endif; ?>
<?php if (trim($view_reinfo) === ''): ?>
<span class="dedup-label empty-notice">(无)</span>
<?php endif; ?>
</div>
<div class="content">
<?php if (trim($view_reinfo) === ''): ?>
<p class="empty-notice">当前判题结果没有原始错误信息(如 TLE/MLE 通常不会写 runtimeinfo。</p>
<?php else: ?>
<pre id="errtxt"><?php
// 普通用户:折叠后;管理员/源浏览者:原文
if (isset($is_admin_session) && $is_admin_session) {
echo htmlspecialchars($view_reinfo_raw, ENT_QUOTES, 'UTF-8');
} elseif (isset($is_source_browser) && $is_source_browser) {
echo htmlspecialchars($view_reinfo_raw, ENT_QUOTES, 'UTF-8');
} else {
echo htmlspecialchars($view_reinfo, ENT_QUOTES, 'UTF-8');
}
?></pre>
<?php endif; ?>
</div>
</div>
<!-- 5. 快捷操作区 -->
<div class="quick-actions">
<div class="ui horizontal divider">
<?php echo isset($MSG_QUICK_ACTIONS) ? $MSG_QUICK_ACTIONS : '快捷操作'; ?>
</div>
<div class="ui buttons">
<a class="ui blue button" href="submitpage.php?id=<?php echo intval($row['problem_id']) ?>">
<i class="edit icon"></i> <?php echo isset($MSG_RESUBMIT) ? $MSG_RESUBMIT : '重新提交'; ?>
</a>
<a class="ui button" href="showsource.php?id=<?php echo intval($id) ?>">
<i class="code icon"></i> <?php echo isset($MSG_VIEW_SOURCE) ? $MSG_VIEW_SOURCE : '查看我的代码'; ?>
</a>
<a class="ui button" href="problem.php?id=<?php echo intval($row['problem_id']) ?>">
<i class="info circle icon"></i> <?php echo isset($MSG_VIEW_PROBLEM) ? $MSG_VIEW_PROBLEM : '查看题目'; ?>
</a>
</div>
</div>
</div>
<?php if(isset($OJ_MARKDOWN) && $OJ_MARKDOWN){ ?>
<script>
$(document).ready(function(){
// 初始化折叠面板
$('#raw-info-accordion').accordion();
// 管理员/superuser 默认展开原始错误
<?php if (isset($is_admin_session) && $is_admin_session) { ?>
$('#raw-info-accordion').accordion('open', 0);
<?php } elseif (isset($is_source_browser) && $is_source_browser) { ?>
$('#raw-info-accordion').accordion('open', 0);
<?php } ?>
HustOJVditor.renderMarkdownBlocks('#errtxt', {
getSource: function (element) {
var pre = element.querySelector ? element.querySelector('pre') : null;
return pre ? pre.textContent : element.textContent || '';
}
}).then(function () {
for(let i=1;i<10;i++){
$(".language-input"+i).parent().before("<div><?php echo $MSG_Input?>"+i+":</div>");
$(".language-output"+i).parent().before("<div><?php echo $MSG_Output?>"+i+":</div>");
}
$("#errtxt table").addClass("ui mini-table cell striped");
$("#errtxt table tr:odd td").css({
"border": "1px solid grey",
"text-align": "center",
"width": "200px",
"background-color": "#8521d022",
"height": "30px"
});
$("#errtxt table tr:even td").css({
"border": "1px solid grey",
"text-align": "center",
"width": "200px",
"background-color": "#2185d022",
"height": "30px"
});
$("#errtxt table th").css({
"border": "1px solid grey",
"width": "200px",
"height": "30px",
"background-color": "#2185d088",
"text-align": "center"
});
<?php
if(isset($OJ_DOWNLOAD) && $OJ_DOWNLOAD){
if(isset($OJ_DL_1ST_WA_ONLY) && $OJ_DL_1ST_WA_ONLY){
?>
let down=$($("#errtxt").find("h2")[0]);
let filename=down.text();
down.html("<a href='download.php?sid=<?php echo $id?>&name=" + filename+ "'>" + filename+ "</a>");
<?php
}else{
?>
$("#errtxt").find("h2").each(function(){
let down=$(this);
let filename=down.text();
console.log(filename);
down.html("<a href='download.php?sid=<?php echo $id?>&name=" + filename+ "'>" + filename+ "</a>");
});
<?php
}
}
?>
$("th").each(function(){
let html=$(this).html();
html=html.replace("Expected","<?php echo $MSG_EXPECTED ?>");
html=html.replace("Yours","<?php echo $MSG_YOURS ?>");
html=html.replace("filename","<?php echo $MSG_FILENAME ?>");
html=html.replace("size","<?php echo $MSG_SIZE ?>");
html=html.replace("result","<?php echo $MSG_RESULT ?>");
html=html.replace("memory","<?php echo $MSG_MEMORY?>");
html=html.replace("time","<?php echo $MSG_TIME ?>");
$(this).html(html);
});
}).catch(function (error) {
console.error('Failed to render reinfo markdown.', error);
});
});
</script>
<?php } else { ?>
<script>
// OJ_MARKDOWN 未开启时也要初始化折叠
$(document).ready(function(){
$('#raw-info-accordion').accordion();
<?php if (isset($is_admin_session) && $is_admin_session) { ?>
$('#raw-info-accordion').accordion('open', 0);
<?php } elseif (isset($is_source_browser) && $is_source_browser) { ?>
$('#raw-info-accordion').accordion('open', 0);
<?php } ?>
});
</script>
<?php } ?>
<!-- 自动刷新判题中result < 4 或 > 14时定时拉取最新状态 -->
<?php
$result_code_js = isset($row['result']) ? intval($row['result']) : 4;
$sid_js = isset($id) ? intval($id) : 0;
// 0=Pending, 1=Pending Rejudging, 2=Compiling, 3=Running/Judging, 14=Manual, 15=Submitting, 16/17=Remote
$is_pending = ($result_code_js < 4) || ($result_code_js > 14);
?>
<?php if ($sid_js > 0 && $is_pending): ?>
<script>
(function(){
var sid = <?php echo $sid_js ?>;
var currentResult = <?php echo $result_code_js ?>;
var interval = 1500; // 初始 1.5s
var maxInterval = 8000; // 最长 8s
var countdown = 0;
var stopped = false;
function showStatus(text){
var el = document.getElementById('auto-refresh-status');
var t = document.getElementById('auto-refresh-text');
if (el && t) {
el.style.display = '';
t.textContent = text;
}
}
function hideStatus(){
var el = document.getElementById('auto-refresh-status');
if (el) el.style.display = 'none';
}
function tick(){
if (stopped) return;
countdown--;
if (countdown > 0) {
showStatus('将在 ' + countdown + ' 秒后自动刷新');
setTimeout(tick, 1000);
return;
}
fetch('status-ajax.php?solution_id=' + sid + '&t=json', {credentials: 'same-origin'})
.then(function(r){ return r.json(); })
.then(function(data){
if (!data || typeof data.result === 'undefined') {
// 解析失败,下次重试
interval = Math.min(interval * 1.5, maxInterval);
countdown = Math.ceil(interval / 1000);
tick();
return;
}
var newResult = parseInt(data.result);
if (newResult >= 4 && newResult <= 14) {
// 判题完成,刷新页面拿到完整错误信息
showStatus('判题完成,正在刷新...');
setTimeout(function(){ location.reload(); }, 400);
stopped = true;
return;
}
// 仍在评判中
interval = Math.min(interval * 1.3, maxInterval);
countdown = Math.ceil(interval / 1000);
showStatus('判题中,将在 ' + countdown + ' 秒后再次查询');
setTimeout(tick, 1000);
})
.catch(function(err){
// 网络错误,重试
console.warn('auto refresh failed:', err);
interval = Math.min(interval * 1.5, maxInterval);
countdown = Math.ceil(interval / 1000);
showStatus('查询失败,' + countdown + ' 秒后重试');
setTimeout(tick, 1000);
});
}
// 用户切到后台标签时停止轮询,避免无意义请求
document.addEventListener('visibilitychange', function(){
if (document.hidden) {
stopped = true;
hideStatus();
} else if (!stopped) {
countdown = 0;
tick();
}
});
// 启动
countdown = Math.ceil(interval / 1000);
showStatus('判题中,将在 ' + countdown + ' 秒后自动刷新');
setTimeout(tick, 1000);
})();
</script>
<?php endif; ?>
<?php include("template/$OJ_TEMPLATE/footer.php");?>