feat: 参考答案图片上传 + AI提示词分有无标准答案图片两种流程
This commit is contained in:
+61
-13
@@ -64,8 +64,11 @@ function getDb() {
|
||||
max_score REAL NOT NULL DEFAULT 100,
|
||||
reference_answer TEXT NOT NULL DEFAULT '',
|
||||
rubric TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
reference_answer_image TEXT NOT NULL DEFAULT ''
|
||||
)`).run()
|
||||
// migrate: add column for existing DBs
|
||||
try { db.prepare('ALTER TABLE rubric ADD COLUMN reference_answer_image TEXT NOT NULL DEFAULT \'\'').run() } catch (_) {}
|
||||
}
|
||||
return db
|
||||
}
|
||||
@@ -120,17 +123,18 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
}, 100)
|
||||
})
|
||||
|
||||
ipcMain.handle('ai:grade', async (_event, { imageBase64, rubric, apiConfig, maxScore, referenceAnswer, questionTitle }: {
|
||||
ipcMain.handle('ai:grade', async (_event, { imageBase64, rubric, apiConfig, maxScore, referenceAnswer, questionTitle, referenceAnswerImage }: {
|
||||
imageBase64: string
|
||||
rubric: string
|
||||
apiConfig: { apiKey: string; baseUrl: string; model: string }
|
||||
maxScore?: number
|
||||
referenceAnswer?: string
|
||||
questionTitle?: string
|
||||
referenceAnswerImage?: string
|
||||
}) => {
|
||||
log.info('IPC: ai:grade')
|
||||
try {
|
||||
const result = await callAiApi(imageBase64, rubric, apiConfig, { maxScore, referenceAnswer, questionTitle })
|
||||
const result = await callAiApi(imageBase64, rubric, apiConfig, { maxScore, referenceAnswer, questionTitle, referenceAnswerImage })
|
||||
return result
|
||||
} catch (err: any) {
|
||||
log.error('AI grade failed:', err)
|
||||
@@ -263,10 +267,10 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
log.info(`rubric:storeData: ${questions.length} questions`, JSON.stringify(questions.map((q: any) => ({ title: q.questionTitle, rubricLen: q.rubric?.length }))))
|
||||
if (questions.length === 0) return
|
||||
d.prepare('DELETE FROM rubric').run()
|
||||
const insert = d.prepare('INSERT INTO rubric (question_title, max_score, reference_answer, rubric, sort_order) VALUES (?, ?, ?, ?, ?)')
|
||||
const insert = d.prepare('INSERT INTO rubric (question_title, max_score, reference_answer, rubric, sort_order, reference_answer_image) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const q = questions[i]
|
||||
insert.run(q.questionTitle || '', q.maxScore ?? 100, q.referenceAnswer || '', q.rubric || '', i)
|
||||
insert.run(q.questionTitle || '', q.maxScore ?? 100, q.referenceAnswer || '', q.rubric || '', i, q.referenceAnswerImage || '')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -280,7 +284,8 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
questionTitle: r.question_title,
|
||||
maxScore: r.max_score,
|
||||
referenceAnswer: r.reference_answer,
|
||||
rubric: r.rubric
|
||||
rubric: r.rubric,
|
||||
referenceAnswerImage: r.reference_answer_image || undefined
|
||||
})),
|
||||
currentIndex: 0
|
||||
}
|
||||
@@ -296,7 +301,8 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
questionTitle: r.question_title,
|
||||
maxScore: r.max_score,
|
||||
referenceAnswer: r.reference_answer,
|
||||
rubric: r.rubric
|
||||
rubric: r.rubric,
|
||||
referenceAnswerImage: r.reference_answer_image || undefined
|
||||
}))
|
||||
const result = await dialogBox.showSaveDialog({
|
||||
defaultPath: '评分标准.json',
|
||||
@@ -336,7 +342,7 @@ export function setupIpc(win: BrowserWindow): void {
|
||||
const dialogPayloads: Record<string, any> = {}
|
||||
let gradingPayload: any = null
|
||||
|
||||
async function callAiApi(imageBase64: string, rubric: string, config: { apiKey: string; baseUrl: string; model: string }, extras?: { maxScore?: number; referenceAnswer?: string; questionTitle?: string }): Promise<{
|
||||
async function callAiApi(imageBase64: string, rubric: string, config: { apiKey: string; baseUrl: string; model: string }, extras?: { maxScore?: number; referenceAnswer?: string; questionTitle?: string; referenceAnswerImage?: string }): Promise<{
|
||||
score: number
|
||||
confidence: number
|
||||
deductions: string[]
|
||||
@@ -344,7 +350,40 @@ async function callAiApi(imageBase64: string, rubric: string, config: { apiKey:
|
||||
}> {
|
||||
const systemRole = `你是高校阅卷专家,严格按照评分标准逐项评分。只输出JSON,不包含任何其他文字。`
|
||||
|
||||
const prompt = `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】
|
||||
const hasRefImage = !!extras?.referenceAnswerImage
|
||||
|
||||
const prompt = hasRefImage
|
||||
? `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】
|
||||
${rubric || '无'}
|
||||
|
||||
【参考答案】
|
||||
${extras?.referenceAnswer || '无'}
|
||||
|
||||
【满分】
|
||||
${extras?.maxScore ?? 100}
|
||||
|
||||
图片1:学生答案
|
||||
图片2:标准答案
|
||||
|
||||
请按照标准答案(图片2)给学生答案(图片1)评分。
|
||||
|
||||
---
|
||||
|
||||
【评分规则】
|
||||
1. 根据评分标准中的各项指标逐项评分,每项给出得分和说明
|
||||
2. 各评分项得分之和不得超过满分${extras?.maxScore ?? 100}分
|
||||
3. 如果学生答案为空、空白或明显与题目无关,直接给0分
|
||||
4. 部分正确时按评分标准酌情给分
|
||||
|
||||
【输出要求】
|
||||
只输出以下JSON,不要包含markdown代码块、\`\`\`标记、注释或任何其他文字:
|
||||
{
|
||||
"score": <总分>,
|
||||
"confidence": <置信度0~1>,
|
||||
"deductions": ["扣分项说明1", "扣分项说明2"],
|
||||
"comment": "总评语及逐项得分说明"
|
||||
}`
|
||||
: `${extras?.questionTitle ? `【题目】\n${extras.questionTitle}\n` : ''}【评分标准】
|
||||
${rubric || '无'}
|
||||
|
||||
【参考答案】
|
||||
@@ -374,6 +413,18 @@ ${extras?.maxScore ?? 100}
|
||||
}`
|
||||
|
||||
const axios = require('axios')
|
||||
|
||||
const userContent: any[] = hasRefImage
|
||||
? [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
||||
{ type: 'image_url', image_url: { url: `data:image/png;base64,${extras!.referenceAnswerImage}` } }
|
||||
]
|
||||
: [
|
||||
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
||||
{ type: 'text', text: prompt }
|
||||
]
|
||||
|
||||
const response = await axios.post(`${config.baseUrl}/chat/completions`, {
|
||||
model: config.model,
|
||||
messages: [
|
||||
@@ -383,10 +434,7 @@ ${extras?.maxScore ?? 100}
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'image_url', image_url: { url: `data:image/png;base64,${imageBase64}` } },
|
||||
{ type: 'text', text: prompt }
|
||||
]
|
||||
content: userContent
|
||||
}
|
||||
],
|
||||
max_tokens: 4096,
|
||||
|
||||
Reference in New Issue
Block a user