Some checks failed
Deploy / deploy (push) Has been cancelled
「失败 3 次解锁 AI 提示」实际是「在当前这次页面会话里再失败 3 次」: problemStore.failCount 是个从 0 起数的内存计数器,刷新、切题、跳进跳出就归零, 而后端闸门数的是数据库里的历史失败数,两边根本不是一回事。昨天在这题上撞了 十次墙的学生今天进来照样看不到按钮。 - 数法收成一个 countFailedSubmissions(),题目详情的 myFailedCount 和 POST /ai/hint 共用。原来详情把「等待/正在评分」也算失败,连点三次提交就能 把按钮点亮,点下去却回 hint-locked - 阈值 3 挪进契约 HINT_MIN_FAILURES,两端引用同一个常量 - failCount 改成 myFailedCount + 本次会话增量;在题目页里登录的补拉一次详情, 否则停在匿名时的 0 - 结果面板改 display-directive="show",不再一收起来就把流式输出中的提示连同 那次 LLM 调用一起作废;补「上次结果」按钮,原来唯一的重开方式是再提交一次 - prompt 里的判题结果翻成中文,原来拼的是裸状态码,模型不知道 -1 是什么 - system_error 不计入失败数、也不显示按钮:判题机自己崩了不是学生的问题 - 比赛中不给提示,和「求助」按钮同一个口径 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RC5uL72UY9aZFuTvUKe2jv
293 lines
9.2 KiB
Vue
293 lines
9.2 KiB
Vue
<script setup lang="ts">
|
||
import { Icon } from "@iconify/vue"
|
||
import { storeToRefs } from "pinia"
|
||
import { formatCode, getReaction, submitCode } from "oj/api"
|
||
import { useCodeStore } from "oj/store/code"
|
||
import { useProblemStore } from "oj/store/problem"
|
||
import { useFireworks } from "oj/problem/composables/useFireworks"
|
||
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
|
||
import { LANGUAGE_FORMAT_VALUE, SubmissionStatus } from "utils/constants"
|
||
import type { SubmitCodePayload } from "utils/types"
|
||
import SubmissionResult from "./SubmissionResult.vue"
|
||
import { getSubmitButtonState } from "./submitButtonState"
|
||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||
import { useUserStore } from "shared/store/user"
|
||
import {
|
||
checkPythonSyntax,
|
||
prefetchPythonSyntaxChecker,
|
||
} from "oj/problem/utils/pythonSyntaxCheck"
|
||
|
||
// ==================== 异步组件 ====================
|
||
const ProblemReaction = defineAsyncComponent(
|
||
() => import("./ProblemReaction.vue"),
|
||
)
|
||
|
||
// ==================== 基础状态 ====================
|
||
const userStore = useUserStore()
|
||
const codeStore = useCodeStore()
|
||
const problemStore = useProblemStore()
|
||
const { problem } = storeToRefs(problemStore)
|
||
const route = useRoute()
|
||
const contestID = (route.params.contestID as string) ?? ""
|
||
const problemSetId = (route.params.problemSetId as string) ?? ""
|
||
|
||
const router = useRouter()
|
||
const [commentPanel] = useToggle()
|
||
const message = useMessage()
|
||
|
||
function closeCommentPanel() {
|
||
commentPanel.value = false
|
||
}
|
||
|
||
const { isDesktop } = useBreakpoints()
|
||
|
||
// ==================== 烟花效果 ====================
|
||
const { celebrate } = useFireworks()
|
||
|
||
// ==================== 判题监控 ====================
|
||
const { submission, judging, pending, submitting, startMonitoring } =
|
||
useSubmissionMonitor()
|
||
|
||
const showResult = ref(false)
|
||
const isFormatting = ref(false)
|
||
const isSubmittingRequest = ref(false)
|
||
|
||
// ==================== Python 语法检测器预取 ====================
|
||
// 选中 Python3 时就把 Skulpt 拉下来,避免点提交时才开始下载
|
||
watch(
|
||
() => codeStore.code.language,
|
||
(language) => {
|
||
if (language === "Python3") prefetchPythonSyntaxChecker()
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
|
||
// ==================== 提交冷却 ====================
|
||
const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, {
|
||
controls: true,
|
||
immediate: false,
|
||
})
|
||
|
||
// ==================== AC 后弹出点评轮盘 ====================
|
||
// 只对已经能评价、且还没评过的人弹:后端要求先有 AC 才收评价,这里刚 AC 完正好;
|
||
// mine 非 null 说明早就评过了,别再打扰。
|
||
const { start: showCommentPanelDelayed } = useTimeoutFn(
|
||
async () => {
|
||
const res = await getReaction(problem.value!.id)
|
||
if (res.mine === null) {
|
||
commentPanel.value = true
|
||
}
|
||
},
|
||
1500,
|
||
{ immediate: false },
|
||
)
|
||
|
||
const { start: goToProblemSetDelayed } = useTimeoutFn(
|
||
() => {
|
||
router.push({
|
||
name: "problemset",
|
||
params: {
|
||
problemSetId: problemSetId,
|
||
},
|
||
})
|
||
},
|
||
1500,
|
||
{ immediate: false },
|
||
)
|
||
|
||
// ==================== 计算属性 ====================
|
||
const buttonState = computed(() =>
|
||
getSubmitButtonState({
|
||
isAuthed: userStore.isAuthed,
|
||
hasCode: codeStore.code.value.trim() !== "",
|
||
isFormatting: isFormatting.value,
|
||
isSubmitting: isSubmittingRequest.value || submitting.value,
|
||
isJudging: judging.value || pending.value,
|
||
isCooldown: isCooldown.value,
|
||
}),
|
||
)
|
||
|
||
// ==================== 提交函数 ====================
|
||
async function submit() {
|
||
if (buttonState.value.disabled) return
|
||
|
||
// 0. Python3 语法检测
|
||
if (codeStore.code.language === "Python3") {
|
||
const syntaxError = await checkPythonSyntax(codeStore.code.value)
|
||
if (syntaxError) {
|
||
message.warning(`第 ${syntaxError.line} 行存在语法错误,请修正后再提交`)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 0.5 提交前自动格式化(Python3 用 ruff,C/C++ 用 clang-format,SQL 用 sqlparse)
|
||
const formatLang = LANGUAGE_FORMAT_VALUE[codeStore.code.language]
|
||
if (["python", "c", "cpp", "sql"].includes(formatLang)) {
|
||
isFormatting.value = true
|
||
try {
|
||
const res = await formatCode({
|
||
code: codeStore.code.value,
|
||
language: formatLang,
|
||
})
|
||
codeStore.setCode(res.code)
|
||
} catch (e: any) {
|
||
if (e?.error === "format-error") {
|
||
// 仅 Python3 会出现:代码本身存在语法错误
|
||
message.warning(`代码格式化失败:${e.data},请检查代码后重试`)
|
||
return
|
||
}
|
||
// server-error / 网络异常:格式化工具问题,静默降级,提交原代码
|
||
} finally {
|
||
isFormatting.value = false
|
||
}
|
||
}
|
||
|
||
// 1. 构建提交数据
|
||
const data: SubmitCodePayload = {
|
||
problemId: problem.value!.id,
|
||
language: codeStore.code.language,
|
||
code: codeStore.code.value,
|
||
}
|
||
if (contestID) {
|
||
data.contestId = parseInt(contestID)
|
||
}
|
||
// 从题单入口进来的,把来源题单一起报上去:提交列表要据此标出「来自题单」。
|
||
// 只是来源标记,题单进度仍由后端判完之后自己记账(见上面那段注释)
|
||
if (problemSetId) {
|
||
data.problemSetId = parseInt(problemSetId)
|
||
}
|
||
// 2. 提交代码到后端
|
||
isSubmittingRequest.value = true
|
||
try {
|
||
const res = await submitCode(data)
|
||
console.log(`[Submit] 代码已提交: ID=${res.submissionId}`)
|
||
|
||
// 3. 启动冷却 + 监控
|
||
startCooldown()
|
||
startMonitoring(res.submissionId)
|
||
showResult.value = true
|
||
} finally {
|
||
isSubmittingRequest.value = false
|
||
}
|
||
}
|
||
|
||
// ==================== 失败计数 ====================
|
||
// 这里只数本次会话的增量,历史失败数由 problem.myFailedCount 带进来。
|
||
// 排除的状态要和后端 judge/status.ts 的 NON_FAILURE_RESULTS 对齐,
|
||
// 尤其是 system_error —— 判题机自己崩了不该推进 AI 提示的解锁进度。
|
||
watch(
|
||
() => submission.value?.result,
|
||
(result) => {
|
||
if (result === undefined || result === null) return
|
||
if (
|
||
result === SubmissionStatus.pending ||
|
||
result === SubmissionStatus.judging ||
|
||
result === SubmissionStatus.submitting
|
||
)
|
||
return
|
||
if (
|
||
result !== SubmissionStatus.accepted &&
|
||
result !== SubmissionStatus.ast_check_failed &&
|
||
result !== SubmissionStatus.system_error
|
||
) {
|
||
problemStore.incrementFailCount()
|
||
}
|
||
},
|
||
)
|
||
|
||
// ==================== AC庆祝效果 ====================
|
||
watch(
|
||
() => submission.value?.result,
|
||
async (result) => {
|
||
if (
|
||
result !== SubmissionStatus.accepted &&
|
||
result !== SubmissionStatus.ast_check_failed
|
||
)
|
||
return
|
||
|
||
// 1. 刷新题目状态
|
||
problem.value!.myStatus = 0
|
||
|
||
// 题单进度不在这里更新了。以前是 AC 之后回调 PUT /problem-set-progress,只认路由
|
||
// 参数里那一个题单:从普通题库入口做出同一道题不计进度,网络一抖、页面提前关掉进度
|
||
// 就静默丢失。现在判题那一路直接记账(judge/run.ts),而且是记进所有已加入且包含
|
||
// 这道题的题单;收到「判完了」的时候进度已经落库,跳回题单页看到的就是新数据。
|
||
|
||
if (result !== SubmissionStatus.accepted) return
|
||
|
||
// 3. 放烟花
|
||
celebrate()
|
||
|
||
// 4. 弹出评价框。比赛里不打扰;题单里 1.5 秒后要跳回题单页,弹了也会被冲掉
|
||
if (!contestID && !problemSetId) {
|
||
showCommentPanelDelayed()
|
||
}
|
||
|
||
if (problemSetId) {
|
||
// 延迟回到题单页面
|
||
goToProblemSetDelayed()
|
||
}
|
||
},
|
||
)
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 提交按钮 + 结果弹窗。
|
||
display-directive 默认是 "if":面板一收起来整个 SubmissionResult 就被卸载,
|
||
正在流式输出的 AI 提示连同已经生成的内容一起没了,那次 LLM 调用白花。
|
||
改成 "show" 之后内容留着,重新打开还是原样。 -->
|
||
<n-popover
|
||
trigger="manual"
|
||
display-directive="show"
|
||
placement="bottom-end"
|
||
scrollable
|
||
:show-arrow="false"
|
||
style="max-height: 600px"
|
||
:show="showResult"
|
||
@clickoutside="showResult = false"
|
||
>
|
||
<template #trigger>
|
||
<n-button
|
||
:size="isDesktop ? 'medium' : 'small'"
|
||
type="primary"
|
||
:disabled="buttonState.disabled"
|
||
@click="submit"
|
||
>
|
||
<template #icon>
|
||
<n-icon>
|
||
<Icon :icon="buttonState.icon" />
|
||
</n-icon>
|
||
</template>
|
||
{{ buttonState.label }}
|
||
</n-button>
|
||
</template>
|
||
|
||
<!-- 结果展示 -->
|
||
<SubmissionResult :submission="submission" />
|
||
</n-popover>
|
||
|
||
<!-- 结果面板点一下别处就收起来,而 showResult 只在提交时被置 true ——
|
||
原来唯一的重开方式是「再提交一次」,AI 提示读到一半去看眼题面就回不来了。
|
||
只在这次会话提交过之后才出现,没提交时工具栏保持原样。 -->
|
||
<n-button
|
||
v-if="submission && !showResult"
|
||
:size="isDesktop ? 'medium' : 'small'"
|
||
@click="showResult = true"
|
||
>
|
||
上次结果
|
||
</n-button>
|
||
|
||
<!-- 评价弹窗 -->
|
||
<n-modal
|
||
preset="card"
|
||
title="恭喜你成功提交,说说你对这道题的感受吧"
|
||
:mask-closable="false"
|
||
:closable="false"
|
||
:close-on-esc="false"
|
||
:style="{ maxWidth: isDesktop && '50vw', maxHeight: '80vh' }"
|
||
v-model:show="commentPanel"
|
||
>
|
||
<ProblemReaction @submitted="closeCommentPanel" />
|
||
</n-modal>
|
||
</template>
|