feat: 图片水印+提示词精简+收起按钮
This commit is contained in:
+26
-53
@@ -354,71 +354,44 @@ async function callAiApi(imageBase64: string, rubric: string, config: { apiKey:
|
|||||||
deductions: string[]
|
deductions: string[]
|
||||||
comment: string
|
comment: string
|
||||||
}> {
|
}> {
|
||||||
const systemRole = `你是高校阅卷专家,严格按照评分标准逐项评分。你必须只输出JSON,严禁包含\`\`\`markdown代码块、\`\`\`json标记、注释、说明文字或任何其他非JSON内容。输出必须以{开头,以}结尾,且必须是合法的JSON。`
|
const systemRole = `你是一名高校阅卷老师,根据评分标准和答案对学生答案评分。请输出JSON格式的结果。`
|
||||||
|
|
||||||
const hasRefImage = !!extras?.referenceAnswerImage
|
const hasRefImage = !!extras?.referenceAnswerImage
|
||||||
|
|
||||||
const prompt = hasRefImage
|
const prompt = hasRefImage
|
||||||
? `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】
|
? `题目:${extras?.questionTitle || ''}
|
||||||
${rubric || '无'}
|
评分标准:${rubric || '无'}
|
||||||
|
参考答案:${extras?.referenceAnswer || '无'}
|
||||||
|
满分:${extras?.maxScore ?? 100}
|
||||||
|
|
||||||
【参考答案】
|
图片1是标准答案,图片2是学生答案。
|
||||||
${extras?.referenceAnswer || '无'}
|
请根据评分标准和标准答案(图片1)给学生答案(图片2)评分。
|
||||||
|
|
||||||
【满分】
|
评分规则:
|
||||||
${extras?.maxScore ?? 100}
|
1. 逐项评分,各项得分之和不超过满分${extras?.maxScore ?? 100}
|
||||||
|
2. 空白或无关答案给0分
|
||||||
|
3. 部分正确酌情给分
|
||||||
|
|
||||||
图片1:标准答案
|
请输出JSON:
|
||||||
图片2:学生答案
|
{"score":分数,"confidence":置信度0~1,"deductions":["扣分项"],"comment":"评语"}
|
||||||
|
|
||||||
请根据评分标准和标准答案(图片1)对学生答案(图片2)评分。
|
只输出JSON,不要包含其他文字和markdown标记。`
|
||||||
|
: `题目:${extras?.questionTitle || ''}
|
||||||
|
评分标准:${rubric || '无'}
|
||||||
|
参考答案:${extras?.referenceAnswer || '无'}
|
||||||
|
满分:${extras?.maxScore ?? 100}
|
||||||
|
|
||||||
---
|
图片是学生答案,请根据评分标准评分。
|
||||||
|
|
||||||
【评分规则】
|
评分规则:
|
||||||
1. 根据评分标准中的各项指标逐项评分,每项给出得分和说明
|
1. 逐项评分,各项得分之和不超过满分${extras?.maxScore ?? 100}
|
||||||
2. 各评分项得分之和不得超过满分${extras?.maxScore ?? 100}分
|
2. 空白或无关答案给0分
|
||||||
3. 如果学生答案为空、空白或明显与题目无关,直接给0分
|
3. 部分正确酌情给分
|
||||||
4. 部分正确时按评分标准酌情给分
|
|
||||||
|
|
||||||
【输出要求】
|
请输出JSON:
|
||||||
你必须严格输出以下格式的纯JSON,严禁包含\`\`\`、\`\`\`json、markdown代码块、注释或任何说明文字。整个回复必须是合法的JSON对象,以{开始,以}结束:
|
{"score":分数,"confidence":置信度0~1,"deductions":["扣分项"],"comment":"评语"}
|
||||||
{
|
|
||||||
"score": <总分>,
|
|
||||||
"confidence": <置信度0~1>,
|
|
||||||
"deductions": ["扣分项说明1", "扣分项说明2"],
|
|
||||||
"comment": "总评语及逐项得分说明"
|
|
||||||
}
|
|
||||||
再次强调:只输出JSON,禁止任何其他内容。`
|
|
||||||
: `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】
|
|
||||||
${rubric || '无'}
|
|
||||||
|
|
||||||
【参考答案】
|
只输出JSON,不要包含其他文字和markdown标记。`
|
||||||
${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,禁止任何其他内容。`
|
|
||||||
|
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,10 @@
|
|||||||
<span class="btn-icon">📜</span>
|
<span class="btn-icon">📜</span>
|
||||||
<span class="btn-label">历史</span>
|
<span class="btn-label">历史</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button class="toolbar-btn collapse-btn" @click="showMenu = false">
|
||||||
|
<span class="btn-icon">↩</span>
|
||||||
|
<span class="btn-label">收起</span>
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-button class="toolbar-btn" @click="openSettings">
|
<el-button class="toolbar-btn" @click="openSettings">
|
||||||
@@ -46,6 +50,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useGradingStore } from '@/stores/grading'
|
import { useGradingStore } from '@/stores/grading'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { addWatermark } from '@/utils/image'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const gradingStore = useGradingStore()
|
const gradingStore = useGradingStore()
|
||||||
@@ -69,9 +74,9 @@ let cleanupScreenshotListener: (() => void) | null = null
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
resizeWindow()
|
resizeWindow()
|
||||||
if (window.electronAPI) {
|
if (window.electronAPI) {
|
||||||
cleanupScreenshotListener = window.electronAPI.screenshot.onCompleted((base64: string) => {
|
cleanupScreenshotListener = window.electronAPI.screenshot.onCompleted(async (base64: string) => {
|
||||||
if (!appStore.isGrading) {
|
if (!appStore.isGrading) {
|
||||||
gradingStore.setImage(base64)
|
gradingStore.setImage(await addWatermark(base64, '学生答案'))
|
||||||
startGrading()
|
startGrading()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -90,7 +95,7 @@ async function selectImage() {
|
|||||||
if (!window.electronAPI) return
|
if (!window.electronAPI) return
|
||||||
const data = await window.electronAPI.dialog.openImage()
|
const data = await window.electronAPI.dialog.openImage()
|
||||||
if (data) {
|
if (data) {
|
||||||
gradingStore.setImage(data.base64)
|
gradingStore.setImage(await addWatermark(data.base64, '学生答案'))
|
||||||
startGrading()
|
startGrading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,6 +211,13 @@ function openSettings() { window.electronAPI?.dialog.open('settings') }
|
|||||||
.sub-btn {
|
.sub-btn {
|
||||||
padding: 4px 8px !important;
|
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 {
|
.toolbar-btn {
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
|
|||||||
@@ -25,9 +25,15 @@
|
|||||||
<div class="section-title">当前评分标准</div>
|
<div class="section-title">当前评分标准</div>
|
||||||
<div class="rubric-text">{{ currentQuestion.rubric }}</div>
|
<div class="rubric-text">{{ currentQuestion.rubric }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-section" v-if="currentQuestion.referenceAnswer">
|
|
||||||
|
<div class="result-section" v-if="gradingStore.currentImage">
|
||||||
|
<div class="section-title">学生答案图片</div>
|
||||||
|
<img class="student-image" :src="'data:image/png;base64,' + gradingStore.currentImage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-section" v-if="currentQuestion.referenceAnswer || currentQuestion.referenceAnswerImage">
|
||||||
<div class="section-title">参考答案</div>
|
<div class="section-title">参考答案</div>
|
||||||
<div class="reference-text">{{ currentQuestion.referenceAnswer }}</div>
|
<div class="reference-text" v-if="currentQuestion.referenceAnswer">{{ currentQuestion.referenceAnswer }}</div>
|
||||||
<img v-if="currentQuestion.referenceAnswerImage" class="ref-image" :src="currentQuestion.referenceAnswerImage" />
|
<img v-if="currentQuestion.referenceAnswerImage" class="ref-image" :src="currentQuestion.referenceAnswerImage" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -355,7 +361,7 @@ async function confirmScore() {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
.ref-image {
|
.ref-image, .student-image {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<div class="ref-image-section">
|
<div class="ref-image-section">
|
||||||
<template v-if="questions[activeIndex].referenceAnswerImage">
|
<template v-if="questions[activeIndex].referenceAnswerImage">
|
||||||
<div class="ref-image-preview">
|
<div class="ref-image-preview">
|
||||||
<img :src="'data:image/png;base64,' + questions[activeIndex].referenceAnswerImage" />
|
<img :src="questions[activeIndex].referenceAnswerImage" />
|
||||||
<el-button size="small" type="danger" text @click="removeRefImage(activeIndex)">删除图片</el-button>
|
<el-button size="small" type="danger" text @click="removeRefImage(activeIndex)">删除图片</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -61,6 +61,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useGradingStore } from '@/stores/grading'
|
import { useGradingStore } from '@/stores/grading'
|
||||||
import type { QuestionItem } from '@/types'
|
import type { QuestionItem } from '@/types'
|
||||||
|
import { addWatermark } from '@/utils/image'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const gradingStore = useGradingStore()
|
const gradingStore = useGradingStore()
|
||||||
@@ -142,7 +143,7 @@ async function uploadRefImage(idx: number) {
|
|||||||
if (!window.electronAPI) return
|
if (!window.electronAPI) return
|
||||||
const data = await window.electronAPI.dialog.openImage()
|
const data = await window.electronAPI.dialog.openImage()
|
||||||
if (data) {
|
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, '参考答案')}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export function addWatermark(base64: string, text: string): Promise<string> {
|
||||||
|
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}`
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user