diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts index 3b5808e..c9a793e 100644 --- a/electron/main/ipc.ts +++ b/electron/main/ipc.ts @@ -354,71 +354,44 @@ async function callAiApi(imageBase64: string, rubric: string, config: { apiKey: deductions: string[] comment: string }> { - const systemRole = `你是高校阅卷专家,严格按照评分标准逐项评分。你必须只输出JSON,严禁包含\`\`\`markdown代码块、\`\`\`json标记、注释、说明文字或任何其他非JSON内容。输出必须以{开头,以}结尾,且必须是合法的JSON。` + const systemRole = `你是一名高校阅卷老师,根据评分标准和答案对学生答案评分。请输出JSON格式的结果。` const hasRefImage = !!extras?.referenceAnswerImage const prompt = hasRefImage - ? `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】 -${rubric || '无'} + ? `题目:${extras?.questionTitle || ''} +评分标准:${rubric || '无'} +参考答案:${extras?.referenceAnswer || '无'} +满分:${extras?.maxScore ?? 100} -【参考答案】 -${extras?.referenceAnswer || '无'} +图片1是标准答案,图片2是学生答案。 +请根据评分标准和标准答案(图片1)给学生答案(图片2)评分。 -【满分】 -${extras?.maxScore ?? 100} +评分规则: +1. 逐项评分,各项得分之和不超过满分${extras?.maxScore ?? 100} +2. 空白或无关答案给0分 +3. 部分正确酌情给分 -图片1:标准答案 -图片2:学生答案 +请输出JSON: +{"score":分数,"confidence":置信度0~1,"deductions":["扣分项"],"comment":"评语"} -请根据评分标准和标准答案(图片1)对学生答案(图片2)评分。 +只输出JSON,不要包含其他文字和markdown标记。` + : `题目:${extras?.questionTitle || ''} +评分标准:${rubric || '无'} +参考答案:${extras?.referenceAnswer || '无'} +满分:${extras?.maxScore ?? 100} ---- +图片是学生答案,请根据评分标准评分。 -【评分规则】 -1. 根据评分标准中的各项指标逐项评分,每项给出得分和说明 -2. 各评分项得分之和不得超过满分${extras?.maxScore ?? 100}分 -3. 如果学生答案为空、空白或明显与题目无关,直接给0分 -4. 部分正确时按评分标准酌情给分 +评分规则: +1. 逐项评分,各项得分之和不超过满分${extras?.maxScore ?? 100} +2. 空白或无关答案给0分 +3. 部分正确酌情给分 -【输出要求】 -你必须严格输出以下格式的纯JSON,严禁包含\`\`\`、\`\`\`json、markdown代码块、注释或任何说明文字。整个回复必须是合法的JSON对象,以{开始,以}结束: -{ - "score": <总分>, - "confidence": <置信度0~1>, - "deductions": ["扣分项说明1", "扣分项说明2"], - "comment": "总评语及逐项得分说明" -} -再次强调:只输出JSON,禁止任何其他内容。` - : `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】 -${rubric || '无'} +请输出JSON: +{"score":分数,"confidence":置信度0~1,"deductions":["扣分项"],"comment":"评语"} -【参考答案】 -${extras?.referenceAnswer || '无'} - -【满分】 -${extras?.maxScore ?? 100} - -图片:学生答案 -请根据评分标准对学生答案评分。 - ---- - -【评分规则】 -1. 根据评分标准中的各项指标逐项评分,每项给出得分和说明 -2. 各评分项得分之和不得超过满分${extras?.maxScore ?? 100}分 -3. 如果学生答案为空、空白或明显与题目无关,直接给0分 -4. 部分正确时按评分标准酌情给分 - -【输出要求】 -你必须严格输出以下格式的纯JSON,严禁包含\`\`\`、\`\`\`json、markdown代码块、注释或任何说明文字。整个回复必须是合法的JSON对象,以{开始,以}结束: -{ - "score": <总分>, - "confidence": <置信度0~1>, - "deductions": ["扣分项说明1", "扣分项说明2"], - "comment": "总评语及逐项得分说明" -} -再次强调:只输出JSON,禁止任何其他内容。` +只输出JSON,不要包含其他文字和markdown标记。` const axios = require('axios') diff --git a/src/components/FloatingToolbar.vue b/src/components/FloatingToolbar.vue index d8c36c3..3245411 100644 --- a/src/components/FloatingToolbar.vue +++ b/src/components/FloatingToolbar.vue @@ -30,6 +30,10 @@ 📜 历史 + + + 收起 + @@ -46,6 +50,7 @@ import { ElMessage } from 'element-plus' import { useAppStore } from '@/stores/app' import { useGradingStore } from '@/stores/grading' import { useSettingsStore } from '@/stores/settings' +import { addWatermark } from '@/utils/image' const appStore = useAppStore() const gradingStore = useGradingStore() @@ -69,9 +74,9 @@ let cleanupScreenshotListener: (() => void) | null = null onMounted(() => { resizeWindow() if (window.electronAPI) { - cleanupScreenshotListener = window.electronAPI.screenshot.onCompleted((base64: string) => { + cleanupScreenshotListener = window.electronAPI.screenshot.onCompleted(async (base64: string) => { if (!appStore.isGrading) { - gradingStore.setImage(base64) + gradingStore.setImage(await addWatermark(base64, '学生答案')) startGrading() } }) @@ -90,7 +95,7 @@ async function selectImage() { if (!window.electronAPI) return const data = await window.electronAPI.dialog.openImage() if (data) { - gradingStore.setImage(data.base64) + gradingStore.setImage(await addWatermark(data.base64, '学生答案')) startGrading() } } @@ -206,6 +211,13 @@ function openSettings() { window.electronAPI?.dialog.open('settings') } .sub-btn { padding: 4px 8px !important; } +.collapse-btn { + padding: 4px 6px !important; + color: rgba(255, 255, 255, 0.5) !important; +} +.collapse-btn:hover { + color: rgba(255, 255, 255, 0.9) !important; +} .toolbar-btn { -webkit-app-region: no-drag; diff --git a/src/components/ResultDrawer.vue b/src/components/ResultDrawer.vue index 282212b..fc2ae8c 100644 --- a/src/components/ResultDrawer.vue +++ b/src/components/ResultDrawer.vue @@ -25,9 +25,15 @@
当前评分标准
{{ currentQuestion.rubric }}
-
+ +
+
学生答案图片
+ +
+ +
参考答案
-
{{ currentQuestion.referenceAnswer }}
+
{{ currentQuestion.referenceAnswer }}
@@ -355,7 +361,7 @@ async function confirmScore() { border-radius: 6px; white-space: pre-wrap; } -.ref-image { +.ref-image, .student-image { max-width: 100%; max-height: 300px; border-radius: 6px; diff --git a/src/components/RubricEditor.vue b/src/components/RubricEditor.vue index d8ff8fc..173da9a 100644 --- a/src/components/RubricEditor.vue +++ b/src/components/RubricEditor.vue @@ -28,7 +28,7 @@
@@ -61,6 +61,7 @@ import { ElMessage } from 'element-plus' import { useAppStore } from '@/stores/app' import { useGradingStore } from '@/stores/grading' import type { QuestionItem } from '@/types' +import { addWatermark } from '@/utils/image' const appStore = useAppStore() const gradingStore = useGradingStore() @@ -142,7 +143,7 @@ async function uploadRefImage(idx: number) { if (!window.electronAPI) return const data = await window.electronAPI.dialog.openImage() if (data) { - questions.value[idx].referenceAnswerImage = `data:${data.mime};base64,${data.base64}` + questions.value[idx].referenceAnswerImage = `data:image/${data.mime};base64,${await addWatermark(data.base64, '参考答案')}` } } diff --git a/src/utils/image.ts b/src/utils/image.ts new file mode 100644 index 0000000..1f489d4 --- /dev/null +++ b/src/utils/image.ts @@ -0,0 +1,21 @@ +export function addWatermark(base64: string, text: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = img.width + canvas.height = img.height + const ctx = canvas.getContext('2d') + if (!ctx) return reject(new Error('Canvas 2D context not available')) + ctx.drawImage(img, 0, 0) + ctx.font = `bold ${Math.max(28, Math.round(img.width / 20))}px sans-serif` + ctx.fillStyle = 'rgba(255, 0, 0, 0.85)' + ctx.textBaseline = 'top' + const padding = Math.max(8, Math.round(img.width / 60)) + ctx.fillText(text, padding, padding) + resolve(canvas.toDataURL('image/png').replace(/^data:image\/png;base64,/, '')) + } + img.onerror = () => reject(new Error('Failed to load image for watermark')) + img.src = `data:image/png;base64,${base64}` + }) +}