Compare commits

..

5 Commits

Author SHA1 Message Date
9b13c95e8a ♻️ refactor(reinfo): 重构 diff 解析逻辑并支持多格式
- 将原本分散在 `parse_diff_side_by_side` 与 `parse_pipe_diff_table` 的两套解析逻辑合并,新增统一的 `parse_one_diff_line` 工具函数
- `parse_one_diff_line` 同时识别 `|a|b`、`a | b`、`a<b` / `a>b` 等多种 diff 输出格式,便于后续扩展
- 简化 `parse_diff_blocks` 中 full mode / simple mode 的内联解析流程,消除代码重复
- 优化 `raw_error` 换行符归一化处理,兼容 `\r\n` 与单独 `\r` 结尾的情况
2026-07-08 21:24:08 +08:00
0e5cac3d4e ♻️ refactor(reinfo): 调整 diff 解析函数定义位置
- 将 parse_diff_blocks、parse_pipe_diff_table、parse_diff_side_by_side 移至 dedup_runtimeinfo 之后
- 与相邻辅助函数保持一致的位置,便于阅读和维护
- 精简部分冗余注释
2026-07-08 21:15:49 +08:00
c22c788cfa feat(ui): 判题结果页新增输出对比视图
- 新增 parse_diff_blocks 系列函数,将 runtimeinfo 解析为结构化的测试点对比块,支持 simple 表格和 diff -y 两种格式
- 在判题结果页插入"输出对比"区块,逐测试点并列展示期望输出与用户输出,差异行高亮
- 补充 diff-section 相关样式,移动端切换为单列布局
2026-07-08 21:13:13 +08:00
90010113dd feat(ui): 判题结果页 AI 点评区支持折叠
- 为 LLM 点评标题增加点击折叠/展开交互,减少页面占用空间
- 新增标题悬停反馈与图标旋转动画样式
- 移除快捷操作区中的"下载测试数据"按钮入口
2026-07-08 21:07:56 +08:00
2b699a185b ♻️ refactor(reinfo): 简化判错提示并新增自动刷新
- 移除 PHP 中冗余的 verdict_hints 数组及其填充逻辑
- 移除模板中 friendly-explain 区与 errexp-list 错误模式匹配脚本(28 条规则)
- 标题提示简化为单行 verdict-tip,详细的修复建议交由 AI 点评
- 调整页面顺序:AI 点评区提前,原始错误折叠下移
- 新增判题中自动刷新状态指示:轮询 status-ajax.php,完成后自动 reload,并加入后台标签暂停与退避策略
2026-07-08 21:04:50 +08:00
2 changed files with 494 additions and 248 deletions

View File

@@ -102,6 +102,145 @@ function dedup_runtimeinfo($text, &$dedup_count) {
return implode("\n", $out);
}
/**
* 把 runtimeinfo.error 解析为结构化的 diff blocks
* 返回 [['name' => 'test1.out', 'expected' => [...], 'yours' => [...], 'full_mode' => bool], ...]
* 解析失败返回空数组
*/
function parse_diff_blocks($text) {
$blocks = array();
if (empty($text)) return $blocks;
// 先按 "\n\n" 切分simple mode 每个测试点之间有空行)
$chunks = preg_split('/\n\s*\n/', $text);
foreach ($chunks as $chunk) {
$chunk = trim($chunk);
if ($chunk === '') continue;
// 格式 1: 整段就是一个 =====[name]===== 块(包含表头+数据+结尾 ====
// 形如 "========[test1.out]========\nExpected | Yours\nFail | Fall\n=============================="
if (preg_match('/^=+\[([^\]]+)\]\=+(.+?)\n=+\s*$/s', $chunk, $m)) {
$name = trim($m[1]);
$body = $m[2];
// body 形如 "\nExpected | Yours\nFail | Fall"
// 去掉首尾的换行符
$body = trim($body, "\n\r");
$lines_in_body = explode("\n", $body);
$expected = array();
$yours = array();
$found_header = false;
foreach ($lines_in_body as $bl) {
$bl = rtrim($bl);
if ($bl === '') continue;
// 跳过表头
if (!$found_header && preg_match('/^Expected.*\|.*Yours/i', $bl)) {
$found_header = true;
continue;
}
if (!$found_header && (preg_match('/^\|?--\s*\|/', $bl) || trim($bl) === '--')) {
continue;
}
$parsed = parse_one_diff_line($bl);
if ($parsed !== null) {
$expected[] = $parsed[0];
$yours[] = $parsed[1];
$found_header = true;
} else {
$expected[] = $bl;
$yours[] = '';
$found_header = true;
}
}
if (!empty($expected)) {
$blocks[] = array(
'name' => $name,
'expected' => $expected,
'yours' => $yours,
'full_mode' => true,
);
}
continue;
}
// 格式 2: simple mode "test1.out\n--\n|Expected|Yours\n|--|--\n|row1|row2"
if (preg_match('/^([^\n=|]+)\n--\n([\s\S]+)$/', $chunk, $m)) {
$name = trim($m[1]);
$body = $m[2];
$lines_in_body = explode("\n", $body);
$expected = array();
$yours = array();
$found_header = false;
foreach ($lines_in_body as $bl) {
$bl = rtrim($bl);
if ($bl === '') continue;
if (!$found_header && preg_match('/Expected.*\|.*Yours/i', $bl)) {
$found_header = true;
continue;
}
if (!$found_header && (preg_match('/^\|?--\s*\|/', $bl) || trim($bl) === '--')) {
continue;
}
$parsed = parse_one_diff_line($bl);
if ($parsed !== null) {
$expected[] = $parsed[0];
$yours[] = $parsed[1];
$found_header = true;
} else {
$expected[] = $bl;
$yours[] = '';
$found_header = true;
}
}
if (!empty($expected)) {
$blocks[] = array(
'name' => $name,
'expected' => $expected,
'yours' => $yours,
'full_mode' => false,
);
}
}
}
return $blocks;
}
/**
* 解析单行 "expected | yours" 格式
* 支持 "|a|b"、"a | b"、"a|b" 等
* 返回 [left, right] 或 null无法解析
*/
function parse_one_diff_line($line) {
$line = rtrim($line);
if ($line === '') return null;
// 格式 A: "|a|b" (首尾带 |)
if ($line[0] === '|') {
$rest = substr($line, 1);
$pos = strpos($rest, '|');
if ($pos !== false) {
return array(substr($rest, 0, $pos), substr($rest, $pos + 1));
}
}
// 格式 B: "a | b" 或 "a | b"(带空格的 markdown 风格)
// 注意:可能行内有空格,所以用 " | " 或 " | " 等宽松匹配
if (preg_match('/^(.*?)\s+\|\s+(.*)$/', $line, $m)) {
// 确保左右两边都非空
$left = trim($m[1]);
$right = trim($m[2]);
if ($left !== '' || $right !== '') {
return array($left, $right);
}
}
// 格式 C: "a<b" 或 "a>b"diff -y 风格)
if (preg_match('/^(.*?)\s+([<>])\s+(.*)$/', $line, $m)) {
return array($m[1], $m[3]);
}
return null;
}
$view_reinfo = "";
$view_reinfo_raw = ""; // 原始(管理员/有权限者看)
$view_reinfo_summary = ""; // 折叠后(普通用户看)
@@ -109,7 +248,6 @@ $view_reinfo_dedup_count = 0; // 折叠掉的测试点数
$verdict_color_class = "blue"; // semantic ui color: green/red/orange/yellow/grey
$result_icon = "info circle";
$verdict_tip = "";
$verdict_hints = array();
if($ok){
@@ -121,7 +259,9 @@ if($ok){
$rt = pdo_query($sql,$id);
$raw_error = "";
if(isset($rt[0])){
$raw_error = str_replace("\n\r","\n",$rt[0]['error']);
$raw_error = $rt[0]['error'];
// 统一换行符:先替换 \r\n再替换单独的 \r
$raw_error = str_replace(array("\r\n", "\r"), "\n", $raw_error);
}
// === 阶段 1基于 solution.result 给"主解释"(永远非空)===
@@ -143,75 +283,39 @@ if($ok){
default: $verdict_color_class = "blue"; $result_icon = "info circle";
}
// 友好提示
// 友好提示:仅保留标题性文字,具体修复建议交给 AI 点评
switch ($result_code) {
case 4: // AC
$verdict_tip = "本次提交通过了所有测试点";
$verdict_hints[] = "注意时间/内存是否接近限制,考虑是否有更优的算法";
$verdict_hints[] = "可以继续挑战更高难度或查看其他人的标程";
break;
case 5: // PE
$verdict_tip = "输出与预期基本一致但格式不符Presentation Error";
$verdict_hints[] = "对照样例:每行末尾是否多/少换行";
$verdict_hints[] = "检查空格、大小写、标点符号";
$verdict_hints[] = "PE 通常是细微的格式问题,请逐字符对比";
break;
case 6: // WA
$verdict_tip = "输出与预期不符Wrong Answer";
$verdict_hints[] = "对照样例手动模拟程序执行过程";
$verdict_hints[] = "检查边界条件n=0、n=1、极值、空输入";
$verdict_hints[] = "注意 long long 溢出、int 范围";
break;
case 7: // TLE
$verdict_tip = $MSG_HINT_TLE;
$verdict_hints[] = "检查是否有死循环(循环变量未更新)";
$verdict_hints[] = "优化算法复杂度(如 O(n²) → O(n log n)";
$verdict_hints[] = "递归层数过深?改为迭代或加深栈";
break;
case 8: // MLE
$verdict_tip = $MSG_HINT_MLE;
$verdict_hints[] = "大数组开在函数内(栈)?改为全局或 new";
$verdict_hints[] = "Java: 减小内存使用或调整 JVM 参数";
$verdict_hints[] = "递归太深未释放,考虑显式清理";
break;
case 9: // OLE
$verdict_tip = $MSG_HINT_OLE;
$verdict_hints[] = "是否有死循环输出";
$verdict_hints[] = "调试用的 print/cout 是否忘记删除";
break;
case 10: // RE
$verdict_tip = "程序运行中发生错误Runtime Error";
$verdict_hints[] = "数组下标是否越界i<n 还是 i<=n";
$verdict_hints[] = "除以零、空指针、栈溢出";
$verdict_hints[] = "递归爆栈:转迭代或加尾递归";
break;
case 11: // CE
$verdict_tip = "编译错误Compile Error。请查看编译信息页。";
break;
case 13: // TR (test run)
$verdict_tip = "测试运行完成Test Run";
$verdict_hints[] = "你输入的测试数据已与程序输出对比";
break;
default:
$verdict_tip = "正在评判,请稍候刷新";
}
// === 阶段 2syscall vs 真正越界 的区分 ===
if ($result_code === 10) {
if (preg_match('/Forbidden system call.*CALLID:(\d+)/', $raw_error, $m)) {
$callid = $m[1];
$verdict_hints[] = sprintf("系统调用 ID=%d 被判题器禁止,常见于直接操作文件/网络/进程", $callid);
}
// Java / Python / Go 的细化提示
if (preg_match('/java\.lang\.(\w+Exception|\w+Error)/', $raw_error, $m2)) {
$verdict_hints[] = "Java 异常类型:" . $m2[1];
} elseif (preg_match('/Traceback \(most recent call last\)/', $raw_error)) {
$verdict_hints[] = "Python 抛出异常,下方 traceback 从下往上读就是异常源头";
} elseif (preg_match('/panic:.*?(index out of range|invalid memory address|nil pointer)/i', $raw_error)) {
$verdict_hints[] = "Go 运行时报错:通常是越界访问或 nil 指针";
}
}
// === 阶段 3原始错误处理syscall 提示 vs 占位符修复)===
$view_reinfo_raw = $raw_error;
if (trim($raw_error) === "") {
@@ -240,6 +344,9 @@ if($ok){
$view_reinfo = $view_reinfo_summary;
// 尝试解析为结构化的 diff 数据(用于 WA/PE/AC 时的对比展示)
$diff_blocks = parse_diff_blocks($raw_error);
// 管理员或 source_browser 永远看完整原文
$show_raw = $is_admin_session || $is_source_browser;
if ($show_raw) {

View File

@@ -51,25 +51,14 @@
margin-right: 4px;
}
#friendly-explain {
#verdict-tip {
margin-bottom: 18px;
}
#friendly-explain .verdict-tip {
font-size: 1.05em;
margin-bottom: 12px;
padding: 8px 12px;
background: rgba(33,150,243,0.08);
border-radius: 6px;
border-left: 3px solid #2185d0;
}
#friendly-explain .verdict-hints {
margin: 0;
padding-left: 22px;
}
#friendly-explain .verdict-hints li {
margin-bottom: 6px;
line-height: 1.5;
}
#raw-info-accordion {
margin-bottom: 18px;
@@ -112,25 +101,130 @@
#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() 填充) */
#errexp-list {
list-style: disc;
padding-left: 22px;
margin: 0;
/* 错误解释列表(由 JS explain() 填充) - 已弃用,由 AI 点评替代 */
/* 自动刷新状态 */
i.icon.spinning {
animation: spin 1.2s linear infinite;
}
#errexp-list li {
margin-bottom: 6px;
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
#auto-refresh-status {
color: #2185d0;
font-weight: 600;
}
/* Diff 对比样式 */
#diff-section {
margin-bottom: 18px;
}
.diff-card {
margin-bottom: 14px;
border-left: 4px solid #fbbd08;
padding: 12px 16px;
}
.diff-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
font-weight: 600;
color: #555;
}
.diff-name {
font-family: 'Fira Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.95em;
}
.diff-table {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 768px) {
.diff-table { grid-template-columns: 1fr; }
}
.diff-side {
background: #fafafa;
border-radius: 6px;
overflow: hidden;
border: 1px solid #e0e0e0;
}
.diff-side-header {
padding: 6px 10px;
font-size: 0.85em;
font-weight: 600;
color: #fff;
}
.diff-expected .diff-side-header {
background: #21ba45;
}
.diff-yours .diff-side-header {
background: #db2828;
}
.diff-content {
margin: 0 !important;
padding: 8px 0 !important;
background: #fff !important;
font-family: 'Fira Mono', 'Cascadia Code', Consolas, monospace;
font-size: 0.85em;
line-height: 1.5;
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
}
#errexp-list li code {
background: rgba(0,0,0,0.05);
padding: 1px 4px;
border-radius: 3px;
font-size: 0.9em;
.diff-line {
display: flex;
align-items: flex-start;
padding: 0;
white-space: pre;
}
.diff-line-mismatch {
background: rgba(255, 230, 100, 0.25);
}
.diff-expected .diff-line-mismatch {
background: rgba(33, 186, 69, 0.12);
}
.diff-yours .diff-line-mismatch {
background: rgba(219, 40, 40, 0.10);
}
.diff-lineno {
display: inline-block;
min-width: 38px;
padding: 0 8px;
text-align: right;
color: #999;
background: #f5f5f5;
border-right: 1px solid #e8e8e8;
user-select: none;
flex-shrink: 0;
}
.diff-text {
padding: 0 10px;
flex: 1;
word-break: break-all;
white-space: pre-wrap;
}
</style>
@@ -142,6 +236,8 @@
$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);
// 兜底:防止 PHP notice
$diff_blocks = isset($diff_blocks) && is_array($diff_blocks) ? $diff_blocks : array();
?>
<!-- 1. 顶部判题结果横幅 -->
@@ -155,91 +251,73 @@
<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. 友好解释区(永远非空 -->
<div class="ui message" id="friendly-explain">
<div class="header">
<i class="lightbulb outline icon"></i>
<?php echo isset($MSG_FRIENDLY_EXPLAIN) ? $MSG_FRIENDLY_EXPLAIN : '错误解释'; ?>
</div>
<div class="verdict-tip">
<i class="info circle icon"></i>
<?php echo isset($verdict_tip) ? htmlspecialchars($verdict_tip, ENT_QUOTES, 'UTF-8') : ''; ?>
</div>
<?php if (!empty($verdict_hints) && is_array($verdict_hints)): ?>
<ul class="verdict-hints">
<?php foreach ($verdict_hints as $h): ?>
<li><?php echo htmlspecialchars($h, ENT_QUOTES, 'UTF-8'); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<!-- JS explain() 也会把 pats[] 匹配到的具体错误模式追加在这里 -->
<ul id="errexp-list"></ul>
<!-- 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. 原始错误折叠(默认收起) -->
<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>
<!-- 4. AI 点评区(直接显示按钮,不再折叠 -->
<!-- 3. AI 点评区(标题可点击折叠/展开 -->
<?php if (isset($OJ_LLM_ENABLED) && $OJ_LLM_ENABLED) { ?>
<div class="ui raised segment" id="llm-section">
<h3 class="ui header">
<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>
<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 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');
@@ -347,6 +425,95 @@
</script>
<?php } ?>
<!-- 3.5. 对比详情WA/PE 时显示diff 视图) -->
<?php if (!empty($diff_blocks) && is_array($diff_blocks)): ?>
<div id="diff-section">
<h3 class="ui header" style="margin-top: 0;">
<i class="exchange icon"></i>
<div class="content">
输出对比
<div class="sub header">逐测试点对比期望输出和你的输出</div>
</div>
</h3>
<?php foreach ($diff_blocks as $idx => $block): ?>
<div class="ui segment diff-card">
<div class="diff-header">
<i class="file outline icon"></i>
<span class="diff-name"><?php echo htmlspecialchars($block['name'], ENT_QUOTES, 'UTF-8'); ?></span>
</div>
<div class="diff-table">
<div class="diff-side diff-expected">
<div class="diff-side-header">
<i class="check icon"></i> 期望输出
</div>
<pre class="diff-content"><?php
$max = max(count($block['expected']), count($block['yours']));
$lines_html = array();
for ($li = 0; $li < $max; $li++) {
$e = isset($block['expected'][$li]) ? $block['expected'][$li] : '';
$y = isset($block['yours'][$li]) ? $block['yours'][$li] : '';
$diff_class = ($e !== $y) ? ' diff-line-mismatch' : '';
$line_no = $li + 1;
$lines_html[] = '<div class="diff-line' . $diff_class . '" data-line="' . $line_no . '"><span class="diff-lineno">' . $line_no . '</span><span class="diff-text">' . htmlspecialchars($e, ENT_QUOTES, 'UTF-8') . '</span></div>';
}
echo implode('', $lines_html);
?></pre>
</div>
<div class="diff-side diff-yours">
<div class="diff-side-header">
<i class="bug icon"></i> 你的输出
</div>
<pre class="diff-content"><?php
$max = max(count($block['expected']), count($block['yours']));
$lines_html = array();
for ($li = 0; $li < $max; $li++) {
$e = isset($block['expected'][$li]) ? $block['expected'][$li] : '';
$y = isset($block['yours'][$li]) ? $block['yours'][$li] : '';
$diff_class = ($e !== $y) ? ' diff-line-mismatch' : '';
$line_no = $li + 1;
$lines_html[] = '<div class="diff-line' . $diff_class . '" data-line="' . $line_no . '"><span class="diff-lineno">' . $line_no . '</span><span class="diff-text">' . htmlspecialchars($y, ENT_QUOTES, 'UTF-8') . '</span></div>';
}
echo implode('', $lines_html);
?></pre>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- 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">
@@ -362,132 +529,12 @@
<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>
<?php if (isset($OJ_DOWNLOAD) && $OJ_DOWNLOAD && ($isRE || $row['result']==6)) { ?>
<a class="ui orange button" href="download.php?sid=<?php echo intval($id) ?>">
<i class="download icon"></i> <?php echo isset($MSG_DOWNLOAD_DATA) ? $MSG_DOWNLOAD_DATA : '下载测试数据'; ?>
</a>
<?php } ?>
</div>
</div>
</div>
<!-- 错误模式 → 解释pats[]/exps[],共 28 条) -->
<script>
var pats = new Array();
var exps = new Array();
var patsSeen = {}; // 防止"段错误"和"stack smashing"双重提示
// === 原有 11 条 ===
pats[0]=/A Not allowed system call.* /;
exps[0]="<?php echo $MSG_A_NOT_ALLOWED_SYSTEM_CALL ?>";
pats[1]=/Segmentation fault/;
exps[1]="<?php echo $MSG_SEGMETATION_FAULT ?>";
pats[2]=/Floating point exception/;
exps[2]="<?php echo $MSG_FLOATING_POINT_EXCEPTION ?>";
pats[3]=/buffer overflow detected/;
exps[3]="<?php echo $MSG_BUFFER_OVERFLOW_DETECTED ?>";
pats[4]=/Killed/;
exps[4]="<?php echo $MSG_PROCESS_KILLED ?>";
pats[5]=/Alarm clock/;
exps[5]="<?php echo $MSG_ALARM_CLOCK ?>";
pats[6]=/CALLID:20/;
exps[6]="<?php echo $MSG_CALLID_20 ?>";
pats[7]=/ArrayIndexOutOfBoundsException/;
exps[7]="<?php echo $MSG_ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION ?>";
pats[8]=/StringIndexOutOfBoundsException/;
exps[8]="<?php echo $MSG_STRING_INDEX_OUT_OF_BOUNDS_EXCEPTION ?>";
pats[9]=/Binary files/;
exps[9]="<?php echo $MSG_WRONG_OUTPUT_TYPE_EXCEPTION ?>";
pats[10]=/non-zero return/;
exps[10]="<?php echo $MSG_NON_ZERO_RETURN ?>";
// === 新增 17 条(覆盖 glibc / Java / Python / Go ===
pats[11]=/stack smashing detected/;
exps[11]="<?php echo $MSG_STACK_SMASHING_DETECTED ?>";
// 同时标 pats[3] 已被覆盖(避免重复)
pats[12]=/terminate called after throwing/;
exps[12]="<?php echo $MSG_TERMINATE_CALLED ?>";
pats[13]=/NullPointerException/;
exps[13]="<?php echo $MSG_NULL_POINTER_EXCEPTION ?>";
pats[14]=/NumberFormatException/;
exps[14]="<?php echo $MSG_NUMBER_FORMAT_EXCEPTION ?>";
pats[15]=/ClassCastException/;
exps[15]="<?php echo $MSG_CLASS_CAST_EXCEPTION ?>";
pats[16]=/NegativeArraySizeException/;
exps[16]="<?php echo $MSG_NEGATIVE_ARRAY_SIZE_EXCEPTION ?>";
pats[17]=/Traceback \(most recent call last\)/;
exps[17]="<?php echo $MSG_PYTHON_TRACEBACK ?>";
pats[18]=/RecursionError/;
exps[18]="<?php echo $MSG_PYTHON_RECURSION_ERROR ?>";
pats[19]=/IndentationError/;
exps[19]="<?php echo $MSG_PYTHON_INDENTATION_ERROR ?>";
pats[20]=/ZeroDivisionError/;
exps[20]="<?php echo $MSG_PYTHON_ZERO_DIVISION ?>";
pats[21]=/NameError:/;
exps[21]="<?php echo $MSG_PYTHON_NAME_ERROR ?>";
pats[22]=/panic:.*runtime error/;
exps[22]="<?php echo $MSG_GO_PANIC ?>";
pats[23]=/Aborted \(core dumped\)/;
exps[23]="<?php echo $MSG_ABORTED_CORE_DUMPED ?>";
pats[24]=/CALLID:(\d+)/;
exps[24]="使用了非法的系统调用syscall请检查是否访问了文件、网络、进程等受限资源。";
pats[25]=/StackOverflowError/;
exps[25]="<?php echo $MSG_STACK_OVERFLOW_ERROR ?? '栈溢出StackOverflowError。通常是递归层数过深或局部大数组占用过多栈空间。' ?>";
pats[26]=/class not found|Could not find or load main class/;
exps[26]="Java 类未找到。检查 public class 名称是否与文件名一致,是否有 package 声明。";
pats[27]=/no test data/;
exps[27]="没有找到测试数据,请联系管理员检查题目数据。";
/**
* 把匹配到的错误模式追加到 <ul id="errexp-list">
* 同一类错误(内存类/异常类/系统调用类)只显示一次
*/
function explain(){
var errmsg = $("#errtxt").text();
if (!errmsg || errmsg.trim() === '') return;
// 同族分组
var familyMap = {
'mem': [1, 3, 11, 25], // 段错误 / 缓冲溢出 / stack smashing / 栈溢出
'sysc': [0, 6, 24], // 系统调用
'time': [4, 5], // 时间/Killed
'arith':[2, 20], // 除零
'jexc': [7, 8, 13, 14, 15, 16, 25], // Java 异常
'pyexc':[17, 18, 19, 20, 21], // Python
'goexc':[22], // Go
};
var shownFamilies = {};
var shown = {};
var items = [];
for (var i = 0; i < pats.length; i++) {
if (!pats[i] || shown[i]) continue;
var ret = pats[i].exec(errmsg);
if (!ret) continue;
// 检查是否已被同族其它规则覆盖
var family = null;
for (var f in familyMap) {
if (familyMap[f].indexOf(i) >= 0) { family = f; break; }
}
if (family && shownFamilies[family]) continue;
if (family) shownFamilies[family] = true;
shown[i] = true;
items.push("<li><code>" + ret[0] + "</code> &rarr; " + exps[i] + "</li>");
}
if (items.length > 0) {
$("#errexp-list").html(items.join(""));
} else {
$("#errexp-list").html("<li class='empty-notice'>未匹配到具体错误模式,请参考原始错误信息或 AI 点评。</li>");
}
}
explain();
</script>
<?php if(isset($OJ_MARKDOWN) && $OJ_MARKDOWN){ ?>
<script>
$(document).ready(function(){
@@ -587,4 +634,96 @@
</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");?>