AI 时代 OJ 设计的 2a:先把现有提示记下来,为后面的分级和两段式诊断攒对照数据。
提示本身的行为不变,发给模型的 prompt 一字未改。
- 迁移 0017 建 ai_hint:模型、prompt 版本、内容、错误、耗时、学生评价;
成功和失败都记(失败率是 AI 悄悄变差时最先动的数)。挂在 submission 上 CASCADE
- streamChat 第三个参数改成 { onComplete, onError }:onComplete 的返回值并进
done 事件,提示 id 由此带回前端;班级学情分析那处调用同步改写,行为不变
- 落库失败只记日志,不影响学生拿到提示
- 新增 POST /ai/hint/:id/feedback:只能评自己的提示(别人的一律 404),可以改票
- 前端提示生成完出「有帮助 / 没帮助」,选中的高亮;后端没存上时不出按钮
- 实跑:本地假 LLM 验证成功 / provider 断开 / 缺 AI_KEY 三条路径的落库,
评价接口六种请求,浏览器里按钮出现、改票、重复点不发请求
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
type AiAnalysisRecord,
|
||||
type AiHintFeedbackRequest,
|
||||
type Contest as OjContest,
|
||||
type ContestAccess,
|
||||
type ContestList,
|
||||
@@ -380,6 +381,13 @@ export function getAIPinnedReport() {
|
||||
return api.get<AiAnalysisRecord | null>("ai/pinned")
|
||||
}
|
||||
|
||||
/** 学生评价一条 AI 提示。id 来自 /ai/hint 流的 done 事件,可以改票 */
|
||||
export function submitHintFeedback(hintId: number, helpful: boolean) {
|
||||
return api.post<null>(`ai/hint/${hintId}/feedback`, {
|
||||
helpful,
|
||||
} satisfies AiHintFeedbackRequest)
|
||||
}
|
||||
|
||||
// ==================== 相似题目推荐 ====================
|
||||
|
||||
export function getSimilarProblems(problemId: string) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { Submission } from "utils/types"
|
||||
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { aiStreamError, consumeJSONEventStream } from "utils/stream"
|
||||
import { submitHintFeedback } from "oj/api"
|
||||
import { MdPreview } from "md-editor-v3"
|
||||
import "md-editor-v3/lib/preview.css"
|
||||
import { useDark } from "@vueuse/core"
|
||||
@@ -31,6 +32,10 @@ const theme = useThemeVars()
|
||||
const hintContent = ref("")
|
||||
const hintLoading = ref(false)
|
||||
const hintError = ref("")
|
||||
// 这条提示在 ai_hint 里的 id,生成完由 done 事件带回来;后端落库失败时没有,就不出评价按钮
|
||||
const hintId = ref<number | null>(null)
|
||||
const hintHelpful = ref<boolean | null>(null)
|
||||
const hintFeedbackSending = ref(false)
|
||||
|
||||
// 错误信息格式化
|
||||
const msg = computed(() => {
|
||||
@@ -95,6 +100,8 @@ watch(
|
||||
hintContent.value = ""
|
||||
hintError.value = ""
|
||||
hintLoading.value = false
|
||||
hintId.value = null
|
||||
hintHelpful.value = null
|
||||
},
|
||||
)
|
||||
|
||||
@@ -102,6 +109,8 @@ async function fetchHint(submissionId: string) {
|
||||
hintLoading.value = true
|
||||
hintContent.value = ""
|
||||
hintError.value = ""
|
||||
hintId.value = null
|
||||
hintHelpful.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/ai/hint", {
|
||||
@@ -117,9 +126,12 @@ async function fetchHint(submissionId: string) {
|
||||
type: string
|
||||
content?: string
|
||||
message?: string
|
||||
hintId?: number
|
||||
}) => {
|
||||
if (data.type === "delta" && data.content) {
|
||||
hintContent.value += data.content
|
||||
} else if (data.type === "done") {
|
||||
hintId.value = data.hintId ?? null
|
||||
} else if (data.type === "error") {
|
||||
hintError.value = data.message || "AI 提示生成失败"
|
||||
}
|
||||
@@ -132,6 +144,22 @@ async function fetchHint(submissionId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 可以改票:点另一个就覆盖。失败了不打扰学生,按钮恢复原样就行 ——
|
||||
// 评价是给我们看的,不值得为它弹一条报错
|
||||
async function sendHintFeedback(helpful: boolean) {
|
||||
if (hintId.value === null || hintFeedbackSending.value) return
|
||||
if (hintHelpful.value === helpful) return
|
||||
hintFeedbackSending.value = true
|
||||
try {
|
||||
await submitHintFeedback(hintId.value, helpful)
|
||||
hintHelpful.value = helpful
|
||||
} catch {
|
||||
// 静默
|
||||
} finally {
|
||||
hintFeedbackSending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用例表格数据(只在部分通过时显示)
|
||||
const infoTable = computed(() => {
|
||||
const submission = props.submission
|
||||
@@ -254,6 +282,30 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
|
||||
preview-theme="vuepress"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
/>
|
||||
<n-flex
|
||||
v-if="hintId !== null && !hintLoading"
|
||||
align="center"
|
||||
size="small"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
<n-text depth="3">这条提示对你有帮助吗?</n-text>
|
||||
<n-button
|
||||
size="tiny"
|
||||
:type="hintHelpful === true ? 'primary' : 'default'"
|
||||
:disabled="hintFeedbackSending"
|
||||
@click="sendHintFeedback(true)"
|
||||
>
|
||||
有帮助
|
||||
</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
:type="hintHelpful === false ? 'warning' : 'default'"
|
||||
:disabled="hintFeedbackSending"
|
||||
@click="sendHintFeedback(false)"
|
||||
>
|
||||
没帮助
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user