diff --git a/internal/handlers/practice_handler.go b/internal/handlers/practice_handler.go index 96fd2dd..979c720 100644 --- a/internal/handlers/practice_handler.go +++ b/internal/handlers/practice_handler.go @@ -175,30 +175,62 @@ func SubmitPracticeAnswer(c *gin.Context) { // AI评分结果(仅简答题) var aiGrading *models.AIGrading = nil - // 对简答题使用AI评分 + // 对简答题使用AI评分(必须成功,失败重试最多5次) if question.Type == "short-answer" { // 获取用户答案字符串 userAnswerStr, ok := submit.Answer.(string) - if ok { - // 获取标准答案字符串 - standardAnswerStr, ok := correctAnswer.(string) - if ok { - // 调用AI评分服务 - aiService := services.NewAIGradingService() - aiResult, err := aiService.GradeShortAnswer(question.Question, standardAnswerStr, userAnswerStr) - if err != nil { - // AI评分失败时记录日志,但不影响主流程 - log.Printf("AI评分失败: %v", err) - } else { - // 使用AI的评分结果 - correct = aiResult.IsCorrect - aiGrading = &models.AIGrading{ - Score: aiResult.Score, - Feedback: aiResult.Feedback, - Suggestion: aiResult.Suggestion, - } - } + if !ok { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "简答题答案格式错误", + }) + return + } + + // 获取标准答案字符串 + standardAnswerStr, ok := correctAnswer.(string) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "题目答案格式错误", + }) + return + } + + // 调用AI评分服务(带重试机制) + aiService := services.NewAIGradingService() + var aiResult *services.AIGradingResult + var err error + maxRetries := 5 + + for attempt := 1; attempt <= maxRetries; attempt++ { + log.Printf("AI评分尝试第 %d 次 (题目ID: %d)", attempt, question.ID) + aiResult, err = aiService.GradeShortAnswer(question.Question, standardAnswerStr, userAnswerStr) + if err == nil { + log.Printf("AI评分成功 (题目ID: %d, 得分: %.1f)", question.ID, aiResult.Score) + break } + log.Printf("AI评分失败 (第 %d 次尝试): %v", attempt, err) + if attempt < maxRetries { + // 等待一小段时间后重试(指数退避) + time.Sleep(time.Second * time.Duration(attempt)) + } + } + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": fmt.Sprintf("AI评分服务暂时不可用,已重试%d次,请稍后再试", maxRetries), + }) + return + } + + // 使用AI的评分结果 + correct = aiResult.IsCorrect + aiGrading = &models.AIGrading{ + Score: aiResult.Score, + Feedback: aiResult.Feedback, + Suggestion: aiResult.Suggestion, } } diff --git a/internal/services/ai_grading.go b/internal/services/ai_grading.go index e64c0f7..e011362 100644 --- a/internal/services/ai_grading.go +++ b/internal/services/ai_grading.go @@ -47,8 +47,8 @@ type AIGradingResult struct { // standardAnswer: 标准答案 // userAnswer: 用户答案 func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) { - // 构建评分提示词 - prompt := fmt.Sprintf(`你是一位专业的阅卷老师,请对以下简答题进行评分。 + // 构建评分提示词(严格评分模式) + prompt := fmt.Sprintf(`你是一位严格的阅卷老师,请严格按照标准答案对以下简答题进行评分。 题目:%s @@ -56,21 +56,29 @@ func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer 学生答案:%s -请按照以下要求进行评分: -1. 给出一个0-100的分数 -2. 判断答案是否正确(60分及以上为正确) -3. 给出简短的评语(不超过50字) -4. 如果答案不完善,给出改进建议(不超过50字,如果答案很好可以为空) +评分标准(请严格遵守): +1. 必须与标准答案进行逐项对比 +2. 答案要点完全覆盖标准答案且表述准确的,给85-100分 +3. 答案要点基本覆盖但有缺漏或表述不够准确的,给60-84分 +4. 答案要点缺失较多或有明显错误的,给40-59分 +5. 答案完全错误或离题的,给0-39分 +6. 判断标准:60分及以上为正确(is_correct: true),否则为错误(is_correct: false) + +评分要求: +1. 给出一个0-100的精确分数 +2. 判断答案是否正确(is_correct: 60分及以上为true,否则为false) +3. 给出简短的评语(说明得分和失分原因,不超过50字) +4. 给出具体的改进建议(如果答案满分可以为空,否则必须指出具体改进方向,不超过50字) 请按照以下JSON格式返回结果: { "score": 85, "is_correct": true, - "feedback": "答案基本正确,要点全面", - "suggestion": "可以补充一些具体的例子" + "feedback": "答案覆盖了主要要点,但XXX部分描述不够准确", + "suggestion": "建议补充XXX内容,并完善XXX的描述" } -注意:只返回JSON格式的结果,不要有其他内容。`, question, standardAnswer, userAnswer) +注意:只返回JSON格式的结果,不要有其他内容。必须严格对照标准答案评分,不要过于宽松。`, question, standardAnswer, userAnswer) // 调用AI API resp, err := s.client.CreateChatCompletion( @@ -80,14 +88,14 @@ func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleSystem, - Content: "你是一位专业的阅卷老师,擅长对简答题进行公正、客观的评分。", + Content: "你是一位严格的阅卷老师,必须严格按照标准答案进行评分,不能过于宽松。你的评分标准是客观的、一致的、可预测的。", }, { Role: openai.ChatMessageRoleUser, Content: prompt, }, }, - Temperature: 0.3, // 较低的温度以获得更稳定的评分结果 + Temperature: 0, // 温度为0,获得最确定、最一致的评分结果 }, ) diff --git a/web/src/pages/Question.tsx b/web/src/pages/Question.tsx index 1c36c7d..1a6433f 100644 --- a/web/src/pages/Question.tsx +++ b/web/src/pages/Question.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { useSearchParams, useNavigate } from "react-router-dom"; -import { Button, message, Typography } from "antd"; -import { ArrowLeftOutlined } from "@ant-design/icons"; +import { Button, message, Typography, Switch, InputNumber, Popover, Space } from "antd"; +import { ArrowLeftOutlined, SettingOutlined } from "@ant-design/icons"; import type { Question, AnswerResult } from "../types/question"; import * as questionApi from "../api/question"; import QuestionProgress from "../components/QuestionProgress"; @@ -28,6 +28,33 @@ const QuestionPage: React.FC = () => { const [wrongCount, setWrongCount] = useState(0); const [showSummary, setShowSummary] = useState(false); + // 自动跳转开关(默认开启) + const [autoNext, setAutoNext] = useState(() => { + const saved = localStorage.getItem('autoNextEnabled'); + return saved !== null ? saved === 'true' : true; + }); + + // 自动跳转延迟时间(秒,默认2秒) + const [autoNextDelay, setAutoNextDelay] = useState(() => { + const saved = localStorage.getItem('autoNextDelay'); + return saved !== null ? parseInt(saved, 10) : 2; + }); + + // 切换自动跳转开关 + const toggleAutoNext = () => { + const newValue = !autoNext; + setAutoNext(newValue); + localStorage.setItem('autoNextEnabled', String(newValue)); + }; + + // 修改自动跳转延迟时间 + const handleDelayChange = (value: number | null) => { + if (value !== null && value >= 1 && value <= 10) { + setAutoNextDelay(value); + localStorage.setItem('autoNextDelay', String(value)); + } + }; + // 从localStorage恢复答题进度 const getStorageKey = () => { const type = searchParams.get("type"); @@ -180,13 +207,13 @@ const QuestionPage: React.FC = () => { saveProgress(currentIndex, correctCount, newWrong); } - // 如果答案正确,1秒后自动进入下一题 - if (res.data.correct) { + // 如果答案正确且开启自动跳转,根据设置的延迟时间后自动进入下一题 + if (res.data.correct && autoNext) { setAutoNextLoading(true); setTimeout(() => { setAutoNextLoading(false); handleNext(); - }, 1000); + }, autoNextDelay * 1000); // 将秒转换为毫秒 } } } catch (error) { @@ -279,6 +306,53 @@ const QuestionPage: React.FC = () => { 错误 {wrongCount} + {/* 设置按钮 */} + + +
+
自动下一题
+
+ + + {autoNext ? '已开启' : '已关闭'} + +
+
+ {autoNext && ( +
+
延迟时间
+ + + 秒后跳转 + +
+ )} +
+ + } + title="答题设置" + trigger="click" + placement="bottomRight" + > +