diff --git a/.gitignore b/.gitignore index 80ec311..d836877 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ node_modules /.cache /build .env + +.idea + diff --git a/app/components/reviews/AIAnalysis.tsx b/app/components/reviews/AIAnalysis.tsx new file mode 100644 index 0000000..3ef4afc --- /dev/null +++ b/app/components/reviews/AIAnalysis.tsx @@ -0,0 +1,132 @@ +/** + * AI智能分析组件 + * 显示AI对文档的分析结果、风险提示和优化建议 + */ + +// 分析项类型 +interface AnalysisItem { + title: string; + content: string; + description: string; +} + +// 分析数据类型 +interface AnalysisData { + riskAlerts: AnalysisItem[]; + suggestions: AnalysisItem[]; + summary: string; +} + +interface AIAnalysisProps { + analysisData: AnalysisData; + score: number; + onConfirmResults: () => void; +} + +export function AIAnalysis({ analysisData, score, onConfirmResults }: AIAnalysisProps) { + const handleExportReport = () => { + alert('导出评查报告功能'); + }; + + // 渲染风险提示项 + const renderRiskAlerts = () => { + return analysisData.riskAlerts.map((item, index) => ( +
+
+ +
+

+ {item.title}: + {item.content} +

+

{item.description}

+
+
+
+ )); + }; + + // 渲染优化建议项 + const renderSuggestions = () => { + return analysisData.suggestions.map((item, index) => ( +
+
+ +
+

+ {item.title}: + {item.content} +

+

{item.description}

+
+
+
+ )); + }; + + // 获取评分对应的颜色类 + const getScoreColorClass = (score: number) => { + if (score >= 90) return 'text-success'; + if (score >= 70) return 'text-warning'; + return 'text-error'; + }; + + // 获取评分条对应的颜色类 + const getScoreBarColorClass = (score: number) => { + if (score >= 90) return 'bg-success'; + if (score >= 70) return 'bg-warning'; + return 'bg-error'; + }; + + return ( +
+
+

AI智能分析

+ + {/* 风险提示 */} + {analysisData.riskAlerts.length > 0 && renderRiskAlerts()} + + {/* 优化建议 */} + {analysisData.suggestions.length > 0 && renderSuggestions()} +
+ +
+

综合评价

+
+ {/* 评价摘要 */} +

{analysisData.summary}

+ + {/* 评分 */} +
+

+ 合规性评分: + {score}分 +

+
+
+
+
+ + {/* 操作按钮 */} +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/reviews/FileDetails.tsx b/app/components/reviews/FileDetails.tsx new file mode 100644 index 0000000..2ab4392 --- /dev/null +++ b/app/components/reviews/FileDetails.tsx @@ -0,0 +1,150 @@ +/** + * 文件详情组件 + * 显示文件基本信息、合同信息和评查信息 + */ +import { ReactNode } from 'react'; + +// 文件基本信息 +interface FileInfo { + fileName: string; + contractNumber: string; + fileSize: string; + fileFormat: string; + pageCount: number; + uploadTime: string; + uploadUser: string; +} + +// 合同信息 +interface ContractInfo { + contractType: string; + signDate: string; + parties: { + partyA: string; + partyB: string; + }; + amount: string; + period: string; +} + +// 评查信息 +interface ReviewInfo { + reviewTime: string; + reviewModel: string; + ruleGroup: string; + result: string; + issueCount: number; +} + +interface FileDetailsProps { + fileInfo: FileInfo; + contractInfo: ContractInfo; + reviewInfo: ReviewInfo; +} + +export function FileDetails({ fileInfo, contractInfo, reviewInfo }: FileDetailsProps) { + // 情况状态对应的标签 + const renderResultBadge = (result: string) => { + switch (result) { + case 'success': + return ( + + 通过 + + ); + case 'warning': + return ( + + 警告 + + ); + case 'error': + return ( + + 不通过 + + ); + default: + return ( + + 警告 + + ); + } + }; + + // 渲染信息区块 + const renderInfoSection = (title: string, icon: string, color: string, children: ReactNode) => { + return ( +
+
+ +

{title}

+
+
+ {children} +
+
+ ); + }; + + // 渲染信息行 + const renderInfoRow = (label: string, value: string | ReactNode) => { + return ( +
+
{label}:
+
{value}
+
+ ); + }; + + return ( +
+ {/* 文件基本信息 */} + {renderInfoSection('文件基本信息', 'ri-file-info-line', 'blue', ( +
+ {renderInfoRow('文件名称', fileInfo.fileName)} + {renderInfoRow('合同编号', fileInfo.contractNumber)} + {renderInfoRow('文件大小', fileInfo.fileSize)} + {renderInfoRow('文件格式', fileInfo.fileFormat)} + {renderInfoRow('页数', `${fileInfo.pageCount}页`)} + {renderInfoRow('上传时间', fileInfo.uploadTime)} + {renderInfoRow('上传用户', fileInfo.uploadUser)} +
+ ))} + + {/* 合同信息 */} + {renderInfoSection('合同信息', 'ri-file-paper-2-line', 'green', ( +
+ {renderInfoRow('合同类型', contractInfo.contractType)} + {renderInfoRow('签约日期', contractInfo.signDate)} + {renderInfoRow('合同当事人', ( +
+
甲方:{contractInfo.parties.partyA}
+
乙方:{contractInfo.parties.partyB}
+
+ ))} + {renderInfoRow('合同金额', contractInfo.amount)} + {renderInfoRow('履行期限', contractInfo.period)} +
+ ))} + + {/* 评查信息 */} + {renderInfoSection('评查信息', 'ri-search-eye-line', 'purple', ( + <> +
+ {renderInfoRow('评查时间', reviewInfo.reviewTime)} + {renderInfoRow('评查模型', reviewInfo.reviewModel)} + {renderInfoRow('评查规则组', reviewInfo.ruleGroup)} + {renderInfoRow('评查结果', ( +
+ {renderResultBadge(reviewInfo.result)} + (共发现{reviewInfo.issueCount}个问题) +
+ ))} +
+ + ))} +
+ ); +} \ No newline at end of file diff --git a/app/components/reviews/FileInfo.tsx b/app/components/reviews/FileInfo.tsx new file mode 100644 index 0000000..ecb8546 --- /dev/null +++ b/app/components/reviews/FileInfo.tsx @@ -0,0 +1,72 @@ +/** + * 文件信息组件 + * 显示文件名称、状态信息以及操作按钮(下载原文件、导出评查报告、确认评查结果) + */ + +interface FileInfoProps { + fileInfo: { + fileName: string; + contractNumber: string; + fileSize?: string; + fileFormat?: string; + pageCount?: number; + uploadTime?: string; + uploadUser?: string; + }; + onConfirmResults: () => void; +} + +export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) { + const handleDownloadFile = () => { + alert('下载原文件功能'); + }; + + const handleExportReport = () => { + alert('导出评查报告功能'); + }; + + return ( +
+
+
+

+ {fileInfo.fileName} + + 合同编号:{fileInfo.contractNumber} + + {fileInfo.fileSize && ( + + {fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页 + + )} +

+ {fileInfo.uploadTime && ( +
+ 上传时间:{fileInfo.uploadTime} | 上传用户:{fileInfo.uploadUser} +
+ )} +
+
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx new file mode 100644 index 0000000..1009c06 --- /dev/null +++ b/app/components/reviews/FilePreview.tsx @@ -0,0 +1,200 @@ +/** + * 文件预览组件 + * 显示文档内容和评查点高亮 + */ +import { useState, useEffect, useRef } from 'react'; + +// 定义评查点类型 +interface ReviewPoint { + id: string; + title: string; + status: string; + content: string; + suggestion: string; + position?: { + section: string; + index: number; + }; +} + +// 定义文档内容类型 +interface FileContent { + title: string; + contractNumber: string; + parties: { + partyA: { + name: string; + address: string; + representative: string; + phone: string; + }; + partyB: { + name: string; + address: string; + representative: string; + phone: string; + }; + }; + sections: { + title: string; + content: string; + }[]; +} + +interface FilePreviewProps { + fileContent: FileContent; + reviewPoints: ReviewPoint[]; + activeReviewPointId: string | null; +} + +export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }: FilePreviewProps) { + const [zoomLevel, setZoomLevel] = useState(100); + const [highlightsVisible, setHighlightsVisible] = useState(true); + const contentRef = useRef(null); + + // 放大文档 + const handleZoomIn = () => { + if (zoomLevel < 200) { + setZoomLevel(prevZoom => prevZoom + 10); + } + }; + + // 缩小文档 + const handleZoomOut = () => { + if (zoomLevel > 50) { + setZoomLevel(prevZoom => prevZoom - 10); + } + }; + + // 切换高亮显示 + const toggleHighlights = () => { + setHighlightsVisible(!highlightsVisible); + }; + + // 当选中的评查点变化时,滚动到对应位置 + useEffect(() => { + if (activeReviewPointId && contentRef.current) { + const highlightElement = contentRef.current.querySelector(`[data-review-id="${activeReviewPointId}"]`); + if (highlightElement) { + highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + // 添加临时突出显示效果 + highlightElement.classList.add('highlight-focus'); + setTimeout(() => { + highlightElement.classList.remove('highlight-focus'); + }, 1500); + } + } + }, [activeReviewPointId]); + + // 获取评查点对应的样式类 + const getHighlightClass = (status: string) => { + switch (status) { + case 'warning': + return 'warning'; + case 'error': + return 'error'; + case 'success': + return 'success'; + default: + return 'warning'; + } + }; + + // 渲染文档内容 + const renderDocumentContent = () => { + return ( +
+

{fileContent.title}

+

合同编号:{fileContent.contractNumber}

+ +
+

甲方(供方):{fileContent.parties.partyA.name}

+

地址:{fileContent.parties.partyA.address}

+

法定代表人:{fileContent.parties.partyA.representative}

+

联系电话:{fileContent.parties.partyA.phone}

+

 

+

乙方(需方):{fileContent.parties.partyB.name}

+

地址:{fileContent.parties.partyB.address}

+

法定代表人:{fileContent.parties.partyB.representative}

+

联系电话:{fileContent.parties.partyB.phone}

+
+ +

根据《中华人民共和国合同法》及有关法律法规的规定,经双方协商一致,签订本合同,共同遵守。

+ + {fileContent.sections.map((section, sectionIndex) => ( +
+

{section.title}

+ {renderSectionContent(section.content, section.title, sectionIndex)} +
+ ))} +
+ ); + }; + + // 渲染章节内容,处理高亮 + const renderSectionContent = (content: string, sectionTitle: string, sectionIndex: number) => { + const lines = content.split('\n'); + + return lines.map((line, lineIndex) => { + // 查找该行对应的评查点 + const reviewPoint = reviewPoints.find(point => + point.position && + point.position.section === sectionTitle && + point.position.index === lineIndex + ); + + if (reviewPoint && highlightsVisible) { + // 如果有对应的评查点,添加高亮 + const isActive = reviewPoint.id === activeReviewPointId; + return ( +
+

{line}

+
+ ); + } else { + // 没有评查点,正常显示 + return

{line}

; + } + }); + }; + + return ( +
+
+
+ + 文件预览 +
+
+ + + +
+
+
+ {renderDocumentContent()} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx new file mode 100644 index 0000000..065d71c --- /dev/null +++ b/app/components/reviews/ReviewPointsList.tsx @@ -0,0 +1,441 @@ +/** + * 评查点列表组件 + * 显示评查结果统计和所有评查点列表 + */ +import { useState } from 'react'; + +// 评查点类型定义 +interface ReviewPoint { + id: string; + title: string; + location: string; + status: string; + content: string; + suggestion: string; + needsHumanReview?: boolean; + humanReviewNote?: string; + humanReviewBy?: string; + humanReviewTime?: string; + position?: { + section: string; + index: number; + }; +} + +// 统计数据类型 +interface Statistics { + total: number; + success: number; + warning: number; + error: number; + score: number; +} + +interface ReviewPointsListProps { + reviewPoints: ReviewPoint[]; + statistics: Statistics; + activeReviewPointId: string | null; + onReviewPointSelect: (id: string) => void; + onStatusChange: (id: string, status: string) => void; +} + +export function ReviewPointsList({ + reviewPoints, + statistics, + activeReviewPointId, + onReviewPointSelect, + onStatusChange +}: ReviewPointsListProps) { + const [editingReviewPoint, setEditingReviewPoint] = useState(null); + const [userInputText, setUserInputText] = useState(''); + const [searchText, setSearchText] = useState(''); + const [statusFilter, setStatusFilter] = useState(null); + + // 过滤评查点 + const filteredReviewPoints = reviewPoints.filter(point => { + const matchesSearch = searchText === '' || + point.title.toLowerCase().includes(searchText.toLowerCase()) || + point.location.toLowerCase().includes(searchText.toLowerCase()) || + point.content.toLowerCase().includes(searchText.toLowerCase()); + + const matchesStatus = statusFilter === null || point.status === statusFilter; + + return matchesSearch && matchesStatus; + }); + + // 处理点击"一键替换"按钮 + const handleReplace = (reviewPointId: string) => { + // 在实际应用中,这里应该调用API进行内容替换 + // 模拟替换操作 + alert(`将为评查点 ${reviewPointId} 执行一键替换操作`); + + // 更新评查点状态为成功 + onStatusChange(reviewPointId, 'success'); + }; + + // 处理评查点审核操作 + const handleReviewAction = (reviewPointId: string, action: 'approve' | 'reject') => { + // 更新评查点状态 + onStatusChange(reviewPointId, action === 'approve' ? 'success' : 'error'); + + // 清除编辑状态 + setEditingReviewPoint(null); + setUserInputText(''); + + alert(`${action === 'approve' ? '通过' : '不通过'}了评查点 ${reviewPointId}`); + }; + + // 显示评查点详情编辑界面 + const handleEditReviewPoint = (reviewPointId: string) => { + setEditingReviewPoint(reviewPointId); + + // 获取评查点的建议内容作为初始值 + const reviewPoint = reviewPoints.find(point => point.id === reviewPointId); + if (reviewPoint) { + setUserInputText(reviewPoint.suggestion || ''); + } + }; + + // 渲染评查统计信息 + const renderStatistics = () => { + return ( +
+
+
+
+ {statistics.total} +
+ 总计 +
+
+
+ + 通过 +
+
+
+ + 警告 +
+
+
+ + 错误 +
+
+
+ ); + }; + + // 渲染搜索框 + const renderSearchBar = () => { + return ( +
+
+ setSearchText(e.target.value)} + /> + + {searchText && ( + + )} +
+
+ ); + }; + + // 渲染评查点状态标签 + const renderStatusBadge = (status: string) => { + switch (status) { + case 'success': + return ( + + 通过 + + ); + case 'warning': + return ( + + 警告 + + ); + case 'error': + return ( + + 不通过 + + ); + case 'processing': + return ( + + 处理中 + + ); + default: + return ( + + 警告 + + ); + } + }; + + // 渲染人工审核标记 + const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => { + if (reviewPoint.needsHumanReview) { + return ( + + 需人工 + + ); + } + return null; + }; + + // 渲染人工审核注释 + const renderHumanReviewNote = (reviewPoint: ReviewPoint) => { + if (reviewPoint.needsHumanReview && reviewPoint.humanReviewNote) { + return ( +
+ {reviewPoint.humanReviewNote} + {reviewPoint.humanReviewBy && reviewPoint.humanReviewTime && ( +
+ 审核人:{reviewPoint.humanReviewBy} | 时间:{reviewPoint.humanReviewTime} +
+ )} +
+ ); + } + return null; + }; + + // 渲染评查点内容与建议 + const renderReviewPointContent = (reviewPoint: ReviewPoint) => { + // 如果当前评查点不处于编辑状态,只显示简单信息 + if (editingReviewPoint !== reviewPoint.id) { + if (reviewPoint.status === 'success') { + // 已通过的评查点只显示基本信息和人工审核注释 + if (reviewPoint.needsHumanReview && reviewPoint.humanReviewNote) { + return ( +
+
+

已处理

+ {reviewPoint.suggestion && ( +
+

{reviewPoint.suggestion}

+
+ )} +
+
+ ); + } + return null; + } + + return ( +
+
+
+ 当前值 + + {reviewPoint.status === 'error' ? '不符合规范' : '需优化'} + +
+

{reviewPoint.content || '(内容为空)'}

+ + {reviewPoint.suggestion && ( +
+
+ 建议修改为 + + 符合规范 + +
+ +
+ + +
+
+
+ ); + }; + + // 渲染无匹配结果提示 + const renderEmptyState = () => { + return ( +
+ +

没有找到匹配的评查点

+

请尝试不同的搜索词或清除筛选条件

+ {(searchText || statusFilter) && ( + + )} +
+ ); + }; + + return ( +
+
+ + 评查结果 +
+ + {/* 评查统计 */} + {renderStatistics()} + + {/* 搜索框 */} + {renderSearchBar()} + + {/* 评查点列表 */} +
+ {filteredReviewPoints.length > 0 ? ( + filteredReviewPoints.map(reviewPoint => ( + + )) + ) : ( + renderEmptyState() + )} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/reviews/ReviewTabs.tsx b/app/components/reviews/ReviewTabs.tsx new file mode 100644 index 0000000..08221e4 --- /dev/null +++ b/app/components/reviews/ReviewTabs.tsx @@ -0,0 +1,48 @@ +/** + * 评查选项卡组件 + * 提供三个选项卡:评查结果、AI智能分析、文件信息 + */ +import { ReactNode } from 'react'; + +interface ReviewTabsProps { + activeTab: string; + onTabChange: (tabKey: string) => void; + children: ReactNode; +} + +export function ReviewTabs({ activeTab, onTabChange, children }: ReviewTabsProps) { + return ( +
+
+ + + +
+ +
+ {children} +
+
+ ); +} \ No newline at end of file diff --git a/app/components/reviews/index.ts b/app/components/reviews/index.ts new file mode 100644 index 0000000..d6d23c8 --- /dev/null +++ b/app/components/reviews/index.ts @@ -0,0 +1,10 @@ +/** + * 评查详情组件导出文件 + */ + +export { FileInfo } from './FileInfo'; +export { ReviewTabs } from './ReviewTabs'; +export { FilePreview } from './FilePreview'; +export { ReviewPointsList } from './ReviewPointsList'; +export { AIAnalysis } from './AIAnalysis'; +export { FileDetails } from './FileDetails'; \ No newline at end of file diff --git a/app/components/rules/new/ActionButtons.tsx b/app/components/rules/new/ActionButtons.tsx new file mode 100644 index 0000000..f680f8c --- /dev/null +++ b/app/components/rules/new/ActionButtons.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +interface ActionButtonsProps { + onSave?: () => void; + onSaveDraft?: () => void; + isEditMode?: boolean; +} + +export function ActionButtons({ onSave, onSaveDraft, isEditMode }: ActionButtonsProps) { + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/BasicInfo.tsx b/app/components/rules/new/BasicInfo.tsx new file mode 100644 index 0000000..7fbba38 --- /dev/null +++ b/app/components/rules/new/BasicInfo.tsx @@ -0,0 +1,421 @@ +import React, { useState, useEffect } from 'react'; +import type { EvaluationPoint } from '~/models/evaluation_points'; +import type { EvaluationPointGroup } from '~/models/evaluation_point_groups'; +interface BasicInfoProps { + onChange?: (data: Record) => void; + initialData?: EvaluationPoint; + evaluationPointGroups?: EvaluationPointGroup[]; + riskOptions?: Array<{value: string, label: string}>; +} + +// 评查点基本信息组件 +export function BasicInfo({ onChange, initialData, evaluationPointGroups = [], riskOptions = [] }: BasicInfoProps) { + const [formData, setFormData] = useState({ + risk: 'medium', // 风险等级 默认中风险 + is_enabled: true, // 是否启用 默认启用 + references_laws: { + name: '', + articles: [], + content: '' + }, + ...(initialData || {}) // 合并初始数据 + }); + + // 找到当前评查点类型对应的code + const getCheckpointTypeCode = () => { + if (!formData.evaluation_point_groups_pid) return ""; + + const typeGroup = evaluationPointGroups.find( + group => group.id === formData.evaluation_point_groups_pid && group.pid === 0 + ); + + return typeGroup?.code || ""; + }; + + // 评查点描述与法律依据 展开状态 + const [isDescExpanded, setIsDescExpanded] = useState(false); + + // 条款号临时输入字符串(不会触发自动分割) + const [lawArticlesText, setLawArticlesText] = useState(''); + + // 根据选择的评查点类型筛选可用的规则组 + const filteredRuleGroups = evaluationPointGroups.filter(group => + formData.evaluation_point_groups_pid && + group.pid === formData.evaluation_point_groups_pid && + group.is_enabled + ); + + // 获取评查点类型选项(pid=0的数据) + const getCheckpointTypeOptions = () => { + if (!evaluationPointGroups || evaluationPointGroups.length === 0) { + return ( + <> + + + ); + } + + const typeGroups = evaluationPointGroups.filter(group => group.pid === 0 && group.is_enabled); + + return ( + <> + + {typeGroups.map(group => ( + + ))} + + ); + }; + + // 评查点描述与法律依据 展开状态 + const handleToggleDescription = () => { + setIsDescExpanded(!isDescExpanded); + }; + + // 处理表单输入变化 + const handleInputChange = (e: React.ChangeEvent) => { + const { id, value } = e.target; + const newData = { ...formData }; + // 映射id到表单字段名 + switch(id) { + case 'rule-name': + newData.name = value; + break; + case 'rule-code': + newData.code = value; + break; + case 'risk-level': + newData.risk = value; + break; + case 'is-enabled': + newData.is_enabled = value === 'true'; + break; + case 'rule-description': + newData.description = value; + break; + case 'law-name': + newData.references_laws = { + ...formData.references_laws, + name: value + }; + break; + case 'law-content': + newData.references_laws = { + ...formData.references_laws, + content: value + }; + break; + case 'evaluation-point-group': + newData.evaluation_point_groups_id = value ? parseInt(value) : null; + break; + case 'checkpoint-type': + // 处理评查点类型选择 + if (value) { + // 找到选中的类型组 + const selectedType = evaluationPointGroups.find(group => group.code === value && group.pid === 0); + if (selectedType) { + newData.evaluation_point_groups_pid = selectedType.id; + } + } else { + newData.evaluation_point_groups_pid = null; + newData.evaluation_point_groups_id = null; // 清空规则组选择 + } + break; + } + + setFormData(newData); + + if (onChange) { + onChange(newData); + } + }; + + // 处理条款号输入框变化 + const handleLawArticlesChange = (e: React.ChangeEvent) => { + // 设置临时字符串状态,这样不会触发任何处理 + setLawArticlesText(e.target.value); + }; + + // 处理条款号输入框失去焦点 + const handleLawArticlesBlur = () => { + if (!lawArticlesText) return; + + // 将输入的文本转换为数组 + const articles = lawArticlesText + .split(',') + .map(article => article.trim()) + .filter(article => article !== ''); + + // 创建一个新的引用法律对象,保留现有字段 + const referencesLaws = { + ...(formData.references_laws || {}), + articles: articles.length > 0 ? articles : [] + }; + + // 更新表单数据 + const newData = { + ...formData, + references_laws: referencesLaws + }; + + setFormData(newData); + + if (onChange) { + onChange(newData); + } + }; + + // 初始化条款号文本字段 + useEffect(() => { + if (formData.references_laws?.articles && formData.references_laws.articles.length > 0) { + setLawArticlesText(formData.references_laws.articles.join(',')); + } + }, [formData.references_laws?.articles]); + + // 检查是否需要自动展开描述区域 + useEffect(() => { + // 如果描述或法律依据相关字段有值,则自动展开 + if ( + formData.description || + formData.references_laws?.name || + (formData.references_laws?.articles && formData.references_laws.articles.length > 0) || + formData.references_laws?.content + ) { + setIsDescExpanded(true); + } + }, [formData]); + + useEffect(() => { + // 可以在这里通知父组件 + if (onChange && filteredRuleGroups.length === 1) { + onChange({ evaluation_point_groups_id: filteredRuleGroups[0].id }); + } + }, [filteredRuleGroups, onChange]); + + return ( +
+
+

基本信息

+
+
+
+
+ + +
请使用简洁明了的名称,不超过30个字符
+
+
+ + +
用于系统标识的唯一编码
+
+
+ + +
请定义评查点的风险等级
+
+
+ + +
评查点类型用于分类管理,便于规则统一调用
+
+
+ + +
+ {!formData.evaluation_point_groups_pid ? "请先选择评查点类型" : + filteredRuleGroups.length === 0 ? "该类型下暂无可用规则组" : + "选择评查点所属的规则组"} +
+
+
+ + +
创建后是否立即启用此评查点
+
+
+ +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + handleToggleDescription(); + } + }} + tabIndex={0} + role="button" + > + + +
+ +
+
+ +
详细描述有助于其他用户了解该评查点的用途
+
+ + {/* 引用法典输入区域 */} +
+ + +
+ + +
+ +
+ + +
多个条款用逗号分隔,将自动转换为数组格式
+
+ +
+ + +
+ +
+ 引用的法律条文将在评查结果中显示,帮助用户理解评查规则的法律依据 +
+ + {/* 预览区域 */} +
+
预览效果
+
+
+ {formData.references_laws?.name || '《中华人民共和国民法典》'} +
+
+ {formData.references_laws?.articles && formData.references_laws.articles.length > 0 ? + formData.references_laws.articles.map((article, index) => ( + {article} + )) : ( + <> + 第五百八十五条 + 第五百八十六条 + + )} +
+
+ {formData.references_laws?.content || '当事人应当按照约定全面履行自己的义务。'} +
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/CodeEditor.tsx b/app/components/rules/new/CodeEditor.tsx new file mode 100644 index 0000000..4131729 --- /dev/null +++ b/app/components/rules/new/CodeEditor.tsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from 'react'; +import CodeMirror from '@uiw/react-codemirror'; +import { javascript } from '@codemirror/lang-javascript'; +import { oneDark } from '@codemirror/theme-one-dark'; + +interface CodeEditorProps { + id: string; + initialValue?: string; + language?: 'javascript' | 'python'; + onChange?: (value: string) => void; +} + +export function CodeEditor({ + id, + initialValue = '', + language = 'javascript', + onChange +}: CodeEditorProps) { + const [code, setCode] = useState(initialValue); + const [copySuccess, setCopySuccess] = useState(false); + + // 当语言变化时更新编辑器 + const extensions = [language === 'javascript' ? javascript() : javascript()]; + + // 处理代码变化 + const handleChange = (value: string) => { + setCode(value); + if (onChange) { + onChange(value); + } + }; + + // 复制代码到剪贴板 + const copyToClipboard = () => { + navigator.clipboard.writeText(code).then(() => { + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + }); + }; + + // 初始示例代码 + const getDefaultCode = (lang: string) => { + if (lang === 'javascript') { + return `// 示例代码 +function checkRule(data) { + // data 包含抽取的字段 + try { + // 在此编写检查逻辑 + if (data.fieldName && condition) { + return { + pass: true, + message: "检查通过" + }; + } else { + return { + pass: false, + message: "检查不通过,原因:..." + }; + } + } catch (error) { + return { + pass: false, + message: "执行出错:" + error.message + }; + } +}`; + } else { + return `# 示例代码 +def check_rule(data): + # data 包含抽取的字段 + try: + # 在此编写检查逻辑 + if 'field_name' in data and condition: + return { + 'pass': True, + 'message': "检查通过" + } + else: + return { + 'pass': False, + 'message': "检查不通过,原因:..." + } + except Exception as error: + return { + 'pass': False, + 'message': f"执行出错:{str(error)}" + }`; + } + }; + + // 如果初始值为空,则使用默认示例代码 + useEffect(() => { + if (!initialValue) { + setCode(getDefaultCode(language)); + } + }, [language, initialValue]); + + return ( +
+
+
+
{language === 'javascript' ? 'script.js' : 'script.py'}
+
+ +
+
+ +
+ {copySuccess && ( +
+ 代码已复制到剪贴板 +
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx new file mode 100644 index 0000000..350fa6b --- /dev/null +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -0,0 +1,1059 @@ +import React, { + useState, + useEffect, +} from "react"; +import type { EvaluationPoint } from "~/models/evaluation_points"; +import { EVALUATION_OPTIONS, VLMFieldType } from "~/models/evaluation_points"; + +/** + * ExtractionSettings 组件 + * + * 功能: + * - 提供三种抽取设置方式:大模型抽取、多模态抽取和正则抽取 + * - 允许在三个标签页中添加不同类型的字段 + * - 统一的更新机制,确保点击"更新全部字段"按钮时,所有三种类型的字段都会被收集和更新 + * + * 优化后的交互逻辑: + * 1. 用户可以在三个标签页之间切换,在每个标签页中添加对应类型的字段 + * 2. 添加字段后,会自动标记为"有未保存更改"状态 + * 3. 无论当前在哪个标签页,点击底部的"更新全部字段"按钮都会收集所有三种类型的字段 + * 4. 更新成功后会显示详细的字段数量统计信息,包括每种类型的字段数 + * 5. 系统会自动检查字段名重复,确保所有字段名唯一 + * + * 注意: + * - 仅当点击"更新全部字段"按钮后,字段才会真正提交给父组件和规则上下文 + * - 用户必须手动点击更新按钮,才能在评查设置中使用这些字段 + * + * 类型定义: + * - LogicType: 'and' | 'or' | 'custom' - 用于评查配置中多个规则的组合逻辑 + * - 'and': 所有规则都必须满足 + * - 'or': 任一规则满足即可 + * - 'custom': 自定义逻辑表达式,如 "(规则1 AND 规则2) OR 规则3" + * + * - LogicOperator: 'and' | 'or' - 用于单个规则内的条件组合 + * - 'and': 规则内所有条件都必须满足 + * - 'or': 规则内任一条件满足即可 + */ + +interface ExtractionSettingsProps { + onChange: (data: Record) => void; + initialData: EvaluationPoint; + promptTypeOptions?: Array<{ value: string; label: string }>; + vlmFieldTypeOptions?: Array<{ value: string; label: string }>; +} + +export function ExtractionSettings({ + onChange, + initialData, +}: ExtractionSettingsProps) { + + // 核心数据状态 + const [formData, setFormData] = useState({ + // 字段配置 + extraction_config: { + llm: initialData?.extraction_config?.llm ?? { + fields: [], + prompt_setting: { + type: "system", + template: "", + }, + }, + vlm: initialData?.extraction_config?.vlm ?? { + fields: [], + prompt_setting: { + type: "system", + template: "", + }, + }, + regex: initialData?.extraction_config?.regex ?? { + fields: [], + }, + }, + }); + + // 当前选中的标签页 + const [currentTab, setCurrentTab] = useState("llm"); + // 字段输入值 + const [inputValue, setInputValue] = useState({ + llm: '', + vlm: '' + }); + // 字段列表 + const [fields, setFields] = useState({ + llm: initialData?.extraction_config?.llm?.fields || [], + vlm: initialData?.extraction_config?.vlm?.fields || [] + }); + // VLM字段类型 + const [selectedVlmFieldType, setSelectedVlmFieldType] = useState('default'); + // 提示词类型 + const [promptType, setPromptType] = useState({ + llm: initialData?.extraction_config?.llm?.prompt_setting?.type || 'system', + vlm: initialData?.extraction_config?.vlm?.prompt_setting?.type || 'system' + }); + // 提示词模板 + const [selectedTemplate, setSelectedTemplate] = useState({ + llm: '', + vlm: '' + }); + // 提示词内容 + const [promptContent, setPromptContent] = useState({ + llm: initialData?.extraction_config?.llm?.prompt_setting?.template || '', + vlm: initialData?.extraction_config?.vlm?.prompt_setting?.template || '' + }); + // 正则表达式字段 + const [regexFields, setRegexFields] = useState( + initialData?.extraction_config?.regex?.fields || [] + ); + // 状态消息 + const [statusMessage, setStatusMessage] = useState<{id: string, message: string} | null>(null); + // 是否有未保存更改 + const [hasPendingChanges, setHasPendingChanges] = useState(false); + // 更新状态 + const [updateStatus, setUpdateStatus] = useState<{success: boolean, message: string} | null>(null); + + const handleTabChange = (tab: string) => { + setCurrentTab(tab); + }; + + // 自动保存字段变更状态 + // 这个效果确保添加字段后自动保存到组件状态,但不自动提交更新 + useEffect(() => { + setHasPendingChanges(true); + }, [fields, regexFields, promptContent]) + + // 处理字段输入变化 + const handleFieldInputChange = ( + e: React.ChangeEvent, + type: 'llm' | 'vlm' + ) => { + setInputValue({ + ...inputValue, + [type]: e.target.value + }); + }; + + // 处理添加字段 + const addField = (type: 'llm' | 'vlm') => { + if (!inputValue[type]) return; + + // 处理多个字段输入 + const inputs = inputValue[type].split(/[,,\s]+/).filter(Boolean); + + if (type === 'llm') { + const newFields = [...fields.llm]; + inputs.forEach(input => { + if (!newFields.includes(input)) { + newFields.push(input); + } + }); + setFields({ ...fields, llm: newFields }); + } else { + const newFields = [...fields.vlm]; + inputs.forEach(input => { + const exists = newFields.some(field => + typeof field === 'string' + ? field === input + : field.name === input + ); + + if (!exists) { + newFields.push({ + name: input, + type: selectedVlmFieldType as VLMFieldType + }); + } + }); + setFields({ ...fields, vlm: newFields }); + } + + // 清空输入框 + setInputValue({ + ...inputValue, + [type]: '' + }); + + setHasPendingChanges(true); + }; + + // 处理键盘事件 + const handleKeyDown = (e: React.KeyboardEvent, type: 'llm' | 'vlm') => { + if (e.key === 'Enter') { + e.preventDefault(); + addField(type); + } + }; + + // 处理删除字段 + const removeField = (type: 'llm' | 'vlm', index: number) => { + if (type === 'llm') { + const newFields = [...fields.llm]; + newFields.splice(index, 1); + setFields({ ...fields, llm: newFields }); + } else { + const newFields = [...fields.vlm]; + newFields.splice(index, 1); + setFields({ ...fields, vlm: newFields }); + } + + setHasPendingChanges(true); + }; + + // 获取VLM字段信息 + const getFieldInfo = (fieldString: string) => { + const parts = fieldString.split('_'); + const fieldName = parts[0]; + const fieldType = parts.length > 1 ? parts[1] : 'default'; + + let typeName, badgeClass; + switch (fieldType) { + case 'currency': + typeName = '货币'; + badgeClass = 'bg-green-100 text-green-800'; + break; + case 'print': + typeName = '打印'; + badgeClass = 'bg-blue-100 text-blue-800'; + break; + case 'seal': + typeName = '印章'; + badgeClass = 'bg-red-100 text-red-800'; + break; + case 'cross-seal': + typeName = '骑缝章'; + badgeClass = 'bg-orange-100 text-orange-800'; + break; + case 'english': + typeName = '英文'; + badgeClass = 'bg-purple-100 text-purple-800'; + break; + case 'number': + typeName = '数字'; + badgeClass = 'bg-yellow-100 text-yellow-800'; + break; + case 'handwriting': + typeName = '手写'; + badgeClass = 'bg-pink-100 text-pink-800'; + break; + default: + typeName = '默认'; + badgeClass = 'bg-gray-100 text-gray-800'; + } + + return { fieldName, fieldType, typeName, badgeClass }; + }; + + // 渲染提示词类型选择 + const renderPromptTypeSelect = (value: string, type: 'llm' | 'vlm') => { + return ( + + ); + }; + + // 处理提示词类型变化 + const handlePromptTypeChange = ( + e: React.ChangeEvent, + type: 'llm' | 'vlm' + ) => { + setPromptType({ + ...promptType, + [type]: e.target.value + }); + + setHasPendingChanges(true); + }; + + // 处理提示词模板变化 + const handleTemplateChange = ( + e: React.ChangeEvent, + type: 'llm' | 'vlm' + ) => { + const templateId = e.target.value; + setSelectedTemplate({ + ...selectedTemplate, + [type]: templateId + }); + + // 这里可以根据模板ID获取模板内容 + const templateContent = getTemplateContent(templateId); + if (templateContent) { + setPromptContent({ + ...promptContent, + [type]: templateContent + }); + } + + setHasPendingChanges(true); + }; + + // 获取模板内容 + const getTemplateContent = (templateId: string) => { + // 模拟模板内容,实际应从API获取或配置中读取 + const templates: Record = { + '1': '请从以下文档中提取关键信息,以JSON格式返回以下字段:${fieldsList}', + '4': '请从以下采购合同中提取乙方资质信息,包括:${fieldsList}', + '5': '请从以下合同中提取关键条款,包括:${fieldsList}', + '6': '请从以下烟草许可证中提取信息:${fieldsList}', + '7': '请识别文档中的印章信息,提取以下字段:${fieldsList}', + '8': '请从文档表格中提取以下信息:${fieldsList}', + '9': '请识别文档中的手写内容,提取以下字段:${fieldsList}' + }; + + return templates[templateId] || ''; + }; + + // 处理提示词内容变化 + const handlePromptContentChange = ( + e: React.ChangeEvent, + type: 'llm' | 'vlm' + ) => { + setPromptContent({ + ...promptContent, + [type]: e.target.value + }); + + setHasPendingChanges(true); + }; + + // 将变量应用到提示词 + const applyVariableToPrompt = (variable: string, type: 'llm' | 'vlm') => { + const variableText = `\${${variable}}`; + setPromptContent({ + ...promptContent, + [type]: promptContent[type] + variableText + }); + + setHasPendingChanges(true); + }; + + // 渲染VLM字段类型选择 + const renderVlmFieldTypeSelect = () => { + return ( + + ); + }; + + // 添加正则字段行 + const addRegexFieldRow = () => { + const newField = { + field: '', + pattern: '' + }; + + setRegexFields([...regexFields, newField]); + setHasPendingChanges(true); + }; + + // 移除正则字段行 + const removeRegexFieldRow = (index: number) => { + const newFields = [...regexFields]; + newFields.splice(index, 1); + setRegexFields(newFields); + + setHasPendingChanges(true); + }; + + // 更新正则字段 + const updateRegexField = (index: number, property: 'field' | 'pattern', value: string) => { + const newFields = [...regexFields]; + newFields[index] = { + ...newFields[index], + [property]: value + }; + + setRegexFields(newFields); + setHasPendingChanges(true); + }; + + // 处理正则字段失去焦点 + const handleRegexFieldBlur = (index: number, property: 'field' | 'pattern') => { + // 显示暂时状态消息 + if (property === 'pattern' && regexFields[index].pattern) { + setStatusMessage({ + id: `field-${index}`, + message: '正则表达式已更新' + }); + + // 3秒后清除消息 + setTimeout(() => { + setStatusMessage(null); + }, 3000); + } + }; + + // 应用正则模板 + const applyRegexTemplate = (regex: string) => { + // 如果有字段,应用到最后一个字段 + if (regexFields.length > 0) { + const lastIndex = regexFields.length - 1; + updateRegexField(lastIndex, 'pattern', regex); + } + + setHasPendingChanges(true); + }; + + // 处理更新全部字段 + const handleUpdateFields = () => { + // 过滤掉没有字段名的正则字段 + const validRegexFields = regexFields.filter(field => field.field.trim() !== ''); + + // 收集所有字段数据 + const updatedFormData = { + ...formData, + extraction_config: { + llm: { + fields: fields.llm, + prompt_setting: { + type: promptType.llm, + template: promptType.llm === 'custom' ? promptContent.llm : '' + } + }, + vlm: { + fields: fields.vlm, + prompt_setting: { + type: promptType.vlm, + template: promptType.vlm === 'custom' ? promptContent.vlm : '' + } + }, + regex: { + fields: validRegexFields + } + } + }; + + // 验证字段唯一性 + const allFieldNames = [ + ...fields.llm, + ...fields.vlm.map(f => typeof f === 'string' ? f : f.name), + ...validRegexFields.map(f => f.field) + ]; + + const duplicates = allFieldNames.filter((item, index) => + allFieldNames.indexOf(item) !== index + ); + + if (duplicates.length > 0) { + setUpdateStatus({ + success: false, + message: `发现重复字段名:${duplicates.join(', ')},请修改后再提交` + }); + return; + } + + // 更新数据 + setFormData(updatedFormData); + + // 调用父组件的onChange回调 + if (onChange) { + onChange(updatedFormData); + + // 同时通过RuleContext上下文更新字段列表,确保评查设置组件能立即使用 + if (typeof window !== 'undefined') { + // 使用setTimeout确保在React更新周期之外执行 + setTimeout(() => { + // 触发一个自定义事件,通知RuleContext更新 + const event = new CustomEvent('extraction-fields-updated', { + detail: { fields: allFieldNames } + }); + window.dispatchEvent(event); + }, 0); + } + + // 显示成功消息 + setUpdateStatus({ + success: true, + message: `更新成功! 共更新 ${fields.llm.length} 个大模型字段, ${fields.vlm.length} 个多模态字段, ${validRegexFields.length} 个正则字段` + }); + + // 重置更改状态 + setHasPendingChanges(false); + + // 3秒后清除状态消息 + setTimeout(() => { + setUpdateStatus(null); + }, 3000); + } + }; + + return ( +
+
+

抽取设置

+
+
+
+
+ + + +
+
+ +
+
+
+ +
+ handleFieldInputChange(e, "llm")} + onKeyDown={(e) => handleKeyDown(e, "llm")} + autoComplete="off" + /> + +
+
+ {fields.llm.map((field, index) => ( +
+ {field} + removeField("llm", index)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") + removeField("llm", index); + }} + role="button" + tabIndex={0} + aria-label={`删除字段 ${field}`} + > + × + +
+ ))} +
+
+ 支持一次输入多个字段,用逗号、空格或顿号分隔 +
+
+
+ +
+
+ +
+ {renderPromptTypeSelect(promptType.llm, "llm")} +
+ +
+ 系统将根据评查点类型和抽取目标自动生成适合的提示词,您无需额外配置。 +
+ +
+
+ + +
+
+ + +
+

+ 支持的变量 + (点击变量将其添加到提示词中): +

+
+ {[ + "docType", + "fieldsList", + "companyName", + "documentId", + "date", + "industry", + "ocrText", + ].map((variable) => ( + + ))} +
+
+
+
+
+
+
+ +
+
+
+ +
+ handleFieldInputChange(e, "vlm")} + onKeyDown={(e) => handleKeyDown(e, "vlm")} + autoComplete="off" + /> + {renderVlmFieldTypeSelect()} + +
+
+ {fields.vlm.map((field, index) => { + const { fieldName, fieldType, typeName, badgeClass } = + getFieldInfo( + typeof field === "string" + ? field + : `${field.name}_${field.type}` + ); + return ( +
+ {fieldName} + + {typeName} + + removeField("vlm", index)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") + removeField("vlm", index); + }} + role="button" + tabIndex={0} + aria-label={`删除字段 ${fieldName}`} + > + × + +
+ ); + })} +
+
+ 请为每个字段选择适当的抽取类型,有助于提高识别准确率 +
+
+
+ +
+
+ +
+ {renderPromptTypeSelect(promptType.vlm, "vlm")} +
+
+ 系统将根据评查点类型和抽取目标自动生成适合的提示词,支持图表、印章等图像内容抽取。 +
+ +
+
+ + +
+
+ + +
+

+ 支持的变量 + (点击变量将其添加到提示词中): +

+
+ {[ + "docType", + "fieldsList", + "companyName", + "documentId", + "date", + "industry", + "contentType", + "pageRange", + "colorMode", + "ocrText", + ].map((variable) => ( + + ))} +
+
+
+
+
+
+
+ +
+
+
+
+
+ + +
+ +
+ {regexFields.map((field, index) => ( +
+
+ + + updateRegexField( + index, + "field", + e.target.value + ) + } + onBlur={() => + handleRegexFieldBlur(index, "field") + } + autoComplete="off" + /> +
+
+ + + updateRegexField( + index, + "pattern", + e.target.value + ) + } + onBlur={() => + handleRegexFieldBlur(index, "pattern") + } + autoComplete="off" + /> + {statusMessage && statusMessage.id === `field-${index}` && ( +
+ {statusMessage.message} +
+ )} + {!field.pattern && ( +
+ 未设置正则表达式,此字段将保留但不会执行抽取 +
+ )} +
+
+ +
+
+ ))} +
+
+
+ +
+ {[ + { + label: "日期格式:yyyy-mm-dd", + regex: + "\\d{4}[-/年](0?[1-9]|1[0-2])[-/月](0?[1-9]|[12][0-9]|3[01])[日]?", + }, + { label: "合同编号格式", regex: "[A-Z]{2,5}-\\d{4,10}" }, + { + label: "金额格式", + regex: + "(人民币|RMB)?\\s?(\\d{1,3}(,\\d{3})*(\\.\\d{2})?)\\s?[万元]?", + }, + { + label: "座机号码格式", + regex: "\\d{3}-\\d{8}|\\d{4}-\\d{7,8}", + }, + { label: "手机号码格式", regex: "1[3-9]\\d{9}" }, + ].map(({ label, regex }) => ( +
applyRegexTemplate(regex)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") + applyRegexTemplate(regex); + }} + > + {label} +
+ ))} +
+
+
+
+
+ + {/* 在所有标签页外部添加统一的更新按钮和状态显示,这样在任何标签页都可见 */} +
+
+ {hasPendingChanges && !updateStatus && ( +
+ 您有未更新的字段变更,请点击下方的“更新全部字段”按钮提交所有标签页的字段变更 +
+ )} + {!hasPendingChanges && !updateStatus && ( +
+ 提示:请在完成所有标签页的字段编辑后,点击此按钮更新所有字段 +
+ )} + +
+ {updateStatus && ( +
+ {updateStatus.message} +
+ )} +
+
+
+ ); +} diff --git a/app/components/rules/new/PageHeader.tsx b/app/components/rules/new/PageHeader.tsx new file mode 100644 index 0000000..37d2776 --- /dev/null +++ b/app/components/rules/new/PageHeader.tsx @@ -0,0 +1,21 @@ +interface PageHeaderProps { + title: string; + onSave?: () => void; +} + +export function PageHeader({ title, onSave }: PageHeaderProps) { + return ( +
+

{title}

+
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx new file mode 100644 index 0000000..1b9ee29 --- /dev/null +++ b/app/components/rules/new/ReviewSettings.tsx @@ -0,0 +1,2191 @@ +import React, { useState, useEffect, useContext, useCallback, useRef } from 'react'; +import { SimpleCodeEditor } from './SimpleCodeEditor'; +import { RuleContext } from '~/contexts/RuleContext'; +import { processFieldNames, areArraysDifferent, getArrayDifference } from '~/utils'; + +interface RuleType { + id: string; + type: string; + config: Record; +} + +// 为配置项添加类型定义 +interface ComparisonPair { + sourceField: string; + targetField: string; + compareMethod: string; +} + +// 添加逻辑条件接口 +interface Condition { + field: string; + operator: string; + value: string; +} + +interface ReviewSettingsProps { + onChange?: (data: Record) => void; + initialData?: { + rules?: RuleType[]; + combinationLogic?: string; + customLogic?: string; + pass_message?: string; + fail_message?: string; + suggestion_message?: string; + suggestion_message_type?: string; + post_action?: string; + action_config?: string; + score?: number; + scoreDisplay?: string; + }; + // 添加选项数据参数 + ruleTypeOptions?: Array<{ value: string; label: string }>; + logicTypeOptions?: Array<{ value: string; label: string }>; + logicOperatorOptions?: Array<{ value: string; label: string }>; + compareMethodOptions?: Array<{ value: string; label: string }>; + formatTypeOptions?: Array<{ value: string; label: string }>; + comparisonOperatorOptions?: Array<{ value: string; label: string }>; + matchTypeOptions?: Array<{ value: string; label: string }>; + suggestionMessageTypeOptions?: Array<{ value: string; label: string }>; + postActionOptions?: Array<{ value: string; label: string }>; +} + +export function ReviewSettings({ + onChange, + initialData, +}: ReviewSettingsProps) { + const [rules, setRules] = useState([ + { id: '1', type: '', config: {} } + ]); + const [combinationLogic, setCombinationLogic] = useState('and'); + const [customLogic, setCustomLogic] = useState(''); + const [showCustomLogic, setShowCustomLogic] = useState(false); + // 添加评查后动作相关状态 + const [post_action, setPostAction] = useState('none'); + const [action_config, setActionConfig] = useState(''); + // 添加分数状态 + const [score, setScore] = useState(0); + const [scoreDisplay, setScoreDisplay] = useState(''); + + // 获取抽取字段的上下文 + const { extractionFields } = useContext(RuleContext); + + // 初始化评查通过/不通过/建议信息 + const [pass_message, setPassMessage] = useState('文档检查通过,符合规范要求。'); + const [fail_message, setFailMessage] = useState('文档存在以下问题,请修改后重新提交。'); + const [suggestion_message, setSuggestMessage] = useState(''); + + // 提示类型 + const [suggestion_message_type, setSuggestionMessageType] = useState('warning'); + + // 保存最近一次可用的字段列表 + const [availableFields, setAvailableFields] = useState( + // 初始化时就处理字段,去掉类型后缀 + processFieldNames(extractionFields || []) + ); + + // 使用useRef跟踪是否已经初始化过 + const initializedRef = useRef(false); + // 保存初始数据的引用,用于检测是否有实际变更 + const initialDataRef = useRef(null); + + // 生成评查配置并发送给父组件 + const generateEvaluationConfig = useCallback(() => { + // 构建评查配置对象 + const evaluationConfig = { + logicType: combinationLogic, + customLogic: customLogic, + rules: rules + .filter(rule => rule.type) // 过滤掉没有选择类型的规则 + .map(rule => { + // 处理不同规则类型的特殊配置 + const processedConfig = { ...rule.config }; + + switch(rule.type) { + case 'exists': + // 确保fields字段是数组 + if (!Array.isArray(processedConfig.fields)) { + processedConfig.fields = []; + } + + // 确保logic字段有值 + if (!processedConfig.logic) { + processedConfig.logic = 'and'; + } + break; + + case 'consistency': + // 确保pairs字段是数组 + if (!Array.isArray(processedConfig.pairs)) { + processedConfig.pairs = []; + } + + // 确保logic字段有值 + if (!processedConfig.logic) { + processedConfig.logic = 'and'; + } + break; + + case 'format': + // 确保field字段正确设置 + if (processedConfig.checkField) { + processedConfig.field = processedConfig.checkField; + delete processedConfig.checkField; + } + + // 确保field字段有值 + if (!processedConfig.field) { + processedConfig.field = ''; + } + + if (processedConfig.formatParams) { + processedConfig.parameters = processedConfig.formatParams; + delete processedConfig.formatParams; + } + + // 确保formatType字段有值 + if (!processedConfig.formatType) { + processedConfig.formatType = ''; + } + + // 确保parameters字段有值 + if (!processedConfig.parameters) { + processedConfig.parameters = ''; + } + break; + + case 'regex': + // 确保field和pattern字段正确设置 + if (processedConfig.checkField) { + processedConfig.field = processedConfig.checkField; + delete processedConfig.checkField; + } + + // 确保field字段有值 + if (!processedConfig.field) { + processedConfig.field = ''; + } + + if (processedConfig.regexPattern) { + processedConfig.pattern = processedConfig.regexPattern; + delete processedConfig.regexPattern; + } + + // 确保pattern字段有值 + if (!processedConfig.pattern) { + processedConfig.pattern = ''; + } + + // 确保matchType字段有值 + if (!processedConfig.matchType) { + processedConfig.matchType = 'match'; + } + break; + + case 'ai': + // 确保model字段有值 + if (!processedConfig.model) { + processedConfig.model = 'qwen14b'; + } + + // 确保temperature字段是数字 + if (typeof processedConfig.temperature !== 'number') { + processedConfig.temperature = 0.1; + } + + // 确保prompt字段有值 + if (!processedConfig.prompt) { + processedConfig.prompt = ''; + } + break; + + case 'code': + // 确保language字段有值 + if (!processedConfig.language) { + processedConfig.language = 'javascript'; + } + + // 确保code字段有值 + if (!processedConfig.code) { + processedConfig.code = ''; + } + break; + } + + // 移除辅助用的UI字段 + delete processedConfig.availableFields; + + return { + id: rule.id, + type: rule.type, + config: processedConfig + }; + }) + }; + + // 使用setTimeout避免连锁更新 + setTimeout(() => { + if (onChange) { + // 仅将一个evaluation_config对象传递给父组件 + onChange({ evaluation_config: evaluationConfig }); + } + }, 0); + + return evaluationConfig; + }, [rules, combinationLogic, customLogic, onChange]); + + // 加载初始数据 + useEffect(() => { + // 如果已经初始化过,则跳过此次处理 + if (initializedRef.current) { + console.log("ReviewSettings已初始化,跳过后续初始化处理"); + return; + } + + // 记录初始化处理 + console.log("ReviewSettings开始初始化,数据:", initialData); + + // 保存初始数据引用,用于后续比较 + initialDataRef.current = JSON.parse(JSON.stringify(initialData)); + + // 设置已初始化标记 + initializedRef.current = true; + + // 只有在有initialData时才进行初始化设置 + if (initialData) { + // 处理初始规则数据 + if (initialData.rules && Array.isArray(initialData.rules) && initialData.rules.length > 0) { + console.log("设置初始规则数据:", initialData.rules); + + const validRules = initialData.rules.map(rule => { + // 确保每个规则都有id + if (!rule.id) { + rule.id = `rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 确保配置对象存在 + if (!rule.config) { + rule.config = {}; + } + + // 添加可用字段 + if (availableFields.length > 0) { + rule.config.availableFields = availableFields; + } + + return rule; + }); + + // 如果没有规则或规则为空,添加一个默认规则 + if (validRules.length === 0) { + validRules.push({ id: '1', type: '', config: { availableFields } }); + } + + setRules(validRules); + } else { + // 如果rules为空或不是数组,添加一个默认规则 + setRules([{ id: '1', type: '', config: { availableFields } }]); + } + + // 设置组合逻辑 + if (initialData.combinationLogic) { + setCombinationLogic(initialData.combinationLogic); + if (initialData.combinationLogic === 'custom') { + setShowCustomLogic(true); + } + } + + // 设置自定义逻辑 + if (initialData.customLogic) { + setCustomLogic(initialData.customLogic); + } + + // 设置通过/不通过消息 + if (initialData.pass_message) { + setPassMessage(initialData.pass_message); + } + + if (initialData.fail_message) { + setFailMessage(initialData.fail_message); + } + + // 设置建议消息 + if (initialData.suggestion_message) { + setSuggestMessage(initialData.suggestion_message); + } + + if (initialData.suggestion_message_type) { + setSuggestionMessageType(initialData.suggestion_message_type); + } + + // 设置后处理动作 + if (initialData.post_action) { + setPostAction(initialData.post_action); + } + + if (initialData.action_config) { + setActionConfig(initialData.action_config); + } + + // 设置分数 + if (initialData.score !== undefined) { + setScore(initialData.score); + } + + // 设置分数显示值 + if (initialData.scoreDisplay) { + setScoreDisplay(initialData.scoreDisplay); + } else if (initialData.score !== undefined && initialData.score > 0) { + setScoreDisplay(String(initialData.score)); + } + + // 数据加载完成后,生成一次完整的评查配置 + setTimeout(() => { + generateEvaluationConfig(); + }, 0); + } + // 移除availableFields依赖,避免死循环 + }, [initialData, availableFields, generateEvaluationConfig]); + + // 监听extractionFields的变化 + useEffect(() => { + if (extractionFields && extractionFields.length > 0) { + // 使用工具函数处理字段 + const uniqueFields = processFieldNames(extractionFields); + + // 只在字段列表实际发生变化时更新 + if (areArraysDifferent(uniqueFields, availableFields)) { + // 检查删除和新增的字段 + const { removed } = getArrayDifference(uniqueFields, availableFields); + + // 处理删除的字段 + if (removed.length > 0) { + handleDeletedFields(removed); + } + + // 更新可用字段 + setAvailableFields(uniqueFields); + + // 使用最新的字段更新规则 + updateRulesWithNewFields(uniqueFields); + } + } + }, [extractionFields, availableFields]); + + // 检查并更新字段(仍然保留此函数供需要时手动触发) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const checkAndUpdateFields = () => { + if (extractionFields.length > 0) { + // 处理字段,去掉类型后缀 + const processedFields = extractionFields.map(field => { + if (field.includes('_')) { + return field.split('_')[0]; // 只保留类型前面的字段名 + } + return field; + }); + + // 去重 + const uniqueFields = [...new Set(processedFields)]; + + // 检查是否有字段被删除 + const deletedFields = availableFields.filter(field => !uniqueFields.includes(field)); + + // 处理新增的字段 + const newFields = uniqueFields.filter((field: string) => !availableFields.includes(field)); + + if (newFields.length > 0 || deletedFields.length > 0) { + console.log('Updating fields in checkAndUpdateFields - deleted:', deletedFields, 'new:', newFields); + // 设置最新的可用字段列表 + setAvailableFields(uniqueFields); + + // 处理规则中已删除的字段 + if (deletedFields.length > 0) { + handleDeletedFields(deletedFields); + } + + // 使用最新的字段列表更新规则配置 + updateRulesWithNewFields(uniqueFields); + + return true; // 表示字段已更新 + } + } + return false; // 表示字段未更新 + }; + + // 初始化评查配置 + useEffect(() => { + // 生成并更新评查配置 + generateEvaluationConfig(); + }, [generateEvaluationConfig]); + + // 处理已删除字段的函数 + const handleDeletedFields = (deletedFields: string[]) => { + setRules(prevRules => { + return prevRules.map(rule => { + const updatedConfig = { ...rule.config }; + + switch (rule.type) { + case 'exists': + case 'logic': + case 'regex': + // 从已选字段中移除被删除的字段 + if (Array.isArray(updatedConfig.selectedFields)) { + updatedConfig.selectedFields = (updatedConfig.selectedFields as string[]).filter( + field => !deletedFields.includes(field) + ); + } + break; + + case 'consistency': + // 从配对字段中移除被删除的字段 + if (Array.isArray(updatedConfig.pairs)) { + updatedConfig.pairs = (updatedConfig.pairs as ComparisonPair[]).filter( + pair => !deletedFields.includes(pair.sourceField) && !deletedFields.includes(pair.targetField) + ); + } + break; + + case 'format': + // 如果判断字段被删除,则清空字段 + if (updatedConfig.checkField && deletedFields.includes(updatedConfig.checkField as string)) { + updatedConfig.checkField = ''; + } + break; + + default: + break; + } + + // 更新可用字段列表,移除被删除的字段 + if (Array.isArray(updatedConfig.availableFields)) { + updatedConfig.availableFields = (updatedConfig.availableFields as string[]).filter( + field => !deletedFields.includes(field) + ); + } + + return { + ...rule, + config: updatedConfig + }; + }); + }); + }; + + // 更新规则配置中的可用字段但保留已选择的字段和规则配置 + const updateRulesWithNewFields = (newFields: string[]) => { + // 更新每个规则的可用字段列表,但保留现有配置 + setRules(prevRules => { + return prevRules.map(rule => { + const updatedConfig = { ...rule.config }; + + // 对所有规则类型都更新availableFields字段 + // 处理字段,只保留字段名,去掉类型后缀 + const processedFields = newFields.map(field => { + if (field.includes('_')) { + return field.split('_')[0]; // 只保留类型前面的字段名 + } + return field; + }); + + // 去重 + const uniqueFields = [...new Set(processedFields)]; + updatedConfig.availableFields = uniqueFields; + + // 根据规则类型更新其他相关字段 + if (rule.type) { + switch (rule.type) { + case 'field_validation': + // 保留已有的字段选择,只添加新字段 + if (!updatedConfig.fields) { + updatedConfig.fields = []; + } + break; + + case 'field_comparison': + // 保留已配置的比较项 + if (!updatedConfig.pairs) { + updatedConfig.pairs = []; + } + break; + + case 'field_regex': + // 保留正则表达式配置 + break; + + case 'custom_code': + break; + + default: + // 对于所有类型规则,确保selectedFields字段存在 + if (!updatedConfig.selectedFields) { + updatedConfig.selectedFields = []; + } + break; + } + } + + return { + ...rule, + config: updatedConfig + }; + }); + }); + }; + + const handleLogicChange = (logic: string) => { + setCombinationLogic(logic); + setShowCustomLogic(logic === 'custom'); + + if (onChange) { + // 确保将完整的数据传递给父组件 + const updateData = { + combinationLogic: logic, + // 如果切换到自定义逻辑,同时传递自定义逻辑内容 + customLogic: logic === 'custom' ? customLogic : '' + }; + + onChange(updateData); + + // 生成完整的评查配置 + setTimeout(() => generateEvaluationConfig(), 0); + } + }; + + const handleCustomLogicChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setCustomLogic(value); + + if (onChange) { + onChange({ customLogic: value }); + } + }; + + const handleAddRule = () => { + const newId = `${rules.length + 1}`; + const newRule = { id: newId, type: '', config: {} }; + setRules([...rules, newRule]); + + if (onChange) { + onChange({ rules: [...rules, newRule] }); + } + }; + + const handleRemoveRule = (id: string) => { + // 如果只有一个规则,不允许删除 + if (rules.length <= 1) { + return; + } + + const newRules = rules.filter(rule => rule.id !== id); + setRules(newRules); + + // 重新编号规则 + const reindexedRules = newRules.map((rule, index) => ({ + ...rule, + id: `${index + 1}` + })); + + setRules(reindexedRules); + + if (onChange) { + onChange({ rules: reindexedRules }); + } + }; + + // 处理规则类型变更 + const handleRuleTypeChange = (id: string, type: string) => { + const newRules = rules.map(rule => { + if (rule.id === id) { + // 查找原始规则以获取现有配置 + const originalRule = rules.find(r => r.id === id); + const originalConfig = originalRule ? originalRule.config : {}; + + // 为新类型初始化配置 + let initialConfig: Record = {}; + + // 如果类型没变,保留原配置 + if (type === rule.type) { + initialConfig = { ...originalConfig }; + } else { + // 根据类型设置初始配置 + switch(type) { + case 'exists': + initialConfig = { + fields: Array.isArray(originalConfig.fields) ? originalConfig.fields : [], + logic: originalConfig.logic || originalConfig.logicRelation || 'and', + availableFields: availableFields + }; + break; + case 'consistency': + initialConfig = { + pairs: Array.isArray(originalConfig.pairs) ? originalConfig.pairs : [], + logic: originalConfig.logic || originalConfig.logicRelation || 'and', + availableFields: availableFields + }; + break; + case 'format': + initialConfig = { + field: originalConfig.field || '', + checkField: originalConfig.checkField || originalConfig.field || '', + formatType: originalConfig.formatType || 'date', + parameters: originalConfig.parameters || '', + formatParams: originalConfig.formatParams || originalConfig.parameters || '', + availableFields: availableFields + }; + break; + case 'logic': + initialConfig = { + conditions: Array.isArray(originalConfig.conditions) ? originalConfig.conditions : [], + logic: originalConfig.logic || originalConfig.logicRelation || 'and', + initialField: '', + initialOperator: 'eq', + initialValue: '', + availableFields: availableFields + }; + break; + case 'regex': + initialConfig = { + field: originalConfig.field || '', + checkField: originalConfig.checkField || originalConfig.field || '', + pattern: originalConfig.pattern || '', + regexPattern: originalConfig.regexPattern || originalConfig.pattern || '', + matchType: originalConfig.matchType || 'match', + availableFields: availableFields + }; + break; + case 'ai': + initialConfig = { + model: originalConfig.model || 'qwen14b', + temperature: typeof originalConfig.temperature === 'number' ? originalConfig.temperature : 0.1, + prompt: originalConfig.prompt || `请判断以下{字段}内容是否符合规范要求,仅回答"符合"或"不符合",并简要说明理由。 + +{字段内容}`, + availableFields: availableFields + }; + break; + case 'code': + initialConfig = { + language: originalConfig.language || 'javascript', + code: originalConfig.code || '', + availableFields: availableFields + }; + break; + default: + initialConfig = { + availableFields: availableFields + }; + } + } + + return { + ...rule, + type, + config: initialConfig + }; + } + return rule; + }); + + setRules(newRules); + + if (onChange) { + onChange({ rules: newRules }); + } + + // 更新评查配置 + generateEvaluationConfig(); + }; + + // 处理规则配置变更 + const handleRuleConfigChange = (id: string, configChanges: Record) => { + const newRules = rules.map(rule => { + if (rule.id === id) { + // 处理特殊的字段映射,确保不同名称的字段保持同步 + const processedChanges = { ...configChanges }; + + // 对于格式判断,确保checkField和field字段同步 + if (rule.type === 'format' && 'checkField' in configChanges) { + processedChanges.field = configChanges.checkField; + } + + // 对于正则判断,确保字段名和模式保持同步 + if (rule.type === 'regex') { + if ('checkField' in configChanges) { + processedChanges.field = configChanges.checkField; + } + if ('regexPattern' in configChanges) { + processedChanges.pattern = configChanges.regexPattern; + } + } + + return { ...rule, config: { ...rule.config, ...processedChanges } }; + } + return rule; + }); + + setRules(newRules); + + if (onChange) { + // 立即触发父组件的onChange回调,确保数据能保存到父组件 + onChange({ rules: newRules }); + } + }; + + // 渲染字段标签,确保已选择的字段即使在新的字段列表中不存在也会显示 + const renderFieldTags = (ruleId: string, config: Record) => { + // 获取规则的当前已选字段 + // 修复:对于exists类型规则,应该使用fields而不是selectedFields + const selectedFields = Array.isArray(config.fields) ? + config.fields as string[] : + (Array.isArray(config.selectedFields) ? config.selectedFields as string[] : []); + + // 优先使用配置中存储的可用字段,如果没有则使用当前可用字段 + const fieldsToRender = Array.isArray(config.availableFields) ? + config.availableFields as string[] : + availableFields; + + return ( +
+ {fieldsToRender.map((field, index) => { + // 使用includes方法检查选中状态 + const isSelected = selectedFields.includes(field); + + return ( +
{ + // 切换选中状态 + const newSelectedFields = isSelected + ? selectedFields.filter(f => f !== field) + : [...selectedFields, field]; + + // 更新规则配置 + handleRuleConfigChange(ruleId, { + fields: newSelectedFields + }); + + // 直接触发配置更新 + generateEvaluationConfig(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + const newSelectedFields = isSelected + ? selectedFields.filter(f => f !== field) + : [...selectedFields, field]; + + handleRuleConfigChange(ruleId, { + fields: newSelectedFields + }); + + // 直接触发配置更新 + generateEvaluationConfig(); + } + }} + role="button" + tabIndex={0} + > + {field} +
+ ); + })} +
+ ); + }; + + // 获取规则类型的Badge样式 + const getRuleTypeBadgeClass = (type: string) => { + switch(type) { + case 'exists': return 'bg-green-500'; + case 'consistency': return 'bg-blue-500'; + case 'format': return 'bg-purple-500'; + case 'logic': return 'bg-yellow-500'; + case 'regex': return 'bg-red-500'; + case 'ai': return 'bg-indigo-500'; + case 'code': return 'bg-gray-700'; + default: return 'bg-primary'; + } + }; + + // 渲染规则配置区域 + const renderRuleConfig = (rule: RuleType) => { + const { id, type, config } = rule; + + // 如果规则中的availableFields不是最新的,则更新它 + if (type && config && (!config.availableFields || + (Array.isArray(config.availableFields) && + !availableFields.every((field) => (config.availableFields as string[]).includes(field)) || + !(config.availableFields as string[]).every((field) => availableFields.includes(field))))) { + // 延迟更新以避免在渲染过程中修改状态 + setTimeout(() => { + console.log('Updating rule config with new available fields:', availableFields); + const updatedConfig = { ...config, availableFields: availableFields }; + handleRuleConfigChange(id, updatedConfig); + }, 0); + } + + if (!type) { + return ( +
+ +

请先选择评查类型

+
+ ); + } + + switch(type) { + case 'exists': + return ( +
+
+ +
+ {renderFieldTags(id, config)} +
+
点击选择需要判断是否存在的字段,已选中的字段会高亮显示
+
+
+ +
+ + +
+
+
+ ); + + case 'consistency': + return ( +
+
+ +
+ {Array.isArray(config.pairs) && config.pairs.length > 0 ? ( + config.pairs.map((pair, pairIndex) => ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ )) + ) : ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ )} +
+
+ +
+
+
+
+ 逻辑关系 * +
+ + +
+
+
+
+ ); + + case 'logic': + return ( +
+
+ +
+ {Array.isArray(config.conditions) && config.conditions.length > 0 ? ( + config.conditions.map((condition, conditionIndex) => ( +
+
+
+ + +
+
+ + +
+
+ + { + const currentConditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : []; + currentConditions[conditionIndex] = { ...currentConditions[conditionIndex], value: e.target.value }; + handleRuleConfigChange(id, { conditions: currentConditions }); + // 触发配置更新 + generateEvaluationConfig(); + }} + /> +
+
+
+ +
+
+ )) + ) : ( +
+
+
+ + +
+
+ + +
+
+ + { + // 获取field和operator的值 + const field = (config.initialField as string) || ''; + const operator = (config.initialOperator as string) || 'eq'; + const value = e.target.value; + + // 如果已经设置了字段,则创建条件 + if (field) { + handleRuleConfigChange(id, { + initialValue: value, + conditions: [{ field, operator, value }] + }); + } else { + // 否则只保存值 + handleRuleConfigChange(id, { initialValue: value }); + } + // 触发配置更新 + generateEvaluationConfig(); + }} + /> +
+
+
+ )} +
+
+ +
+
+
+
+ 逻辑关系 * +
+ + +
+
+
+
+ ); + + case 'regex': + return ( +
+
+ + +
+
+ + { + handleRuleConfigChange(id, { + regexPattern: e.target.value, + pattern: e.target.value // 同步更新内部字段 + }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} + /> +
+ 输入标准正则表达式,例如: "^[a-zA-Z0-9]+$" 表示仅允许字母和数字 +
+
+
+ +
+ + +
+
+
+ ); + + case 'ai': + return ( +
+
+
+ + +
+
+ + { + const value = e.target.value; + const numberValue = value === '' ? 0.1 : parseFloat(value); + handleRuleConfigChange(id, { temperature: numberValue }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} + /> +
+
+ + +
+
+ {availableFields.map((field, idx) => ( + + ))} +
+
+
+ ); + + case 'code': + return ( +
+
+
+ 代码语言 * +
+ + +
+
+
+
+ +
编写返回true或false的评查函数,可以使用字段变量进行判断
+ { + handleRuleConfigChange(id, { code: value }); + generateEvaluationConfig(); + }} + /> +
+
+ ); + + case 'format': + return ( +
+
+ + +
+
+ + +
+
+ + { + handleRuleConfigChange(id, { + formatParams: e.target.value, + parameters: e.target.value + }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} + /> +
+ 根据格式类型传入特定参数,如日期格式可传入"YYYY-MM-DD" +
+
+
+ ); + + default: + return ( +
+

已选择 {type} 类型规则,请继续配置。

+
+ ); + } + }; + + // 组件初次渲染后,主动发送一次完整配置数据 + useEffect(() => { + // 如果有初始数据,在组件挂载后主动发送一次完整规则配置 + if (initialDataRef.current && onChange) { + console.log("组件挂载后发送初始完整配置"); + setTimeout(() => generateEvaluationConfig(), 100); + } + }, [generateEvaluationConfig, onChange]); + + // 处理评查结果消息变更 + const handleMessageChange = (type: string, value: string) => { + switch(type) { + case 'pass': + setPassMessage(value); + break; + case 'fail': + setFailMessage(value); + break; + case 'suggest': + setSuggestMessage(value); + break; + } + + if (onChange) { + // 使用正确的字段名 + const fieldName = type === 'pass' ? 'pass_message' : + type === 'fail' ? 'fail_message' : + 'suggestion_message'; + onChange({ [fieldName]: value }); + } + }; + + // 处理严重程度变更 + const handleSeverityChange = (value: string) => { + setSuggestionMessageType(value); + + if (onChange) { + onChange({ suggestion_message_type: value }); + } + }; + + // 处理分数变更 + const handleScoreChange = (value: string) => { + // 保存用户输入的显示值 + setScoreDisplay(value); + + let scoreValue = 0; + // 只在值不为空时更新实际分数 + if (value.trim() !== '') { + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + scoreValue = Math.min(Math.max(numValue, 0), 100); + } + } + + // 更新状态 + setScore(scoreValue); + + // 通知父组件 + if (onChange) { + onChange({ score: scoreValue }); + } + }; + + return ( +
+ +
+

评查设置

+
+
+
+
+
+ +
+
+
+ + + +
+ + {showCustomLogic && ( +
+ + +
+ 使用规则编号和逻辑运算符(AND、OR、NOT)组合 +
+
+ )} +
+
+ +
+
+ +
+
+
+
已添加 {rules.length} 条规则
+
+ + {rules.length === 0 ? ( +
+
+ +

尚未添加任何规则

+

点击“添加规则”按钮开始创建评查规则

+
+
+ ) : ( +
+ {rules.map((rule) => ( +
+
+ + 规则 #{rule.id} + + +
+ +
+ + +
选择评查类型后将显示对应的配置项
+
+ +
+ {renderRuleConfig(rule)} +
+
+ ))} +
+ )} +
+
+ +
+ +
+ +
+ + {/* 评查结果提示信息 */} +
+

评查结果提示信息

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* 建议信息类别 */} +
+

建议信息类别

+
+
handleSeverityChange('info')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSeverityChange('info'); + } + }} + role="radio" + aria-checked={suggestion_message_type === 'info'} + tabIndex={0} + > + handleSeverityChange('info')} + /> + +
+
提示 (Info)
+
建议性提示,不影响评查结果
+
+ {suggestion_message_type === 'info' && ( +
+ +
+ )} +
+ +
handleSeverityChange('warning')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSeverityChange('warning'); + } + }} + role="radio" + aria-checked={suggestion_message_type === 'warning'} + tabIndex={0} + > + handleSeverityChange('warning')} + /> + +
+
警告 (Warning)
+
需引起注意的问题
+
+ {suggestion_message_type === 'warning' && ( +
+ +
+ )} +
+ +
handleSeverityChange('error')} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSeverityChange('error'); + } + }} + role="radio" + aria-checked={suggestion_message_type === 'error'} + tabIndex={0} + > + handleSeverityChange('error')} + /> + +
+
错误 (Error)
+
严重错误,必须修正
+
+ {suggestion_message_type === 'error' && ( +
+ +
+ )} +
+
+
不同类别会影响问题的展示方式和处理流程
+
+ + {/* 评查后动作 */} +
+ + +
+ + {/* 分数设置 */} +
+ +
+ handleScoreChange(e.target.value)} + onBlur={() => { + // 在失去焦点时,如果显示值为空,则设置为0 + if (scoreDisplay.trim() === '') { + setScoreDisplay('0'); + setScore(0); + if (onChange) { + onChange({ score: 0 }); + } + } else { + // 否则更新为实际分数值的字符串表示 + setScoreDisplay(String(score)); + } + }} + /> + +
+
该评查点的分值,范围0-100
+
+ + {/* 动作描述区域 */} + {post_action && post_action !== 'none' && ( +
+ + +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/SimpleCodeEditor.tsx b/app/components/rules/new/SimpleCodeEditor.tsx new file mode 100644 index 0000000..ab4fb06 --- /dev/null +++ b/app/components/rules/new/SimpleCodeEditor.tsx @@ -0,0 +1,95 @@ +import { useState, useEffect } from 'react'; + +interface SimpleCodeEditorProps { + id: string; + initialValue?: string; + language?: 'javascript' | 'python'; + onChange?: (value: string) => void; +} + +export function SimpleCodeEditor({ + id, + initialValue = '', + language = 'javascript', + onChange +}: SimpleCodeEditorProps) { + const [code, setCode] = useState(initialValue); + const [copySuccess, setCopySuccess] = useState(false); + + // 复制代码到剪贴板 + const copyToClipboard = () => { + navigator.clipboard.writeText(code).then(() => { + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + }); + }; + + // 处理代码变化 + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setCode(value); + if (onChange) { + onChange(value); + } + }; + + // 更新初始值 + useEffect(() => { + if (initialValue !== undefined) { + setCode(initialValue); + } + }, [initialValue]); + + return ( +
+
+
+
{language === 'javascript' ? 'script.js' : 'script.py'}
+
+ +
+
+
+ +
+
+ {copySuccess && ( +
+ 代码已复制到剪贴板 +
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/contexts/RuleContext.tsx b/app/contexts/RuleContext.tsx new file mode 100644 index 0000000..6369a9e --- /dev/null +++ b/app/contexts/RuleContext.tsx @@ -0,0 +1,26 @@ +import { createContext } from 'react'; + +/** + * 规则上下文类型 + * 用于在抽取设置和评查设置之间共享数据 + */ +export interface RuleContextType { + /** + * 抽取的字段列表 + */ + extractionFields: string[]; + + /** + * 更新字段列表的函数 + */ + updateFields: (fields: string[]) => void; +} + +/** + * 创建规则上下文 + * 用于在抽取设置和评查设置组件之间共享字段数据 + */ +export const RuleContext = createContext({ + extractionFields: [], + updateFields: () => {} +}); \ No newline at end of file diff --git a/app/entry.client.tsx b/app/entry.client.tsx index 94d5dc0..30656b8 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -5,14 +5,12 @@ */ import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; +import { startTransition } from "react"; import { hydrateRoot } from "react-dom/client"; startTransition(() => { hydrateRoot( document, - - - + ); }); diff --git a/app/models/evaluation_point_groups.ts b/app/models/evaluation_point_groups.ts new file mode 100644 index 0000000..0805d1d --- /dev/null +++ b/app/models/evaluation_point_groups.ts @@ -0,0 +1,15 @@ +/** + * 评查点分组表 + */ +interface EvaluationPointGroup { + id: number; // 主键,自增 + pid: number | null; // 所属分组ID,外键引用本表,可为空 + code: string; // 分组编码,唯一且非空 + name: string; // 分组名称,非空 + description?: string; // 分组描述,可为空 + is_enabled: boolean; // 是否启用,默认true + created_at: string; // 创建时间,带时区,默认当前时间 + updated_at: string; // 更新时间,带时区,默认当前时间 +} + +export type { EvaluationPointGroup }; diff --git a/app/models/evaluation_points.ts b/app/models/evaluation_points.ts new file mode 100644 index 0000000..578c2d3 --- /dev/null +++ b/app/models/evaluation_points.ts @@ -0,0 +1,308 @@ +/** + * 评查点数据类型定义 + */ +interface EvaluationPoint { + id?: number; + name?: string; // 评查点名称 + code?: string; // 评查点编码 + risk?: string; // 风险级别 + is_enabled?: boolean; // 是否启用 + description?: string; // 描述 + references_laws?: { // 法律法规引用 + name?: string; // 法规名称 + articles?: string[]; // 条款列表 + content?: string; // 法规内容 + }; + evaluation_point_groups_pid?: number | null; // 评查点类型组ID + evaluation_point_groups_id?: number | null; // 所属评查组ID + extraction_config?: ExtactionConfigType; // 抽取配置 + evaluation_config?: EvaluationConfigType; // 评查配置 + pass_message?: string; // 通过消息 + fail_message?: string; // 失败消息 + suggestion_message?: string; // 建议消息 + suggestion_message_type?: SuggestionMessageType; // 建议消息类型 默认warning(info/warning/error) + post_action?: PostActionType; // 评查后动作 可为空(none/manual/replace) + action_config?: string; // 动作配置 + score?: number; // 分数 +} + +/** + * 风险级别类型定义 + */ +type RiskLevelType = 'high' | 'medium' | 'low'; + +/** + * 建议消息类型定义 + */ +type SuggestionMessageType = 'info' | 'warning' | 'error'; + +/** + * 评查后动作类型定义 + */ +type PostActionType = 'none' | 'manual' | 'replace'; + +/** + * 抽取配置类型定义 + */ +interface ExtactionConfigType { + llm: { + fields: string[]; + prompt_setting: PromptSetting; + }; + vlm: { + fields: Array<{ + name: string; + type: VLMFieldType; // 多模态字段类型 默认、货币、打印、印章、骑缝章、英文、数字、手写 + }>; + prompt_setting: PromptSetting; + }; + regex: { + fields: Array<{ + field: string; + pattern: string; + }>; + }; +} + +/** + * VLM字段类型定义 + */ +type VLMFieldType = 'default' | 'currency' | 'print' | 'seal' | 'cross-seal' | 'english' | 'number' | 'handwriting'; + +/** + * 提示配置类型定义 + */ +interface PromptSetting { + type: PromptType; // "system" or "custom" 如果为 "custom" 则为自定义提示 + template: string; // 提示模板 +} + +/** + * 提示类型定义 + */ +type PromptType = 'system' | 'custom'; + +/** + * 评查配置类型定义 + */ +interface EvaluationConfigType { + logicType: LogicType; + customLogic: string; // 自定义逻辑:(规则1 AND 规则2) OR 规则3 + rules: Rule[]; +} + +/** + * 逻辑类型定义 + */ +type LogicType = 'and' | 'or' | 'custom'; + +interface Rule { + id: string; + type: RuleType; // 规则类型 有无、一致性、格式、逻辑、正则、AI、代码 + config: { + // exists 规则配置 + fields?: string[]; + logic?: LogicOperator; + + // consistency 规则配置 + pairs?: Array<{ + sourceField: string; + targetField: string; + compareMethod: CompareMethod; // 比较方法 包含、精确、大模型语义 + }>; + + // format 规则配置 + field?: string; + formatType?: FormatType; // 格式类型 日期格式、数字格式、电话号码、电子邮箱、银行卡号、身份证号码、邮政编码、统一社会信用代码 + parameters?: string; + + // logic 规则配置 + conditions?: Array<{ + field: string; + operator: ComparisonOperator; // 比较运算符 等于、不等于、大于、大于等于、小于、小于等于、包含、不包含 + value: string; + }>; + + // regex 规则配置 + pattern?: string; + matchType?: MatchType; // 匹配类型 必须匹配(符合为通过)、不得匹配(不符合为通过) + + // ai 规则配置 + model?: string; // 大模型名称 + temperature?: number; // 温度 + prompt?: string; // 提示 + + // code 规则配置 + language?: ProgrammingLanguage; + code?: string; + } +} + +/** + * 规则类型定义 + */ +type RuleType = 'exists' | 'consistency' | 'format' | 'logic' | 'regex' | 'ai' | 'code'; + +/** + * 逻辑操作符定义 + */ +type LogicOperator = 'and' | 'or'; + +/** + * 比较方法定义 + */ +type CompareMethod = 'contains' | 'exact' | 'semantic'; + +/** + * 格式类型定义 + */ +type FormatType = 'date' | 'number' | 'phone' | 'email' | 'bank' | 'id' | 'postal' | 'credit'; + +/** + * 比较运算符定义 + */ +type ComparisonOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains'; + +/** + * 匹配类型定义 + */ +type MatchType = 'match' | 'not_match'; + +/** + * 编程语言定义 + */ +type ProgrammingLanguage = 'python' | 'javascript'; + +/** + * 下拉选项数据集 + */ +export const EVALUATION_OPTIONS = { + // 风险级别选项 + riskLevelOptions: [ + { value: 'high', label: '高风险' }, + { value: 'medium', label: '中风险' }, + { value: 'low', label: '低风险' } + ], + + // 建议消息类型选项 + suggestionMessageTypeOptions: [ + { value: 'info', label: '提示' }, + { value: 'warning', label: '警告' }, + { value: 'error', label: '错误' } + ], + + // 评查后动作选项 + postActionOptions: [ + { value: 'none', label: '无' }, + { value: 'manual', label: '人工确认' }, + { value: 'replace', label: '内容替换' } + ], + + // VLM字段类型选项 + vlmFieldTypeOptions: [ + { value: 'default', label: '默认' }, + { value: 'currency', label: '货币' }, + { value: 'print', label: '打印' }, + { value: 'seal', label: '印章' }, + { value: 'cross-seal', label: '骑缝章' }, + { value: 'english', label: '英文' }, + { value: 'number', label: '数字' }, + { value: 'handwriting', label: '手写' } + ], + + // 提示类型选项 + promptTypeOptions: [ + { value: 'system', label: '使用系统默认提示词' }, + { value: 'custom', label: '使用自定义提示词' } + ], + + // 逻辑类型选项 + logicTypeOptions: [ + { value: 'and', label: '全部满足(AND)' }, + { value: 'or', label: '任一满足(OR)' }, + { value: 'custom', label: '自定义组合' } + ], + + // 规则类型选项 + ruleTypeOptions: [ + { value: 'exists', label: '有无判断' }, + { value: 'consistency', label: '一致性判断' }, + { value: 'format', label: '格式判断' }, + { value: 'logic', label: '逻辑判断' }, + { value: 'regex', label: '正则表达式' }, + { value: 'ai', label: '大模型判断' }, + { value: 'code', label: '自定义代码' } + ], + + // 逻辑操作符选项 + logicOperatorOptions: [ + { value: 'and', label: 'AND(所有条件都满足)' }, + { value: 'or', label: 'OR(任一条件满足)' } + ], + + // 比较方法选项 + compareMethodOptions: [ + { value: 'contains', label: '包含关系' }, + { value: 'exact', label: '精确匹配' }, + { value: 'semantic', label: '大模型语义匹配' } + ], + + // 格式类型选项 + formatTypeOptions: [ + { value: 'date', label: '日期格式' }, + { value: 'number', label: '数字格式' }, + { value: 'phone', label: '电话号码' }, + { value: 'email', label: '电子邮箱' }, + { value: 'bank', label: '银行卡号' }, + { value: 'id', label: '身份证号码' }, + { value: 'postal', label: '邮政编码' }, + { value: 'credit', label: '统一社会信用代码' } + ], + + // 比较运算符选项 + comparisonOperatorOptions: [ + { value: 'eq', label: '等于(=)' }, + { value: 'neq', label: '不等于(≠)' }, + { value: 'gt', label: '大于(>)' }, + { value: 'gte', label: '大于等于(≥)' }, + { value: 'lt', label: '小于(<)' }, + { value: 'lte', label: '小于等于(≤)' }, + { value: 'contains', label: '包含' }, + { value: 'not_contains', label: '不包含' } + ], + + // 匹配类型选项 + matchTypeOptions: [ + { value: 'match', label: '必须匹配(符合为通过)' }, + { value: 'not_match', label: '不得匹配(不符合为通过)' } + ], + + // 编程语言选项 + programmingLanguageOptions: [ + { value: 'python', label: 'Python' }, + { value: 'javascript', label: 'JavaScript' } + ] +}; + +// 导出类型定义供其他模块使用 +export type { + EvaluationPoint, + RiskLevelType, + SuggestionMessageType, + PostActionType, + ExtactionConfigType, + VLMFieldType, + PromptSetting, + PromptType, + EvaluationConfigType, + LogicType, + Rule, + RuleType, + LogicOperator, + CompareMethod, + FormatType, + ComparisonOperator, + MatchType, + ProgrammingLanguage +}; + diff --git a/app/root.tsx b/app/root.tsx index ce49e15..ea3e333 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -55,6 +55,19 @@ export default function App() { +