337 lines
10 KiB
TypeScript
337 lines
10 KiB
TypeScript
import { useState } from 'react'
|
||
import { Button, Card, Form, Input, message, Space, Switch } from 'antd'
|
||
import { PageHeader } from '@ant-design/pro-components'
|
||
import { checkAndCorrectSpelling } from '@/api/spellCheck'
|
||
|
||
interface CorrectionResult {
|
||
original: string
|
||
corrected: string
|
||
position: number
|
||
suggestions?: string[]
|
||
}
|
||
|
||
const SpellCheck = () => {
|
||
const [form] = Form.useForm()
|
||
const [isGenerating, setIsGenerating] = useState(false)
|
||
const [correctionResults, setCorrectionResults] = useState<CorrectionResult[]>([])
|
||
const [correctedText, setCorrectedText] = useState('')
|
||
const maxChars = 5000
|
||
|
||
const validateCustomVocab = (_: any, value: string) => {
|
||
if (!value) {
|
||
return Promise.resolve()
|
||
}
|
||
try {
|
||
const parsed = JSON.parse(value)
|
||
if (!Array.isArray(parsed)) {
|
||
return Promise.reject(new Error('必须是JSON数组格式'))
|
||
}
|
||
if (!parsed.every(item => typeof item === 'string')) {
|
||
return Promise.reject(new Error('数组元素必须为字符串'))
|
||
}
|
||
return Promise.resolve()
|
||
} catch (error) {
|
||
return Promise.reject(new Error('JSON格式无效'))
|
||
}
|
||
}
|
||
|
||
const handleGenerate = async () => {
|
||
try {
|
||
const formValues = await form.validateFields()
|
||
|
||
// 验证用户输入文本
|
||
if (!formValues.user_input_text?.trim()) {
|
||
message.warning('请输入需要检测错别字的文本')
|
||
return
|
||
}
|
||
|
||
// 验证文本长度
|
||
if (formValues.user_input_text.length > maxChars) {
|
||
message.warning(`输入文本不能超过${maxChars}个字符`)
|
||
return
|
||
}
|
||
|
||
setIsGenerating(true)
|
||
setCorrectionResults([])
|
||
setCorrectedText('')
|
||
|
||
let customVocabArray: string[] = []
|
||
if (formValues.custom_vocab) {
|
||
try {
|
||
customVocabArray = JSON.parse(formValues.custom_vocab)
|
||
} catch (error) {
|
||
message.error('自定义词汇库格式错误')
|
||
setIsGenerating(false)
|
||
return
|
||
}
|
||
}
|
||
|
||
const response = await checkAndCorrectSpelling(
|
||
formValues.user_input_text,
|
||
formValues.auto_correct || false,
|
||
customVocabArray
|
||
)
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text()
|
||
throw new Error(`检测请求失败: ${response.status} ${errorText}`)
|
||
}
|
||
|
||
if (!response.body) {
|
||
throw new Error('响应体为空')
|
||
}
|
||
|
||
const reader = response.body.getReader()
|
||
const decoder = new TextDecoder('utf-8')
|
||
let buffer = ''
|
||
let fullContent = ''
|
||
let results: CorrectionResult[] = []
|
||
|
||
try {
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
|
||
buffer += decoder.decode(value, { stream: true })
|
||
const lines = buffer.split('\n')
|
||
buffer = lines.pop() || ''
|
||
|
||
for (const line of lines) {
|
||
const trimmedLine = line.trim()
|
||
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
|
||
if (trimmedLine === 'data: [DONE]') {
|
||
message.success('检测完成')
|
||
break
|
||
}
|
||
continue
|
||
}
|
||
|
||
if (trimmedLine.startsWith('data: ')) {
|
||
try {
|
||
const data = trimmedLine.slice(6)
|
||
const parsed = JSON.parse(data)
|
||
|
||
if (parsed.event === 'message' && parsed.answer) {
|
||
fullContent += parsed.answer
|
||
// 尝试解析JSON结果
|
||
try {
|
||
results = JSON.parse(fullContent)
|
||
setCorrectionResults(results)
|
||
if (formValues.auto_correct) {
|
||
let corrected = formValues.user_input_text
|
||
results.forEach(item => {
|
||
if (item.corrected && item.original) {
|
||
corrected = corrected.replace(item.original, item.corrected)
|
||
}
|
||
})
|
||
setCorrectedText(corrected)
|
||
}
|
||
} catch (e) {
|
||
// 解析失败,继续等待
|
||
}
|
||
} else if (parsed.event === 'error') {
|
||
throw new Error(parsed.message || 'Dify API 返回错误')
|
||
}
|
||
} catch (parseError) {
|
||
console.warn('跳过无法解析的行:', trimmedLine)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
reader.releaseLock()
|
||
}
|
||
|
||
if (results.length === 0 && fullContent) {
|
||
try {
|
||
results = JSON.parse(fullContent)
|
||
setCorrectionResults(results)
|
||
if (formValues.auto_correct) {
|
||
let corrected = formValues.user_input_text
|
||
results.forEach(item => {
|
||
if (item.corrected && item.original) {
|
||
corrected = corrected.replace(item.original, item.corrected)
|
||
}
|
||
})
|
||
setCorrectedText(corrected)
|
||
}
|
||
} catch (e) {
|
||
message.warning('检测完成,但结果格式异常')
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('检测错误:', error)
|
||
message.error(error instanceof Error ? error.message : '检测失败,请稍后重试')
|
||
} finally {
|
||
setIsGenerating(false)
|
||
}
|
||
}
|
||
|
||
const handleReset = () => {
|
||
form.resetFields()
|
||
setCorrectionResults([])
|
||
setCorrectedText('')
|
||
message.info('已重置表单')
|
||
}
|
||
|
||
const renderCorrectionResults = () => {
|
||
if (isGenerating) {
|
||
return (
|
||
<div style={{ textAlign: 'center', color: '#999', padding: '20px' }}>
|
||
正在检测中,请稍候...
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (correctionResults.length === 0) {
|
||
return (
|
||
<div style={{ textAlign: 'center', color: '#999' }}>
|
||
错别字检测结果将在这里显示
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||
{correctionResults.map((item, index) => (
|
||
<div
|
||
key={index}
|
||
style={{
|
||
padding: '12px',
|
||
backgroundColor: '#f5f5f5',
|
||
borderRadius: '6px',
|
||
border: '1px solid #d9d9d9'
|
||
}}
|
||
>
|
||
<div style={{ marginBottom: '8px', fontWeight: 'bold' }}>
|
||
位置 {item.position}:
|
||
</div>
|
||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||
<span style={{ color: '#ff4d4f' }}>原词:{item.original}</span>
|
||
<span>→</span>
|
||
<span style={{ color: '#52c41a' }}>建议:{item.corrected}</span>
|
||
</div>
|
||
{item.suggestions && item.suggestions.length > 0 && (
|
||
<div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
|
||
其他建议:{item.suggestions.join('、')}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '20px' }}>
|
||
<div style={{ textAlign: 'center', marginBottom: '30px' }}>
|
||
<h1 style={{ color: '#1890ff', fontSize: '28px', fontWeight: 'bold', margin: 0 }}>
|
||
错别字检测与纠正系统
|
||
</h1>
|
||
</div>
|
||
|
||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||
<Card title="参数输入区">
|
||
<Form form={form} layout="vertical">
|
||
<Form.Item
|
||
name="user_input_text"
|
||
label="用户输入文本"
|
||
rules={[
|
||
{ required: true, message: '请输入需要检测错别字的文本' },
|
||
{ max: maxChars, message: `输入文本不能超过${maxChars}个字符` }
|
||
]}
|
||
>
|
||
<Input.TextArea
|
||
rows={8}
|
||
placeholder="请输入需要检测错别字的中文文本..."
|
||
maxLength={maxChars}
|
||
showCount
|
||
style={{ fontSize: '14px' }}
|
||
/>
|
||
</Form.Item>
|
||
|
||
<Form.Item name="auto_correct" label="是否自动替换" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="custom_vocab"
|
||
label="自定义词汇库(可选)"
|
||
rules={[{ validator: validateCustomVocab }]}
|
||
extra={
|
||
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||
格式要求:合法的JSON数组,元素为字符串
|
||
</div>
|
||
}
|
||
>
|
||
<Input.TextArea
|
||
rows={4}
|
||
placeholder='请输入JSON格式词汇库,如:["正确词1", "正确词2"]'
|
||
style={{ fontSize: '14px' }}
|
||
/>
|
||
</Form.Item>
|
||
</Form>
|
||
</Card>
|
||
|
||
<div className="button-group" style={{ display: 'flex', justifyContent: 'center', gap: '16px' }}>
|
||
<Button
|
||
type="primary"
|
||
size="large"
|
||
onClick={handleGenerate}
|
||
loading={isGenerating}
|
||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||
>
|
||
检测错别字
|
||
</Button>
|
||
<Button
|
||
size="large"
|
||
onClick={handleReset}
|
||
disabled={isGenerating}
|
||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||
>
|
||
重置
|
||
</Button>
|
||
</div>
|
||
|
||
<Card title="检测结果">
|
||
<div
|
||
style={{
|
||
minHeight: '150px',
|
||
maxHeight: '400px',
|
||
overflowY: 'auto',
|
||
padding: '15px',
|
||
backgroundColor: '#fafafa',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '4px'
|
||
}}
|
||
>
|
||
{renderCorrectionResults()}
|
||
</div>
|
||
</Card>
|
||
|
||
{correctedText && (
|
||
<Card title="纠正后的文本">
|
||
<div
|
||
style={{
|
||
minHeight: '120px',
|
||
padding: '15px',
|
||
backgroundColor: '#f6ffed',
|
||
border: '1px solid #b7eb8f',
|
||
borderRadius: '4px',
|
||
whiteSpace: 'pre-wrap',
|
||
wordWrap: 'break-word',
|
||
fontSize: '14px',
|
||
lineHeight: '1.6'
|
||
}}
|
||
>
|
||
{correctedText}
|
||
</div>
|
||
</Card>
|
||
)}
|
||
</Space>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default SpellCheck
|