374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
import { useState } from 'react'
|
||
import { Button, Card, Form, Input, message, Space, Typography } from 'antd'
|
||
import { reviewContract } from '@/api/contract-review'
|
||
|
||
const { Title, Paragraph, Text } = Typography
|
||
|
||
const { TextArea } = Input
|
||
|
||
const HomePage: React.FC = () => {
|
||
const [form] = Form.useForm()
|
||
const [loading, setLoading] = useState(false)
|
||
const [reviewResult, setReviewResult] = useState('')
|
||
const [currentInput, setCurrentInput] = useState('')
|
||
|
||
const maxChars = 5000
|
||
|
||
// 简单的markdown处理函数
|
||
const formatMarkdown = (text: string) => {
|
||
if (!text) return []
|
||
|
||
const lines = text.split('\n')
|
||
const elements: React.ReactNode[] = []
|
||
let currentList: string[] = []
|
||
let inTable = false
|
||
let tableHeaders: string[] = []
|
||
let tableRows: string[][] = []
|
||
|
||
const flushList = () => {
|
||
if (currentList.length > 0) {
|
||
elements.push(
|
||
<ul key={elements.length} style={{ paddingLeft: '20px', margin: '12px 0' }}>
|
||
{currentList.map((item, index) => (
|
||
<li key={index} style={{ margin: '4px 0' }}>{item}</li>
|
||
))}
|
||
</ul>
|
||
)
|
||
currentList = []
|
||
}
|
||
}
|
||
|
||
const flushTable = () => {
|
||
if (tableHeaders.length > 0) {
|
||
elements.push(
|
||
<div key={elements.length} style={{ margin: '16px 0', overflowX: 'auto' }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr>
|
||
{tableHeaders.map((header, index) => (
|
||
<th key={index} style={{
|
||
border: '1px solid #d9d9d9',
|
||
padding: '8px 12px',
|
||
backgroundColor: '#f5f5f5',
|
||
fontWeight: 'bold',
|
||
textAlign: 'left'
|
||
}}>
|
||
{header}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{tableRows.map((row, rowIndex) => (
|
||
<tr key={rowIndex}>
|
||
{row.map((cell, cellIndex) => (
|
||
<td key={cellIndex} style={{
|
||
border: '1px solid #d9d9d9',
|
||
padding: '8px 12px'
|
||
}}>
|
||
{cell}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)
|
||
tableHeaders = []
|
||
tableRows = []
|
||
inTable = false
|
||
}
|
||
}
|
||
|
||
lines.forEach((line, index) => {
|
||
const trimmedLine = line.trim()
|
||
|
||
// 处理标题
|
||
if (trimmedLine.startsWith('# ')) {
|
||
flushList()
|
||
flushTable()
|
||
elements.push(
|
||
<Title key={elements.length} level={1} style={{ color: '#1890ff', borderBottom: '1px solid #d9d9d9', paddingBottom: '8px', marginBottom: '16px' }}>
|
||
{trimmedLine.substring(2)}
|
||
</Title>
|
||
)
|
||
} else if (trimmedLine.startsWith('## ')) {
|
||
flushList()
|
||
flushTable()
|
||
elements.push(
|
||
<Title key={elements.length} level={2} style={{ color: '#1890ff', marginTop: '24px', marginBottom: '12px' }}>
|
||
{trimmedLine.substring(3)}
|
||
</Title>
|
||
)
|
||
} else if (trimmedLine.startsWith('### ')) {
|
||
flushList()
|
||
flushTable()
|
||
elements.push(
|
||
<Title key={elements.length} level={3} style={{ color: '#1890ff', marginTop: '20px', marginBottom: '10px' }}>
|
||
{trimmedLine.substring(4)}
|
||
</Title>
|
||
)
|
||
}
|
||
// 处理分割线
|
||
else if (trimmedLine === '---') {
|
||
flushList()
|
||
flushTable()
|
||
elements.push(
|
||
<hr key={elements.length} style={{ border: 'none', borderTop: '1px solid #d9d9d9', margin: '24px 0' }} />
|
||
)
|
||
}
|
||
// 处理列表项
|
||
else if (trimmedLine.startsWith('- ') || trimmedLine.startsWith('* ')) {
|
||
flushTable()
|
||
currentList.push(trimmedLine.substring(2))
|
||
}
|
||
// 处理表格
|
||
else if (trimmedLine.includes('|') && !inTable) {
|
||
flushList()
|
||
inTable = true
|
||
tableHeaders = trimmedLine.split('|').map(h => h.trim()).filter(h => h)
|
||
} else if (trimmedLine.includes('|') && inTable && !trimmedLine.includes('---')) {
|
||
const row = trimmedLine.split('|').map(c => c.trim()).filter(c => c)
|
||
if (row.length > 0) {
|
||
tableRows.push(row)
|
||
}
|
||
} else if (trimmedLine.includes('|') && inTable && trimmedLine.includes('---')) {
|
||
// 跳过分隔行
|
||
}
|
||
// 处理普通段落
|
||
else if (trimmedLine) {
|
||
flushList()
|
||
flushTable()
|
||
|
||
// 处理加粗文本
|
||
const boldRegex = /\*\*(.*?)\*\*/g
|
||
const parts = trimmedLine.split(boldRegex)
|
||
|
||
if (parts.length > 1) {
|
||
elements.push(
|
||
<Paragraph key={elements.length} style={{ margin: '12px 0' }}>
|
||
{parts.map((part, partIndex) =>
|
||
partIndex % 2 === 1 ?
|
||
<Text key={partIndex} strong style={{ color: '#262626' }}>{part}</Text> :
|
||
part
|
||
)}
|
||
</Paragraph>
|
||
)
|
||
} else {
|
||
elements.push(
|
||
<Paragraph key={elements.length} style={{ margin: '12px 0', lineHeight: '1.6' }}>
|
||
{trimmedLine}
|
||
</Paragraph>
|
||
)
|
||
}
|
||
}
|
||
})
|
||
|
||
// 清理剩余的内容
|
||
flushList()
|
||
flushTable()
|
||
|
||
return elements
|
||
}
|
||
|
||
const handleReview = async () => {
|
||
try {
|
||
const contract_text = form.getFieldValue('contract_text') || ''
|
||
|
||
if (!contract_text.trim()) {
|
||
message.warning('请输入合同片段内容')
|
||
return
|
||
}
|
||
|
||
setLoading(true)
|
||
setReviewResult('')
|
||
|
||
const response = await reviewContract(contract_text)
|
||
|
||
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 = ''
|
||
|
||
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('审核完成')
|
||
setLoading(false)
|
||
return
|
||
}
|
||
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
|
||
setReviewResult(fullContent)
|
||
} else if (parsed.event === 'error') {
|
||
throw new Error(parsed.message || 'Dify API 返回错误')
|
||
}
|
||
} catch (parseError) {
|
||
console.warn('跳过无法解析的行:', trimmedLine)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
reader.releaseLock()
|
||
}
|
||
|
||
if (fullContent) {
|
||
message.success('审核完成')
|
||
} else {
|
||
throw new Error('未收到审核结果')
|
||
}
|
||
} catch (error) {
|
||
console.error('审核错误:', error)
|
||
message.error(error instanceof Error ? error.message : '审核失败,请稍后重试')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleReset = () => {
|
||
form.resetFields()
|
||
setReviewResult('')
|
||
setCurrentInput('')
|
||
message.info('已重置内容')
|
||
}
|
||
|
||
const handleReReview = () => {
|
||
handleReview()
|
||
}
|
||
|
||
return (
|
||
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '24px' }}>
|
||
<Title level={2} style={{ textAlign: 'center', marginBottom: '30px' }}>合同智能审核</Title>
|
||
<Paragraph style={{ textAlign: 'center', marginBottom: '40px', fontSize: '16px' }}>
|
||
通过AI技术对合同片段进行智能审核,提供专业建议
|
||
</Paragraph>
|
||
|
||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||
{/* 参数输入区 */}
|
||
<Card title="参数输入区">
|
||
<Form form={form} layout="vertical">
|
||
<Form.Item
|
||
name="contract_text"
|
||
label={
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||
<span>合同片段文本</span>
|
||
<span style={{ color: currentInput.length > maxChars ? '#ff4d4f' : '#999' }}>
|
||
{currentInput.length} / {maxChars}
|
||
</span>
|
||
</div>
|
||
}
|
||
rules={[
|
||
{ required: true, message: '请输入合同片段内容' },
|
||
{ max: maxChars, message: `输入内容不能超过${maxChars}个字符` }
|
||
]}
|
||
>
|
||
<TextArea
|
||
placeholder="请输入需要审核的合同片段内容(最多5000个字符)"
|
||
rows={8}
|
||
value={currentInput}
|
||
onChange={(e) => setCurrentInput(e.target.value)}
|
||
showCount
|
||
maxLength={maxChars}
|
||
style={{ fontSize: '14px', fontFamily: 'monospace' }}
|
||
/>
|
||
</Form.Item>
|
||
</Form>
|
||
</Card>
|
||
|
||
{/* 操作按钮区 */}
|
||
<Space style={{ width: '100%', justifyContent: 'center' }} size="middle">
|
||
<Button
|
||
type="primary"
|
||
size="large"
|
||
onClick={handleReview}
|
||
loading={loading}
|
||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||
>
|
||
开始审核
|
||
</Button>
|
||
{reviewResult && (
|
||
<Button
|
||
size="large"
|
||
onClick={handleReReview}
|
||
disabled={loading}
|
||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||
>
|
||
重新审核
|
||
</Button>
|
||
)}
|
||
<Button
|
||
size="large"
|
||
onClick={handleReset}
|
||
disabled={loading}
|
||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||
>
|
||
重置
|
||
</Button>
|
||
</Space>
|
||
|
||
{/* 内容展示区 */}
|
||
<Card title="审核结果">
|
||
<div
|
||
style={{
|
||
backgroundColor: '#fafafa',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '4px',
|
||
padding: '15px',
|
||
minHeight: '300px',
|
||
maxHeight: '500px',
|
||
overflowY: 'auto',
|
||
fontSize: '14px',
|
||
lineHeight: '1.6'
|
||
}}
|
||
>
|
||
{loading ? (
|
||
<div style={{ textAlign: 'center', color: '#999', padding: '20px' }}>
|
||
正在审核中,请稍候...
|
||
</div>
|
||
) : reviewResult ? (
|
||
<div>
|
||
{formatMarkdown(reviewResult)}
|
||
</div>
|
||
) : (
|
||
<div style={{ textAlign: 'center', color: '#999' }}>
|
||
审核结果将在这里显示
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
</Space>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default HomePage
|