✨ feat(llm): 引入基于完成度的 AI 提交指导与限流
- 新增 llm_guidance 策略层,根据源码、判题结果与通过率评估完成度分级 - llm-review.php 改为服务端缓冲上游输出,仅下发白名单分类的固定模板 - 新增 llm_review 与 llm_review_rate_limit 表,实现结果缓存与账户/提交两级限流 - ceinfo 模板渲染编译器标出的源码行号/列号,reinfo 仅向高完成度提交展示结构化 diff - download.php 按完成度授权测试点下载,封堵白卷提交套取隐藏用例 - judge_client 在 ACM 非比赛场景下记录已通过测试点数,前端可显示进度 - 附 llm_guidance_policy_test 单测覆盖空白卷、注释卷、隐藏 SPJ、OJ_FULL_DIFF 等关键路径
This commit is contained in:
+77
-181
@@ -1,188 +1,88 @@
|
||||
<?php $show_title=$id." - $MSG_COMPILE_INFO - $OJ_NAME"; ?>
|
||||
<?php include("template/$OJ_TEMPLATE/header.php");?>
|
||||
|
||||
<script src="template/<?php echo $OJ_TEMPLATE?>/js/textFit.min.js"></script>
|
||||
<script src="include/vditor-adapter.js"></script>
|
||||
<link href='<?php echo $OJ_CDN_URL?>highlight/styles/shCore.css' rel='stylesheet' type='text/css'/>
|
||||
<link href='<?php echo $OJ_CDN_URL?>highlight/styles/shThemeDefault.css' rel='stylesheet' type='text/css'/>
|
||||
<style>
|
||||
#compile-location-section { margin: 0 0 18px; }
|
||||
.compile-location-card { border-left: 4px solid #db2828 !important; margin-bottom: 12px !important; }
|
||||
.compile-location-title { display: flex; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 8px; }
|
||||
.compile-source-excerpt {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
background: #fafafa;
|
||||
font-family: 'Fira Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.compile-source-line { display: flex; min-width: max-content; white-space: pre; }
|
||||
.compile-source-line.is-error { background: rgba(219, 40, 40, 0.12); }
|
||||
.compile-source-lineno {
|
||||
width: 50px;
|
||||
padding: 1px 10px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
background: #f1f1f1;
|
||||
border-right: 1px solid #ddd;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.compile-source-line.is-error .compile-source-lineno { color: #db2828; font-weight: 700; }
|
||||
.compile-source-text { padding: 1px 12px; }
|
||||
</style>
|
||||
|
||||
<div class="padding">
|
||||
<div style="margin-top: 0px; margin-bottom: 14px; padding-bottom: 0px; " >
|
||||
<p class="transition visible">
|
||||
<strong ><?php echo $MSG_SOURCE_CODE ?></strong>
|
||||
</p>
|
||||
<div style="margin:0 0 14px;">
|
||||
<p class="transition visible"><strong><?php echo $MSG_SOURCE_CODE ?></strong></p>
|
||||
<div class="ui existing segment">
|
||||
<pre v-if="escape" style="margin-top: 0; margin-bottom: 0; "><code><div class="brush:c" id='source' name="source"></div></code></pre>
|
||||
<pre style="margin:0;"><code><div class="brush:c" id="source" name="source"></div></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 0px; margin-bottom: 14px; " >
|
||||
<p class="transition visible">
|
||||
<strong ><?php echo $MSG_COMPILE_INFO ?></strong>
|
||||
</p>
|
||||
|
||||
<div style="margin:0 0 14px;">
|
||||
<p class="transition visible"><strong><?php echo $MSG_COMPILE_INFO ?></strong></p>
|
||||
<div class="ui existing segment">
|
||||
<pre v-if="escape" style="margin-top: 0; margin-bottom: 0; "><code><div id='errtxt'><?php echo $view_reinfo?></div></code></pre>
|
||||
<pre style="margin:0; white-space:pre-wrap; word-break:break-word;"><code><div id="errtxt"><?php echo $view_reinfo?></div></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 0px; margin-bottom: 14px; " >
|
||||
<p v-if="title" class="transition visible">
|
||||
<strong v-html="title"><?php echo $MSG_ERROR_EXPLAIN;?></strong>
|
||||
</p>
|
||||
<div class="ui existing segment">
|
||||
<pre v-if="escape" style="margin-top: 0; margin-bottom: 0; "><code><div id='errexp'></div></code></pre>
|
||||
|
||||
<?php if (!empty($compile_locations) && is_array($compile_locations)): ?>
|
||||
<div id="compile-location-section">
|
||||
<h3 class="ui header">
|
||||
<i class="map marker alternate icon"></i>
|
||||
<div class="content">
|
||||
编译错误定位
|
||||
<div class="sub header">只标出你的源码行号和列号,不提供修改答案</div>
|
||||
</div>
|
||||
</h3>
|
||||
<?php foreach ($compile_locations as $location): ?>
|
||||
<div class="ui segment compile-location-card">
|
||||
<div class="compile-location-title">
|
||||
<span class="ui red label">第 <?php echo intval($location['line']); ?> 行<?php
|
||||
if (!empty($location['column'])) echo ',第 ' . intval($location['column']) . ' 列';
|
||||
?></span>
|
||||
<span>编译器在这里报告错误</span>
|
||||
</div>
|
||||
<div class="compile-source-excerpt">
|
||||
<?php foreach ($location['excerpt'] as $source_line): ?>
|
||||
<div class="compile-source-line<?php echo $source_line['is_error'] ? ' is-error' : ''; ?>">
|
||||
<span class="compile-source-lineno"><?php echo intval($source_line['number']); ?></span>
|
||||
<span class="compile-source-text"><?php echo htmlspecialchars($source_line['text'], ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<script>
|
||||
var i=0;
|
||||
var pats=new Array();
|
||||
var exps=new Array();
|
||||
pats[i]=/System\.out\.print.*%.*/;
|
||||
exps[i++]="<?php echo $MSG_SYSTEM_OUT_PRINT; ?>";
|
||||
pats[i]=/.*没有那个文件或目录.*/;
|
||||
exps[i++]="<?php echo $MSG_NO_SUCH_FILE_OR_DIRECTORY; ?>";
|
||||
pats[i]=/not a statement/;
|
||||
exps[i++]="<?php echo $MSG_NOT_A_STATEMENT; ?>";
|
||||
pats[i]=/class, interface, or enum expected/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_CLASS_INTERFACE_ENUM; ?>";
|
||||
pats[i]=/asm.*java/;
|
||||
exps[i++]="<?php echo $MSG_SUBMIT_JAVA_AS_C_LANG; ?>";
|
||||
pats[i]=/package .* does not exist/;
|
||||
exps[i++]="<?php echo $MSG_DOES_NOT_EXIST_PACKAGE; ?>";
|
||||
pats[i]=/possible loss of precision/;
|
||||
exps[i++]="<?php echo $MSG_POSSIBLE_LOSS_OF_PRECISION; ?>";
|
||||
pats[i]=/incompatible types/;
|
||||
exps[i++]="<?php echo $MSG_INCOMPATIBLE_TYPES; ?>";
|
||||
pats[i]=/illegal start of expression/;
|
||||
exps[i++]="<?php echo $MSG_ILLEGAL_START_OF_EXPRESSION; ?>";
|
||||
pats[i]=/cannot find symbol/;
|
||||
exps[i++]="<?php echo $MSG_CANNOT_FIND_SYMBOL; ?>";
|
||||
pats[i]=/';' expected/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_SEMICOLON; ?>";
|
||||
pats[i]=/should be declared in a file named/;
|
||||
exps[i++]="<?php echo $MSG_DECLARED_JAVA_FILE_NAMED; ?>";
|
||||
pats[i]=/expected ‘.*’ at end of input/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_WILDCARD_CHARACTER_AT_END_OF_INPUT; ?>";
|
||||
pats[i]=/invalid conversion from ‘.*’ to ‘.*’/;
|
||||
exps[i++]="<?php echo $MSG_INVALID_CONVERSION; ?>";
|
||||
pats[i]=/warning.*declaration of 'main' with no type/;
|
||||
exps[i++]="<?php echo $MSG_NO_RETURN_TYPE_IN_MAIN; ?>";
|
||||
pats[i]=/'.*' was not declared in this scope/;
|
||||
exps[i++]="<?php echo $MSG_NOT_DECLARED_IN_SCOPE; ?>";
|
||||
pats[i]=/main’ must return ‘int’/;
|
||||
exps[i++]="<?php echo $MSG_MAIN_MUST_RETURN_INT; ?>";
|
||||
pats[i]=/expected identifier or '\(' before numeric constant/
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_IDENTIFIER; ?>";
|
||||
pats[i]=/printf.*was not declared in this scope/;
|
||||
exps[i++]="<?php echo $MSG_PRINTF_NOT_DECLARED_IN_SCOPE; ?>";
|
||||
pats[i]=/warning: ignoring return value of/;
|
||||
exps[i++]="<?php echo $MSG_IGNOREING_RETURN_VALUE; ?>";
|
||||
pats[i]=/:.*__int64’ undeclared/;
|
||||
exps[i++]="<?php echo $MSG_NOT_DECLARED_INT64; ?>";
|
||||
pats[i]=/:.*expected ‘;’ before/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_SEMICOLON_BEFORE; ?>";
|
||||
pats[i]=/ .* undeclared \(first use in this function\)/;
|
||||
exps[i++]="<?php echo $MSG_UNDECLARED_NAME; ?>";
|
||||
pats[i]=/scanf.*was not declared in this scope/;
|
||||
exps[i++]="<?php echo $MSG_SCANF_NOT_DECLARED_IN_SCOPE; ?>";
|
||||
pats[i]=/memset.*was not declared in this scope/;
|
||||
exps[i++]="<?php echo $MSG_MEMSET_NOT_DECLARED_IN_SCOPE; ?>";
|
||||
pats[i]=/malloc.*was not declared in this scope/;
|
||||
exps[i++]="<?php echo $MSG_MALLOC_NOT_DECLARED_IN_SCOPE; ?>";
|
||||
pats[i]=/puts.*was not declared in this scope/;
|
||||
exps[i++]="<?php echo $MSG_PUTS_NOT_DECLARED_IN_SCOPE; ?>";
|
||||
pats[i]=/gets.*was not declared in this scope/;
|
||||
exps[i++]="<?php echo $MSG_GETS_NOT_DECLARED_IN_SCOPE; ?>";
|
||||
pats[i]=/str.*was not declared in this scope/;
|
||||
exps[i++]="<?php echo $MSG_STRING_NOT_DECLARED_IN_SCOPE; ?>";
|
||||
pats[i]=/‘import’ does not name a type/;
|
||||
exps[i++]="<?php echo $MSG_NO_TYPE_IMPORT_IN_C_CPP; ?>";
|
||||
pats[i]=/asm’ undeclared/;
|
||||
exps[i++]="<?php echo $MSG_ASM_UNDECLARED; ?>";
|
||||
pats[i]=/redefinition of/;
|
||||
exps[i++]="<?php echo $MSG_REDEFINITION_OF; ?>";
|
||||
pats[i]=/expected declaration or statement at end of input/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_DECLARATION_OR_STATEMENT; ?>";
|
||||
pats[i]=/warning: unused variable/;
|
||||
exps[i++]="<?php echo $MSG_UNUSED_VARIABLE; ?>";
|
||||
pats[i]=/implicit declaration of function/;
|
||||
exps[i++]="<?php echo $MSG_IMPLICIT_DECLARTION_OF_FUNCTION; ?>";
|
||||
pats[i]=/too .* arguments to function/;
|
||||
exps[i++]="<?php echo $MSG_ARGUMENTS_ERROR_IN_FUNCTION; ?>";
|
||||
pats[i]=/expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘namespace’/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_BEFORE_NAMESPACE; ?>";
|
||||
pats[i]=/stray ‘\\[0123456789]*’ in program/;
|
||||
exps[i++]="<?php echo $MSG_STRAY_PROGRAM; ?>";
|
||||
pats[i]=/division by zero/;
|
||||
exps[i++]="<?php echo $MSG_DIVISION_BY_ZERO; ?>";
|
||||
pats[i]=/cannot be used as a function/;
|
||||
exps[i++]="<?php echo $MSG_CANNOT_BE_USED_AS_A_FUNCTION; ?>";
|
||||
pats[i]=/format .* expects type .* but argument .* has type .*/;
|
||||
exps[i++]="<?php echo $MSG_CANNOT_FIND_TYPE; ?>";
|
||||
pats[i]=/类.*是公共的,应在名为 .*java 的文件中声明/;
|
||||
exps[i++]="<?php echo $MSG_JAVA_CLASS_ERROR; ?>";
|
||||
pats[i]=/expected ‘\)’ before ‘.*’ token/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_BRACKETS_TOKEN; ?>";
|
||||
pats[i]=/找不到符号/;
|
||||
exps[i++]="<?php echo $MSG_NOT_FOUND_SYMBOL; ?>";
|
||||
pats[i]=/需要为 class、interface 或 enum/;
|
||||
exps[i++]="<?php echo $MSG_NEED_CLASS_INTERFACE_ENUM; ?>";
|
||||
pats[i]=/符号: 类 .*List/;
|
||||
exps[i++]="<?php echo $MSG_CLASS_SYMBOL_ERROR; ?>";
|
||||
pats[i]=/方法声明无效;需要返回类型/;
|
||||
exps[i++]="<?php echo $MSG_INVALID_METHOD_DECLARATION; ?>";
|
||||
pats[i]=/expected.*before.*&.*token/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_AMPERSAND_TOKEN; ?>";
|
||||
pats[i]=/非法的表达式开始/;
|
||||
exps[i++]="<?php echo $MSG_DECLARED_FUNCTION_ORDER; ?>";
|
||||
pats[i]=/需要 ';'/;
|
||||
exps[i++]="<?php echo $MSG_NEED_SEMICOLON; ?>";
|
||||
pats[i]=/extra tokens at end of #include directive/;
|
||||
exps[i++]="<?php echo $MSG_EXTRA_TOKEN_AT_END_OF_INCLUDE; ?>";
|
||||
pats[i]=/int.*hasNext/;
|
||||
exps[i++]="<?php echo $MSG_INT_HAS_NEXT; ?>";
|
||||
pats[i]=/unterminated comment/;
|
||||
exps[i++]="<?php echo $MSG_UNTERMINATED_COMMENT; ?>";
|
||||
pats[i]=/expected '=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘{’ token/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_BRACES_TOKEN; ?>";
|
||||
pats[i]=/进行语法解析时已到达文件结尾/;
|
||||
exps[i++]="<?php echo $MSG_REACHED_END_OF_FILE_1; ?>";
|
||||
pats[i]=/subscripted value is neither array nor pointer/;
|
||||
exps[i++]="<?php echo $MSG_SUBSCRIPT_ERROR; ?>";
|
||||
pats[i]=/expected expression before ‘%’ token/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_PERCENT_TOKEN; ?>";
|
||||
pats[i]=/expected expression before ‘.*’ token/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_EXPRESSION_TOKEN; ?>";
|
||||
pats[i]=/expected but/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_BUT; ?>";
|
||||
pats[i]=/redefinition of ‘main’/;
|
||||
exps[i++]="<?php echo $MSG_REDEFINITION_MAIN; ?>";
|
||||
pats[i]=/iostream: No such file or directory/;
|
||||
exps[i++]="<?php echo $MSG_IOSTREAM_ERROR; ?>";
|
||||
pats[i]=/expected unqualified-id before ‘\[’ token/;
|
||||
exps[i++]="<?php echo $MSG_EXPECTED_UNQUALIFIED_ID_TOKEN; ?>";
|
||||
pats[i]=/解析时已到达文件结尾/;
|
||||
exps[i++]="<?php echo $MSG_REACHED_END_OF_FILE_2; ?>";
|
||||
pats[i]=/非法字符/;
|
||||
exps[i++]="<?php echo $MSG_INVALID_SYMBOL; ?>";
|
||||
pats[i]=/应在名为.*的文件中声明/;
|
||||
exps[i++]="<?php echo $MSG_DECLARED_FILE_NAMED; ?>";
|
||||
pats[i]=/variably modified/;
|
||||
exps[i++]="<?php echo $MSG_VARIABLY_MODIFIED; ?>";
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (isset($OJ_LLM_ENABLED) && $OJ_LLM_ENABLED) {
|
||||
require("template/$OJ_TEMPLATE/llm-guidance.php");
|
||||
} ?>
|
||||
</div>
|
||||
|
||||
function explain(){
|
||||
//alert("asdf");
|
||||
var errmsg=$("#errtxt").text();
|
||||
var expmsg="";
|
||||
for(var i=0;i<pats.length;i++){
|
||||
var pat=pats[i];
|
||||
var exp=exps[i];
|
||||
var ret=pat.exec(errmsg);
|
||||
if(ret){
|
||||
expmsg+=ret+":"+exp+"<br>";
|
||||
}
|
||||
}
|
||||
document.getElementById("errexp").innerHTML=expmsg;
|
||||
//alert(expmsg);
|
||||
}
|
||||
</script>
|
||||
<script src='<?php echo $OJ_CDN_URL?>highlight/scripts/shCore.js' type='text/javascript'></script>
|
||||
<script src='<?php echo $OJ_CDN_URL?>highlight/scripts/shBrushCpp.js' type='text/javascript'></script>
|
||||
<script src='<?php echo $OJ_CDN_URL?>highlight/scripts/shBrushCss.js' type='text/javascript'></script>
|
||||
@@ -198,17 +98,13 @@ document.getElementById("errexp").innerHTML=expmsg;
|
||||
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
$("#source").load("showsource2.php?id=<?php echo $id?>",function(response,status,xhr){
|
||||
|
||||
if(status=="success"){
|
||||
SyntaxHighlighter.config.bloggerMode = false;
|
||||
SyntaxHighlighter.config.clipboardSwf = '<?php echo $OJ_CDN_URL?>highlight/scripts/clipboard.swf';
|
||||
SyntaxHighlighter.highlight();
|
||||
explain();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$('#source').load('showsource2.php?id=<?php echo intval($id)?>', function(response, status){
|
||||
if (status === 'success') {
|
||||
SyntaxHighlighter.config.bloggerMode = false;
|
||||
SyntaxHighlighter.config.clipboardSwf = '<?php echo $OJ_CDN_URL?>highlight/scripts/clipboard.swf';
|
||||
SyntaxHighlighter.highlight();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
$llm_guidance = isset($llm_guidance) && is_array($llm_guidance)
|
||||
? $llm_guidance
|
||||
: array('mode' => 'hidden', 'level' => 'none', 'score' => 0, 'auto_fetch' => false);
|
||||
$llm_guidance_visible = $llm_guidance['mode'] !== 'hidden';
|
||||
$llm_guidance_auto_fetch = isset($llm_guidance_auto_fetch)
|
||||
? !!$llm_guidance_auto_fetch
|
||||
: !empty($llm_guidance['auto_fetch']);
|
||||
$llm_guidance_label = llm_guidance_mode_label($llm_guidance['mode']);
|
||||
?>
|
||||
|
||||
<?php if ($llm_guidance_visible): ?>
|
||||
<style>
|
||||
#llm-section {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 18px;
|
||||
border-left: 4px solid #2185d0;
|
||||
}
|
||||
#llm-section .llm-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color 0.15s;
|
||||
padding: 4px 8px;
|
||||
margin: -4px -8px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
#llm-section .llm-section-header:hover { background-color: rgba(33,150,243,0.06); }
|
||||
#llm-section .llm-toggle-icon { margin-left: auto !important; transition: transform 0.2s; color: #888; }
|
||||
#llm-section .llm-guidance-meta { margin: 10px 0 4px; }
|
||||
#llm-section .llm-guidance-meta .label { margin-bottom: 4px; }
|
||||
#llm-review-result { word-break: break-word; }
|
||||
#llm-review-content > :first-child { margin-top: 0; }
|
||||
#llm-review-content > :last-child { margin-bottom: 0; }
|
||||
</style>
|
||||
|
||||
<div class="ui raised segment" id="llm-section"
|
||||
data-guidance-mode="<?php echo htmlspecialchars($llm_guidance['mode'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-completion-level="<?php echo htmlspecialchars($llm_guidance['level'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-completion-score="<?php echo intval($llm_guidance['score']); ?>">
|
||||
<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" id="llm-mode-description"><?php echo htmlspecialchars($llm_guidance_label, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||
</div>
|
||||
<i class="dropdown icon llm-toggle-icon"></i>
|
||||
</h3>
|
||||
|
||||
<div id="llm-section-body">
|
||||
<div class="ui tiny labels llm-guidance-meta">
|
||||
<span class="ui blue basic label" id="llm-mode-label"><?php echo htmlspecialchars($llm_guidance_label, ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
<span class="ui basic label" id="llm-completion-label">完成度 <?php echo intval($llm_guidance['score']); ?>%</span>
|
||||
</div>
|
||||
|
||||
<div id="llm-review-loading" class="ui active inline text loader" style="margin:12px 0;">
|
||||
正在定位最值得检查的一处...
|
||||
</div>
|
||||
<div id="llm-review-result" style="display:none; margin-top:12px;">
|
||||
<div class="ui existing segment" id="llm-review-content"></div>
|
||||
</div>
|
||||
<div id="llm-review-error" style="display:none; margin-top:12px;">
|
||||
<div class="ui negative message">
|
||||
<p id="llm-review-error-msg"></p>
|
||||
<button type="button" class="ui small button" id="llm-review-retry">重新获取</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var section = document.getElementById('llm-section');
|
||||
var header = document.getElementById('llm-toggle');
|
||||
var body = document.getElementById('llm-section-body');
|
||||
var icon = header ? header.querySelector('.llm-toggle-icon') : null;
|
||||
var started = false;
|
||||
var finished = false;
|
||||
var currentEvent = '';
|
||||
|
||||
if (!section || !header || !body) return;
|
||||
|
||||
function setCollapsed(collapsed) {
|
||||
body.style.display = collapsed ? 'none' : '';
|
||||
if (icon) icon.style.transform = collapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
|
||||
}
|
||||
|
||||
header.addEventListener('click', function(){
|
||||
setCollapsed(body.style.display !== 'none');
|
||||
});
|
||||
|
||||
function modeLabel(mode) {
|
||||
var labels = {
|
||||
flowchart: '流程图引导',
|
||||
focused_hint: '聚焦排错',
|
||||
output_diff: '输出差异定位',
|
||||
compile_location: '编译错误定位'
|
||||
};
|
||||
return labels[mode] || 'AI 指导';
|
||||
}
|
||||
|
||||
function applyMeta(data) {
|
||||
if (!data) return;
|
||||
var label = modeLabel(data.mode);
|
||||
section.setAttribute('data-guidance-mode', data.mode || '');
|
||||
document.getElementById('llm-mode-label').textContent = label;
|
||||
document.getElementById('llm-mode-description').textContent = label;
|
||||
document.getElementById('llm-completion-label').textContent = '完成度 ' + parseInt(data.completion_score || 0, 10) + '%';
|
||||
|
||||
var diff = document.getElementById('diff-section');
|
||||
if (diff && data.show_output_diff) diff.classList.add('guidance-diff-open');
|
||||
}
|
||||
|
||||
function renderResult(fullText) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
var container = document.getElementById('llm-review-content');
|
||||
var result = document.getElementById('llm-review-result');
|
||||
document.getElementById('llm-review-loading').style.display = 'none';
|
||||
container.textContent = fullText;
|
||||
result.style.display = 'block';
|
||||
if (fullText && typeof HustOJVditor !== 'undefined' && HustOJVditor.renderMarkdownBlocks) {
|
||||
HustOJVditor.renderMarkdownBlocks('#llm-review-content', {
|
||||
useTextContent: true,
|
||||
previewOptions: { markdown: { sanitize: true } }
|
||||
}).catch(function(error){
|
||||
console.error('Failed to render AI guidance.', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
if (finished) return;
|
||||
started = false;
|
||||
document.getElementById('llm-review-loading').style.display = 'none';
|
||||
document.getElementById('llm-review-error-msg').textContent = message || '获取指导失败';
|
||||
document.getElementById('llm-review-error').style.display = 'block';
|
||||
}
|
||||
|
||||
function fetchGuidance() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
finished = false;
|
||||
currentEvent = '';
|
||||
setCollapsed(false);
|
||||
document.getElementById('llm-review-loading').style.display = '';
|
||||
document.getElementById('llm-review-error').style.display = 'none';
|
||||
document.getElementById('llm-review-result').style.display = 'none';
|
||||
|
||||
var fullText = '';
|
||||
var sseBuffer = '';
|
||||
var decoder = null;
|
||||
|
||||
function processSSE(text) {
|
||||
sseBuffer += text;
|
||||
var lines = sseBuffer.split('\n');
|
||||
sseBuffer = lines.pop();
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].replace(/\r$/, '');
|
||||
if (line.indexOf('event: ') === 0) {
|
||||
currentEvent = line.substring(7);
|
||||
} else if (line.indexOf('data: ') === 0) {
|
||||
try {
|
||||
var data = JSON.parse(line.substring(6));
|
||||
if (currentEvent === 'meta') {
|
||||
applyMeta(data);
|
||||
} else if (currentEvent === 'chunk') {
|
||||
fullText += data.text || '';
|
||||
} else if (currentEvent === 'cached') {
|
||||
fullText = data.text || '';
|
||||
} else if (currentEvent === 'done') {
|
||||
renderResult(fullText);
|
||||
} else if (currentEvent === 'error') {
|
||||
showError(data.message || '获取指导失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Invalid AI guidance event.', error);
|
||||
}
|
||||
currentEvent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof fetch === 'undefined' || typeof TextDecoder === 'undefined') {
|
||||
showError('浏览器版本过旧,请升级后重试');
|
||||
return;
|
||||
}
|
||||
decoder = new TextDecoder();
|
||||
|
||||
fetch('llm-review.php?sid=<?php echo intval($id); ?><?php
|
||||
if (isset($llm_guidance_has_output_diff)) {
|
||||
echo '&structured_diff=' . ($llm_guidance_has_output_diff ? '1' : '0');
|
||||
}
|
||||
?>', { credentials: 'same-origin' })
|
||||
.then(function(response){
|
||||
if (!response.ok) {
|
||||
return response.text().then(function(text){
|
||||
var message = '请求失败 (HTTP ' + response.status + ')';
|
||||
try {
|
||||
var data = JSON.parse(text);
|
||||
if (data && data.error) message = data.error;
|
||||
} catch (ignore) {}
|
||||
throw new Error(message);
|
||||
});
|
||||
}
|
||||
if (!response.body || !response.body.getReader) throw new Error('浏览器不支持流式响应');
|
||||
var reader = response.body.getReader();
|
||||
|
||||
function readNext() {
|
||||
return reader.read().then(function(result){
|
||||
if (result.done) {
|
||||
var tail = decoder.decode();
|
||||
if (tail || sseBuffer) processSSE(tail + '\n');
|
||||
if (!finished && fullText) {
|
||||
renderResult(fullText);
|
||||
} else if (!finished) {
|
||||
showError('响应提前结束,请重试');
|
||||
}
|
||||
return;
|
||||
}
|
||||
processSSE(decoder.decode(result.value, { stream: true }));
|
||||
return readNext();
|
||||
});
|
||||
}
|
||||
return readNext();
|
||||
})
|
||||
.catch(function(error){ showError(error.message || '网络请求失败'); });
|
||||
}
|
||||
|
||||
document.getElementById('llm-review-retry').addEventListener('click', fetchGuidance);
|
||||
window.fetchLLMReview = fetchGuidance;
|
||||
|
||||
<?php if ($llm_guidance_auto_fetch): ?>
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', fetchGuidance, { once: true });
|
||||
} else {
|
||||
fetchGuidance();
|
||||
}
|
||||
<?php else: ?>
|
||||
document.getElementById('llm-review-loading').style.display = 'none';
|
||||
<?php endif; ?>
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
+29
-207
@@ -93,34 +93,6 @@
|
||||
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;
|
||||
@@ -138,6 +110,10 @@ i.icon.spinning {
|
||||
#diff-section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
#diff-section.guidance-diff-open {
|
||||
border-top: 3px solid rgba(33, 133, 208, 0.35);
|
||||
padding-top: 14px;
|
||||
}
|
||||
.diff-card {
|
||||
margin-bottom: 14px;
|
||||
border-left: 4px solid #fbbd08;
|
||||
@@ -157,6 +133,10 @@ i.icon.spinning {
|
||||
font-family: 'Fira Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.diff-download {
|
||||
margin-left: auto;
|
||||
font-weight: 400;
|
||||
}
|
||||
.diff-table {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -266,164 +246,10 @@ i.icon.spinning {
|
||||
</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 } ?>
|
||||
<!-- 3. AI 指导:仅错误终态渲染,并自动展开/请求一次 -->
|
||||
<?php if (isset($OJ_LLM_ENABLED) && $OJ_LLM_ENABLED) {
|
||||
require("template/$OJ_TEMPLATE/llm-guidance.php");
|
||||
} ?>
|
||||
|
||||
<!-- 3.5. 对比详情(WA/PE 时显示,diff 视图) -->
|
||||
<?php if (!empty($diff_blocks) && is_array($diff_blocks)): ?>
|
||||
@@ -440,6 +266,18 @@ i.icon.spinning {
|
||||
<div class="diff-header">
|
||||
<i class="file outline icon"></i>
|
||||
<span class="diff-name"><?php echo htmlspecialchars($block['name'], ENT_QUOTES, 'UTF-8'); ?></span>
|
||||
<?php
|
||||
$download_base = llm_guidance_testcase_base($block['name']);
|
||||
$download_allowed = isset($OJ_DOWNLOAD) && $OJ_DOWNLOAD
|
||||
&& !empty($llm_guidance_download_allowed)
|
||||
&& $download_base !== null
|
||||
&& (!(isset($OJ_DL_1ST_WA_ONLY) && $OJ_DL_1ST_WA_ONLY) || $idx === 0);
|
||||
if ($download_allowed):
|
||||
?>
|
||||
<a class="diff-download" href="download.php?sid=<?php echo intval($id); ?>&name=<?php echo rawurlencode($download_base . '.out'); ?>">
|
||||
<i class="download icon"></i>下载该测试点
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="diff-table">
|
||||
<div class="diff-side diff-expected">
|
||||
@@ -552,6 +390,11 @@ i.icon.spinning {
|
||||
getSource: function (element) {
|
||||
var pre = element.querySelector ? element.querySelector('pre') : null;
|
||||
return pre ? pre.textContent : element.textContent || '';
|
||||
},
|
||||
previewOptions: {
|
||||
markdown: {
|
||||
sanitize: true
|
||||
}
|
||||
}
|
||||
}).then(function () {
|
||||
for(let i=1;i<10;i++){
|
||||
@@ -581,27 +424,6 @@ i.icon.spinning {
|
||||
"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 ?>");
|
||||
|
||||
Reference in New Issue
Block a user