Files
secondary-school-grade-archive/frontend/src/pages/WrongQuestionDetail.tsx
T
dekun 43483bf56f 移动端拍照上传、奥数区、学段约束解题与 AI 模型配置。
- 手机/平板响应式布局,支持拍照与相册上传

- 学生详情新增奥数区,按初/高中学段生成解法并禁止超纲

- 系统设置可配置 Ollama 或 OpenAI 兼容 API

- 更新 frontend/dist 与使用说明
2026-06-28 13:39:54 +08:00

167 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('生成失败,请检查 AI 模型配置')
} finally {
setRegenerating(false)
}
}
const handleRetryOcr = async () => {
await wrongQuestionApi.retryOcr(questionId)
message.info('已重新识别,请稍后刷新')
onUpdated()
onClose()
}
return (
<Modal
title={
wq
? `${wq.subject_name}${wq.category === 'olympiad' ? ' · 奥数' : ''} · 详情`
: '详情'
}
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>
)
}