Compare commits
4 Commits
2b699a185b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b13c95e8a | |||
| 0e5cac3d4e | |||
| c22c788cfa | |||
| 90010113dd |
146
web/reinfo.php
146
web/reinfo.php
@@ -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 = ""; // 折叠后(普通用户看)
|
||||
@@ -120,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 给"主解释"(永远非空)===
|
||||
@@ -203,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) {
|
||||
|
||||
@@ -101,6 +101,20 @@
|
||||
#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;
|
||||
}
|
||||
@@ -119,6 +133,99 @@ i.icon.spinning {
|
||||
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;
|
||||
}
|
||||
.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>
|
||||
|
||||
<div class="padding">
|
||||
@@ -129,6 +236,8 @@ i.icon.spinning {
|
||||
$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. 顶部判题结果横幅 -->
|
||||
@@ -157,16 +266,18 @@ i.icon.spinning {
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 3. 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>
|
||||
<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>
|
||||
@@ -182,8 +293,31 @@ i.icon.spinning {
|
||||
</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');
|
||||
@@ -291,6 +425,63 @@ i.icon.spinning {
|
||||
</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">
|
||||
@@ -338,11 +529,7 @@ i.icon.spinning {
|
||||
<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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user