Initial commit: secondary school grade archive system.
Add FastAPI/React app with Docker deployment, Ubuntu one-click install, and docs for junior/senior high score tracking and mistake bank. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import { Alert, Button, Col, Input, Modal, Row, Space, Spin, Typography, message } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { wrongQuestionApi } from '../api/client'
|
||||
import type { WrongQuestion } from '../types'
|
||||
import { STATUS_LABELS } from '../types'
|
||||
|
||||
interface Props {
|
||||
questionId: string
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onUpdated: () => void
|
||||
}
|
||||
|
||||
export default function WrongQuestionDetail({ questionId, open, onClose, onUpdated }: Props) {
|
||||
const [wq, setWq] = useState<WrongQuestion | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [questionText, setQuestionText] = useState('')
|
||||
const [solutionText, setSolutionText] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [regenerating, setRegenerating] = useState(false)
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data } = await wrongQuestionApi.get(questionId)
|
||||
setWq(data)
|
||||
setQuestionText(data.question_text || '')
|
||||
setSolutionText(data.solution_text || '')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open && questionId) load()
|
||||
}, [open, questionId])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
await wrongQuestionApi.update(questionId, {
|
||||
question_text: questionText,
|
||||
solution_text: solutionText,
|
||||
})
|
||||
message.success('已保存')
|
||||
onUpdated()
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
setRegenerating(true)
|
||||
try {
|
||||
const { data } = await wrongQuestionApi.regenerate(questionId)
|
||||
setWq(data)
|
||||
setQuestionText(data.question_text || '')
|
||||
setSolutionText(data.solution_text || '')
|
||||
message.success('解法已重新生成')
|
||||
onUpdated()
|
||||
} catch {
|
||||
message.error('生成失败,请确认 Ollama 已启动')
|
||||
} finally {
|
||||
setRegenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetryOcr = async () => {
|
||||
await wrongQuestionApi.retryOcr(questionId)
|
||||
message.info('已重新识别,请稍后刷新')
|
||||
onUpdated()
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={wq ? `${wq.subject_name} · 错题详情` : '错题详情'}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
width="90%"
|
||||
style={{ maxWidth: 960 }}
|
||||
footer={
|
||||
<Space wrap>
|
||||
<Button onClick={handleRetryOcr}>重新 OCR</Button>
|
||||
<Button loading={regenerating} onClick={handleRegenerate}>
|
||||
重新生成解法
|
||||
</Button>
|
||||
<Button type="primary" loading={saving} onClick={handleSave}>
|
||||
保存编辑
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
{wq && (
|
||||
<>
|
||||
<Typography.Text type="secondary">状态:{STATUS_LABELS[wq.status]}</Typography.Text>
|
||||
{wq.solution_text && (
|
||||
<Alert
|
||||
message="AI 生成内容,请核对后再使用"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ margin: '12px 0' }}
|
||||
/>
|
||||
)}
|
||||
<Row gutter={16} style={{ marginTop: 12 }}>
|
||||
<Col xs={24} md={10}>
|
||||
<img
|
||||
src={wrongQuestionApi.imageUrl(wq.id)}
|
||||
alt="原题"
|
||||
style={{ width: '100%', borderRadius: 8, border: '1px solid #f0f0f0' }}
|
||||
/>
|
||||
{wq.ocr_raw_text && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Typography.Text strong>OCR 原文</Typography.Text>
|
||||
<pre
|
||||
style={{
|
||||
background: '#fafafa',
|
||||
padding: 8,
|
||||
fontSize: 12,
|
||||
maxHeight: 150,
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{wq.ocr_raw_text}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col xs={24} md={14}>
|
||||
<Typography.Text strong>识别题目(可编辑)</Typography.Text>
|
||||
<Input.TextArea
|
||||
rows={6}
|
||||
value={questionText}
|
||||
onChange={(e) => setQuestionText(e.target.value)}
|
||||
style={{ marginTop: 8, marginBottom: 16 }}
|
||||
/>
|
||||
<Typography.Text strong>解法</Typography.Text>
|
||||
<Input.TextArea
|
||||
rows={8}
|
||||
value={solutionText}
|
||||
onChange={(e) => setSolutionText(e.target.value)}
|
||||
style={{ marginTop: 8, marginBottom: 12 }}
|
||||
/>
|
||||
{solutionText && (
|
||||
<div style={{ background: '#fafafa', padding: 12, borderRadius: 8 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
预览
|
||||
</Typography.Text>
|
||||
<ReactMarkdown>{solutionText}</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user