Files
copilot-toolbox-template-0123/src/pages/contract-review/index.tsx
2026-01-23 02:29:22 +00:00

292 lines
11 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 { useState } from 'react'
import { Button, Card, Form, Input, message, Row, Col, Divider, Typography, Space } from 'antd'
import { FileTextOutlined, DownloadOutlined } from '@ant-design/icons'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { submitContractReview } from '@/api/contract-review'
const { Title, Paragraph } = Typography
const { TextArea } = Input
const ContractReview = () => {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [reviewResult, setReviewResult] = useState('')
const [currentContract, setCurrentContract] = useState('')
const maxChars = 10000
const handleReview = async () => {
try {
const contract_text = form.getFieldValue('contract_fragment') || ''
const knowledge_base = '通用合同审核规则' // 固定使用通用知识库
// 验证输入
if (!contract_text.trim()) {
message.warning('请输入需要审核的合同片段')
return
}
setLoading(true)
setReviewResult('')
const response = await submitContractReview(contract_text, knowledge_base)
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 handleClear = () => {
form.resetFields()
setReviewResult('')
setCurrentContract('')
message.info('已清空内容')
}
const handleExport = () => {
if (!reviewResult) {
message.warning('没有可导出的内容')
return
}
// 创建下载链接
const element = document.createElement('a')
const file = new Blob([reviewResult], { type: 'text/plain;charset=utf-8' })
element.href = URL.createObjectURL(file)
element.download = `合同审核报告_${new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '-')}.txt`
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
message.success('导出成功')
}
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
<Space direction="vertical" size="small" style={{ marginBottom: '30px', textAlign: 'center' }}>
<Title level={2} style={{ color: '#1890ff', marginBottom: '8px' }}>
</Title>
<Paragraph style={{ color: '#666', fontSize: '14px', marginBottom: 0 }}>
AI驱动的合同智能审核工具
</Paragraph>
</Space>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{/* 参数输入区 */}
<Card title="参数输入区">
<Form form={form} layout="vertical">
<Form.Item
name="contract_fragment"
label={
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<span></span>
<span style={{ color: currentContract.length > maxChars ? '#ff4d4f' : '#999' }}>
{currentContract.length} / {maxChars}
</span>
</div>
}
rules={[
{ required: true, message: '请输入需要审核的合同片段' },
{ max: maxChars, message: `输入内容不能超过${maxChars}个字符` }
]}
>
<TextArea
placeholder="请输入需要审核的合同片段内容最多10000个字符"
rows={10}
value={currentContract}
onChange={(e) => setCurrentContract(e.target.value)}
showCount
maxLength={maxChars}
style={{ fontSize: '14px' }}
/>
</Form.Item>
{/* 知识库选择已隐藏,默认使用通用合同审核规则 */}
<input type="hidden" name="knowledge_base" value="通用合同审核规则" />
</Form>
</Card>
{/* 操作按钮区 */}
<div style={{ display: 'flex', justifyContent: 'center', gap: '16px' }}>
<Button
type="primary"
size="large"
onClick={handleReview}
loading={loading}
icon={<FileTextOutlined />}
style={{ minWidth: '140px', height: '44px', fontSize: '16px' }}
>
</Button>
<Button
size="large"
onClick={handleClear}
disabled={loading}
style={{ minWidth: '120px', height: '44px', fontSize: '16px' }}
>
</Button>
<Button
size="large"
onClick={handleExport}
disabled={!reviewResult || loading}
icon={<DownloadOutlined />}
style={{ minWidth: '120px', height: '44px', fontSize: '16px' }}
>
</Button>
</div>
{/* 内容展示区 */}
<Card
title="审核结果"
extra={
reviewResult && (
<span style={{ color: '#52c41a', fontWeight: 'bold' }}>
</span>
)
}
>
<div
style={{
minHeight: '300px',
maxHeight: '600px',
overflowY: 'auto',
padding: '20px',
backgroundColor: '#fafafa',
border: '1px solid #d9d9d9',
borderRadius: '6px'
}}
>
{loading ? (
<div style={{ textAlign: 'center', color: '#999', padding: '40px' }}>
<div style={{ fontSize: '16px', marginBottom: '8px' }}>🤖 AI ...</div>
<div style={{ fontSize: '12px' }}></div>
</div>
) : reviewResult ? (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// 自定义样式
h1: ({ children }) => <h1 style={{ color: '#1890ff', fontSize: '20px', marginBottom: '16px', borderBottom: '2px solid #1890ff', paddingBottom: '8px' }}>{children}</h1>,
h2: ({ children }) => <h2 style={{ color: '#1890ff', fontSize: '18px', marginBottom: '12px', marginTop: '20px' }}>{children}</h2>,
h3: ({ children }) => <h3 style={{ color: '#333', fontSize: '16px', marginBottom: '10px', marginTop: '16px' }}>{children}</h3>,
p: ({ children }) => <p style={{ marginBottom: '12px', lineHeight: '1.6', color: '#333' }}>{children}</p>,
table: ({ children }) => (
<div style={{ overflowX: 'auto', marginBottom: '16px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>{children}</table>
</div>
),
th: ({ children }) => <th style={{ border: '1px solid #d9d9d9', padding: '8px 12px', backgroundColor: '#f5f5f5', fontWeight: 'bold', textAlign: 'left' }}>{children}</th>,
td: ({ children }) => <td style={{ border: '1px solid #d9d9d9', padding: '8px 12px' }}>{children}</td>,
ul: ({ children }) => <ul style={{ marginBottom: '12px', paddingLeft: '20px' }}>{children}</ul>,
ol: ({ children }) => <ol style={{ marginBottom: '12px', paddingLeft: '20px' }}>{children}</ol>,
li: ({ children }) => <li style={{ marginBottom: '4px', lineHeight: '1.6' }}>{children}</li>,
blockquote: ({ children }) => (
<blockquote style={{ borderLeft: '4px solid #1890ff', paddingLeft: '16px', marginLeft: 0, marginBottom: '12px', fontStyle: 'italic', backgroundColor: '#f0f9ff', padding: '12px 16px' }}>
{children}
</blockquote>
),
code: ({ children }) => (
<code style={{ backgroundColor: '#f5f5f5', padding: '2px 4px', borderRadius: '3px', fontSize: '13px', fontFamily: 'Monaco, Consolas, monospace' }}>
{children}
</code>
),
pre: ({ children }) => (
<pre style={{ backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '6px', overflowX: 'auto', marginBottom: '12px' }}>
{children}
</pre>
),
strong: ({ children }) => <strong style={{ fontWeight: 'bold', color: '#1890ff' }}>{children}</strong>
}}
>
{reviewResult}
</ReactMarkdown>
) : (
<div style={{ textAlign: 'center', color: '#999', padding: '60px' }}>
<FileTextOutlined style={{ fontSize: '48px', marginBottom: '16px', display: 'block' }} />
<div></div>
<div style={{ fontSize: '12px', marginTop: '8px' }}>
"开始审核"
</div>
</div>
)}
</div>
</Card>
</div>
</div>
)
}
export default ContractReview