diff --git a/app/api/cross-checking/cross-files-upload.ts b/app/api/cross-checking/cross-files-upload.ts index d2b674a..1ef7341 100644 --- a/app/api/cross-checking/cross-files-upload.ts +++ b/app/api/cross-checking/cross-files-upload.ts @@ -1,5 +1,6 @@ import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config'; import axios from 'axios'; +import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from '~/constants/contractTypes'; /** * 从不同格式的 API 响应中提取数据 @@ -224,7 +225,8 @@ export async function batchUploadAndAssignCrossCheckingFiles( docType: string, taskType: string = '市局间交叉评查', token: string | null = null, - principalUserIds: number[] = [] + principalUserIds: number[] = [], + attributeType?: string ): Promise<{ successes: Array<{file: CrossCheckingUploadedFile; result: Record}>; failures: Array<{file: CrossCheckingUploadedFile; error: string}>; @@ -249,7 +251,8 @@ export async function batchUploadAndAssignCrossCheckingFiles( is_test_document: isTestDocument, task_name: taskName, doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType, - task_type: taskType + task_type: taskType, + attribute_type: attributeType || null }; // console.log('fileInfo', fileInfo) diff --git a/app/api/evaluation_points/reviews.ts b/app/api/evaluation_points/reviews.ts index cfcbe9f..2c6c0e3 100644 --- a/app/api/evaluation_points/reviews.ts +++ b/app/api/evaluation_points/reviews.ts @@ -94,6 +94,53 @@ interface StatsData { score: number; } +// GraphRAG Scored 评查结果类型 +interface FieldScore { + field_path: string; + evaluation_as: string; + weight: number; + scored: number; + max_score: number; + status: string; // 'filled' | 'placeholder' + value: string; + page?: string; + ai_feedback?: string; +} + +interface ScoredEvaluationResult { + evaluation_point_id: number; + code: string; + name: string; + passed: boolean; + machine_score: number; + score: number; + percentage: number; + total_score: number; + total_weight: number; + pass_threshold: number; + result_type: 'scored'; + field_results: FieldScore[]; + missing_fields?: string[]; + ai_suggestion?: string; +} + +interface EvaluationSummary { + total_points: number; + passed_count: number; + failed_count: number; + total_score: number; + total_full_score: number; + average_percentage: number; +} + +interface UnifiedEvaluationResponse { + document_id: number; + flow_type: 'graphrag' | 'legacy'; + results: ScoredEvaluationResult[]; + summary: EvaluationSummary; + evaluated_at: string; +} + // 在文件顶部添加的类型定义,在interface区块前添加 interface OcrDataResult { pages?: number[]; @@ -1126,3 +1173,49 @@ export async function getReviewPoints_fromApi(fileId: string, request: Request) }; } } + +/** + * 获取统一评查结果(GraphRAG Scored 模式) + * + * @param fileId 文档ID + * @param request Remix请求对象 + * @returns 统一评查结果 + */ +export async function getUnifiedEvaluationResults(fileId: string, request: Request) { + try { + const { userInfo, frontendJWT } = await getUserSession(request); + if (!userInfo?.user_id) { + return { error: '用户身份验证失败', status: 401 }; + } + + const response = await apiRequest( + `/api/v2/evaluation/results-unified/${fileId}`, + { + method: 'GET', + headers: { + 'Authorization': `Bearer ${frontendJWT}`, + 'Content-Type': 'application/json' + } + } + ); + + if (response.error) { + console.error('❌ [getUnifiedEvaluationResults] API调用失败:', response.error); + return { error: response.error, status: response.status || 500 }; + } + + if (response.data) { + return response.data; + } + + console.error('❌ [getUnifiedEvaluationResults] API响应数据为空'); + return { error: 'API响应数据为空', status: 500 }; + + } catch (error) { + console.error('❌ [getUnifiedEvaluationResults] 调用失败:', error); + return { + error: error instanceof Error ? error.message : '获取评查结果失败', + status: 500 + }; + } +} diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index c01b84c..9ee3879 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -2,6 +2,7 @@ import { postgrestGet, type PostgrestParams } from '../postgrest-client'; import dayjs from 'dayjs'; import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config'; import axios from 'axios'; +import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE, type ContractType } from '~/constants/contractTypes'; /** * 检查文档名称是否重复 @@ -371,9 +372,9 @@ export async function appendContractAttachments( } export async function uploadDocumentToServer( - binaryData: ArrayBuffer, - fileName: string, - fileType: string, + binaryData: ArrayBuffer, + fileName: string, + fileType: string, typeId: string | number, priority: string, documentNumber?: string | null, @@ -382,7 +383,8 @@ export async function uploadDocumentToServer( documentId?: number | null, isReupload: boolean = false, jwtToken?: string, - attachments?: File[] + attachments?: File[], + attributeType?: string ): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> { try { // console.log('【调试】开始上传文档:', { fileName, fileSize: binaryData.byteLength }); @@ -397,13 +399,14 @@ export async function uploadDocumentToServer( // 将信息添加到一个JSON对象中 const uploadInfo = { - type_id: Number(typeId), + type_id: Number(typeId), evaluation_level: priority, document_number: documentNumber || null, remark: remark || null, is_test_document: isTestDocument, document_id: documentId || null, - is_reupload: isReupload + is_reupload: isReupload, + attribute_type: attributeType || null }; // 添加JSON字符串到FormData diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index ffc14a6..04b9280 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -93,6 +93,32 @@ export interface CharPosition { score: number; // OCR识别置信度 } +/** + * text_bbox -> CharPosition[] 转换 + * GraphRAG 抽取结果只有 text_bbox (段落级坐标), 没有 char_positions (字符级坐标)。 + * 将 text_bbox 转为单个 CharPosition 矩形框, 让 PdfPreview 的高亮逻辑复用。 + */ +function resolveCharPositions(data: any): CharPosition[] | undefined { + // 优先用 char_positions + if (data?.char_positions && data.char_positions.length > 0) { + return data.char_positions; + } + // fallback: text_bbox -> CharPosition[] + if (data?.text_bbox) { + const b = data.text_bbox; + if (b.x_min != null && b.y_min != null && b.x_max != null && b.y_max != null + && (b.x_max - b.x_min) > 0 && (b.y_max - b.y_min) > 0) { + return [{ + box: [[b.x_min, b.y_min], [b.x_max, b.y_min], [b.x_max, b.y_max], [b.x_min, b.y_max]], + char: '', + score: 1 + }]; + } + } + return undefined; +} + + /** * 评查点类型定义 * 用于展示单个评查结果 @@ -1512,7 +1538,7 @@ export function ReviewPointsList({ for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { hasPage = true; - onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value); + onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data), item.data.value); break; } } @@ -1526,7 +1552,7 @@ export function ReviewPointsList({ // 遍历chain找到第一个有效的page for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value); + onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data), item.data.value); break; } } @@ -1566,11 +1592,11 @@ export function ReviewPointsList({ // 假设onReviewPointSelect在作用域内可用 const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions, item.data.value); + onReviewPointSelect(reviewPointId, Number(item.data.page), resolveCharPositions(item.data), item.data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), item.data.char_positions, item.data.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), resolveCharPositions(item.data), item.data.value); } else{ toastService.error(`没有找到${item.field}对应的索引内容`); @@ -1649,11 +1675,11 @@ export function ReviewPointsList({ if (chain[0].data.page) { const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions, chain[0].data.value); + onReviewPointSelect(reviewPointId, chain[0].data.page, resolveCharPositions(chain[0].data), chain[0].data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), chain[0].data.char_positions, chain[0].data.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), resolveCharPositions(chain[0].data), chain[0].data.value); } else{ toastService.error(`没有找到${chain[0].field}对应的索引内容`); @@ -1675,11 +1701,11 @@ export function ReviewPointsList({ if (chain[1].data.page) { const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions, chain[1].data.value); + onReviewPointSelect(reviewPointId, chain[1].data.page, resolveCharPositions(chain[1].data), chain[1].data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), chain[1].data.char_positions, chain[1].data.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), resolveCharPositions(chain[1].data), chain[1].data.value); } else{ toastService.error(`没有找到${chain[1].field}对应的索引内容`); @@ -1815,9 +1841,9 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (mainTypeValue.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value); + onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value); }else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value); }else{ toastService.error(`没有找到${fieldKey}对应的索引内容`); } @@ -1826,9 +1852,9 @@ export function ReviewPointsList({ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (mainTypeValue.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value); + onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value); }else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value); }else{ toastService.error(`没有找到${fieldKey}对应的索引内容`); } @@ -1960,9 +1986,9 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (value.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value); + onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value); }else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value), value.value); }else{ toastService.error(`没有找到${key}对应的索引内容`); } @@ -1972,9 +1998,9 @@ export function ReviewPointsList({ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (value.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value); + onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value); }else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value), value.value); }else{ toastService.error(`没有找到${key}对应的索引内容`); } diff --git a/app/components/evaluation/FieldResultList.tsx b/app/components/evaluation/FieldResultList.tsx new file mode 100644 index 0000000..232d052 --- /dev/null +++ b/app/components/evaluation/FieldResultList.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +interface FieldResultItem { + field_path: string; + evaluation_as: string; + weight: number; + scored: number; + status: string; // 'filled' | 'placeholder' + value: string; + page?: string; + ai_feedback?: string; +} + +interface FieldResultListProps { + items: FieldResultItem[]; +} + +export function FieldResultList({ items }: FieldResultListProps) { + return ( +
+ {items.map((item) => ( +
+
+ + {item.status === 'filled' ? '✅' : '⚠️'} {item.evaluation_as} + + + {item.scored}/{item.weight} + +
+
+ {item.value} +
+ {item.ai_feedback && ( +
+ 💬 {item.ai_feedback} +
+ )} +
+ ))} +
+ ); +} diff --git a/app/components/evaluation/ScoreBar.tsx b/app/components/evaluation/ScoreBar.tsx new file mode 100644 index 0000000..05e2e0a --- /dev/null +++ b/app/components/evaluation/ScoreBar.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface ScoreBarProps { + score: number; // Actual score (e.g., 3 for 3/5) + fullScore: number; // Max score (e.g., 5) + percentage: number; // 0.0-1.0 + passed: boolean; +} + +export function ScoreBar({ score, fullScore, percentage, passed }: ScoreBarProps) { + const pct = Math.round(percentage * 100); + + return ( +
+
+ {pct}% + + {passed ? '✅ 通过' : '❌ 未通过'} + +
+
+
+
+
+ 得分 {score.toFixed(1)} / 满分 {fullScore.toFixed(1)} +
+
+ ); +} diff --git a/app/components/evaluation/ScoredResultCard.tsx b/app/components/evaluation/ScoredResultCard.tsx new file mode 100644 index 0000000..40712ee --- /dev/null +++ b/app/components/evaluation/ScoredResultCard.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { ScoreBar } from './ScoreBar'; +import { FieldResultList } from './FieldResultList'; + +interface ScoredResultCardProps { + result: { + evaluation_point_id: number; + code: string; + name: string; + passed: boolean; + machine_score: number; // e.g., 3 + score: number; // e.g., 5 (full score from evaluation_points) + percentage: number; + total_score: number; // e.g., 60 + total_weight: number; // e.g., 100 + field_results: Array<{ + field_path: string; + evaluation_as: string; + weight: number; + scored: number; + status: string; + value: string; + page?: string; + ai_feedback?: string; + }>; + missing_fields?: string[]; + ai_suggestion?: string; + }; +} + +export function ScoredResultCard({ result }: ScoredResultCardProps) { + return ( +
+
+ {result.code} + {result.name} +
+ + + + + + {result.ai_suggestion && ( +
+ 💡 建议: + {result.ai_suggestion} +
+ )} +
+ ); +} diff --git a/app/components/evaluation/index.ts b/app/components/evaluation/index.ts new file mode 100644 index 0000000..38a2db8 --- /dev/null +++ b/app/components/evaluation/index.ts @@ -0,0 +1,3 @@ +export { ScoreBar } from './ScoreBar'; +export { FieldResultList } from './FieldResultList'; +export { ScoredResultCard } from './ScoredResultCard'; diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index adbd7d9..e692608 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -53,6 +53,7 @@ interface FilePreviewProps { activeReviewPointResultId: string | null; targetPage?: number; // 新增目标页码参数 charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息(仅用于PDF) + textBbox?: { x_min: number; y_min: number; x_max: number; y_max: number }; // GraphRAG段落级坐标 highlightValue?: string; // 高亮文本值(用于DOCX) isStructuredView?: boolean; // 是否显示结构化视图 userInfo?: { @@ -74,7 +75,7 @@ export interface FilePreviewHandle { } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { -export const FilePreview = forwardRef(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) { +export const FilePreview = forwardRef(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, textBbox, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) { // 获取文件类型 const real_path = fileContent.path || fileContent.template_contract_path || ''; const fileExtension = real_path.split('.').pop()?.toLowerCase(); @@ -236,6 +237,7 @@ export const FilePreview = forwardRef(funct filePath={real_path} targetPage={targetPage} charPositions={charPositions} + textBbox={textBbox} isStructuredView={isStructuredView} activeReviewPointResultId={activeReviewPointResultId} pageOffset={pageOffset} diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 5a117f0..59190e9 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -24,6 +24,7 @@ import { Tooltip } from '../ui/Tooltip'; import { CorporateInfoModal } from '../corporate-information'; import type { BusinessInfoResult, DishonestyResult } from '../corporate-information'; import { queryCompanyInfo } from '~/api/corporate-information/qichacha'; +import { ScoredResultCard } from '~/components/evaluation'; // import '../../styles/components/TooltipStyles.css'; /** @@ -81,6 +82,32 @@ export interface CharPosition { score: number; // OCR识别置信度 } +/** + * text_bbox -> CharPosition[] 转换 + * GraphRAG 抽取结果只有 text_bbox (段落级坐标), 没有 char_positions (字符级坐标)。 + * 将 text_bbox 转为单个 CharPosition 矩形框, 让 PdfPreview 的高亮逻辑复用。 + */ +function resolveCharPositions(data: any): CharPosition[] | undefined { + // 优先用 char_positions + if (data?.char_positions && data.char_positions.length > 0) { + return data.char_positions; + } + // fallback: text_bbox -> CharPosition[] + if (data?.text_bbox) { + const b = data.text_bbox; + if (b.x_min != null && b.y_min != null && b.x_max != null && b.y_max != null + && (b.x_max - b.x_min) > 0 && (b.y_max - b.y_min) > 0) { + return [{ + box: [[b.x_min, b.y_min], [b.x_max, b.y_min], [b.x_max, b.y_max], [b.x_min, b.y_max]], + char: '', + score: 1 + }]; + } + } + return undefined; +} + + /** * 评查点类型定义 * 用于展示单个评查结果 @@ -145,6 +172,45 @@ interface Statistics { score: number; } +// GraphRAG Scored 评查结果类型 +interface FieldScore { + field_path: string; + evaluation_as: string; + weight: number; + scored: number; + max_score: number; + status: string; + value: string; + page?: string; + ai_feedback?: string; +} + +interface ScoredEvaluationResult { + evaluation_point_id: number; + code: string; + name: string; + passed: boolean; + machine_score: number; + score: number; + percentage: number; + total_score: number; + total_weight: number; + pass_threshold: number; + result_type: 'scored'; + field_results: FieldScore[]; + missing_fields?: string[]; + ai_suggestion?: string; +} + +interface EvaluationSummary { + total_points: number; + passed_count: number; + failed_count: number; + total_score: number; + total_full_score: number; + average_percentage: number; +} + interface ReviewPointsListProps { reviewPoints: ReviewPoint[]; statistics: Statistics; @@ -153,6 +219,10 @@ interface ReviewPointsListProps { onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; fileFormat?: string; // 文档格式类型(PDF、DOCX等) onAiSuggestionReplace?: (searchText: string, replaceText: string, pageNumber: number) => void; // AI建议替换回调 + // GraphRAG Scored 模式支持 + flowType?: 'graphrag' | 'legacy'; + scoredResults?: ScoredEvaluationResult[]; + scoredSummary?: EvaluationSummary; } /** @@ -411,7 +481,10 @@ export function ReviewPointsList({ onReviewPointSelect, onStatusChange, fileFormat, - onAiSuggestionReplace + onAiSuggestionReplace, + flowType, + scoredResults, + scoredSummary }: ReviewPointsListProps) { // 状态管理 const [editingReviewPoint, setEditingReviewPoint] = useState(null); // 当前正在编辑的评查点ID @@ -587,6 +660,9 @@ export function ReviewPointsList({ } else if (statusFilter === 'error') { // 过滤"错误"状态 matchesStatus = point.result === false && point.status === 'error'; + } else if (statusFilter === 'notApplicable') { + // 过滤"未涉及"状态 + matchesStatus = point.status === 'notApplicable' || point.status === 'not_applicable'; } // console.log('筛选point', point); @@ -618,6 +694,7 @@ export function ReviewPointsList({ success: 0, warning: 0, error: 0, + notApplicable: 0, score: 0 }; @@ -639,6 +716,10 @@ export function ReviewPointsList({ const successToShow = successCount || statsToUse.success; const warningToShow = warningCount || statsToUse.warning; const errorToShow = errorCount || statsToUse.error; + const notApplicableCount = reviewPoints.filter( + point => point.status === 'notApplicable' || point.status === 'not_applicable' + ).length; + const notApplicableToShow = notApplicableCount || statsToUse.notApplicable || 0; return (
@@ -697,6 +778,19 @@ export function ReviewPointsList({ 错误
+ {/* 未涉及数量 */} +
+
+ +
); @@ -1194,7 +1288,7 @@ export function ReviewPointsList({ for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { hasPage = true; - onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions); + onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data)); break; } } @@ -1208,7 +1302,7 @@ export function ReviewPointsList({ // 遍历chain找到第一个有效的page for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions); + onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data)); break; } } @@ -1244,15 +1338,15 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (item.data.page) { - console.log('点击了长链条评查点', item.data.char_positions, item.data); + console.log('点击了长链条评查点', resolveCharPositions(item.data), item.data); // 假设onReviewPointSelect在作用域内可用 const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions, item.data.value); + onReviewPointSelect(reviewPointId, Number(item.data.page), resolveCharPositions(item.data), item.data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), item.data.char_positions, item.data.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), resolveCharPositions(item.data), item.data.value); } else{ toastService.error(`没有找到${item.field}对应的索引内容`); @@ -1328,16 +1422,16 @@ export function ReviewPointsList({ ${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'} transition-colors flex flex-col`} onClick={(e) => { e.stopPropagation(); - console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data) + console.log('点击了短链1左', resolveCharPositions(chain[0].data), chain[0].data) if (chain[0].data.page) { - // console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data) + // console.log('点击了短链1左', resolveCharPositions(chain[0].data), chain[0].data) const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions, chain[0].data.value); + onReviewPointSelect(reviewPointId, chain[0].data.page, resolveCharPositions(chain[0].data), chain[0].data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), chain[0].data.char_positions,chain[0].data.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), resolveCharPositions(chain[0].data),chain[0].data.value); } else{ toastService.error(`没有找到${chain[0].field}对应的索引内容`); @@ -1357,14 +1451,14 @@ export function ReviewPointsList({ onClick={(e) => { e.stopPropagation(); if (chain[1].data.page) { - console.log('点击了短链2右', chain[1].data.char_positions, chain[1].data) + console.log('点击了短链2右', resolveCharPositions(chain[1].data), chain[1].data) const reviewPointId = reviewPoint.id as string; if (reviewPointId && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions, chain[1].data.value); + onReviewPointSelect(reviewPointId, chain[1].data.page, resolveCharPositions(chain[1].data), chain[1].data.value); } } else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), chain[1].data.char_positions, chain[1].data.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), resolveCharPositions(chain[1].data), chain[1].data.value); } else{ toastService.error(`没有找到${chain[1].field}对应的索引内容`); @@ -1501,10 +1595,10 @@ export function ReviewPointsList({ e.stopPropagation(); if (mainTypeValue.page && typeof onReviewPointSelect === 'function') { console.log("点击了其他评查点", mainTypeValue) - onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value); - // onReviewPointSelect(reviewPoint.id, undefined, mainTypeValue.char_positions, mainTypeValue.value); + onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value); + // onReviewPointSelect(reviewPoint.id, undefined, resolveCharPositions(mainTypeValue), mainTypeValue.value); }else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){ - onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value); + onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value); }else{ toastService.error(`没有找到${fieldKey}对应的索引内容`); } @@ -1513,7 +1607,7 @@ export function ReviewPointsList({ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (mainTypeValue.page && typeof onReviewPointSelect === 'function') { - onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value); + onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value); }else{ toastService.error(`没有找到${fieldKey}对应的索引内容`); } @@ -1583,6 +1677,7 @@ export function ReviewPointsList({ value: string; res: boolean; char_positions?: CharPosition[]; + res?: boolean; }>; ai_suggestion?: { summary?: string; @@ -1635,7 +1730,8 @@ export function ReviewPointsList({ // 遍历fields,获取每个字段的值并生成对应的JSX元素 if (config.fields) { Object.entries(config.fields).forEach(([key, value], index) => { - const res = value.value.trim() !== ''; + // 优先使用后端传入的 per-field res,fallback 到 value 非空判定 + const res = value.res !== undefined && value.res !== null ? value.res : value.value.trim() !== ''; fieldElements.push( + ))} + + + + )} + {/* 文件上传区域 - 左右布局 */} diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 09f4092..a3028a8 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -27,6 +27,7 @@ import { import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files"; import { links as fileTypeTagLinks } from "~/components/ui/FileTypeTag"; import { getQueueStatus, type QueueStatus } from "~/api/queue"; +import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from "~/constants/contractTypes"; export function links() { return [ @@ -131,7 +132,8 @@ async function handleFileUpload( documentId?: number | null, isReupload: boolean = false, jwtToken?: string, - attachments?: File[] + attachments?: File[], + attributeType?: string ): Promise { // console.log('【handleFileUpload】开始上传:', { // fileName, @@ -153,7 +155,8 @@ async function handleFileUpload( documentId, isReupload, jwtToken, - attachments + attachments, + attributeType ); // console.log('【handleFileUpload】uploadDocumentToServer返回:', { @@ -349,6 +352,7 @@ export default function FilesUpload() { const [documentNumber, setDocumentNumber] = useState(""); const [remark, setRemark] = useState(""); const [currentFiles, setCurrentFiles] = useState([]); + const [attributeType, setAttributeType] = useState(DEFAULT_CONTRACT_TYPE); // 合同文件上传状态 // 这些变量暂时未使用,但保留以备将来扩展 @@ -1193,7 +1197,8 @@ export default function FilesUpload() { null, false, loaderData.frontendJWT || undefined, - attachmentFiles + attachmentFiles, + attributeType ); // console.log('【合同上传】服务器响应数据:', uploadResp); @@ -1550,7 +1555,9 @@ export default function FilesUpload() { isTestDocument, temp_n > 1 ? firstFileDocumentId : null, // 第二个文件及以后使用第一个文件的document_id false, - loaderData.frontendJWT || undefined + loaderData.frontendJWT || undefined, + undefined, + attributeType ); const timeoutPromise = new Promise((_, reject) => { @@ -2326,6 +2333,28 @@ export default function FilesUpload() {
优先级影响文档在队列中的处理顺序
+ {/* 子类型(专属类型)- 始终显示 */} +
+ + +
选择文档专属类型以应用对应的审核规则(合同大类请选择技术/租赁/买卖等)
+
r.result_type === 'not_applicable') + .map((r: any) => ({ + id: `na-${r.evaluation_point_id}`, + documentId: id, + pointId: r.evaluation_point_id, + editAuditStatusId: '', + editAuditStatus: '', + editAuditStatusMessage: '', + title: '该评查点未涉及', + pointName: r.name || '', + groupName: '', + status: 'notApplicable', + content: {}, + contentPage: {}, + suggestion: r.ai_suggestion || '该评查点未涉及', + result: null, + score: r.score || 0, + finalScore: null, + machineScore: 0, + postAction: '', + })); + const allReviewPoints = [...existingPoints, ...notApplicablePoints]; + + return Response.json({ + previousRoute: previousRoute, + document: ('document' in reviewData && !('error' in reviewData)) ? reviewData.document : null, + reviewPoints: allReviewPoints, + reviewInfo: { reviewTime: unifiedData.evaluated_at, reviewModel: 'GraphRAG', ruleGroup: '', result: '', issueCount: unifiedData.summary?.total_points || 0 }, + statistics: { + total: unifiedData.summary?.total_points || 0, + success: unifiedData.summary?.passed_count || 0, + warning: unifiedData.summary?.failed_count || 0, + error: 0, + notApplicable: unifiedData.summary?.not_applicable_count || 0, + score: unifiedData.summary?.total_score || 0 + }, + comparison_document: ('comparison_document' in reviewData && !('error' in reviewData)) ? reviewData.comparison_document : null, + userInfo, + frontendJWT, + flowType: 'graphrag', + scoredResults: unifiedData.results, + scoredSummary: unifiedData.summary + }); + } else { + // legacy 流程但统一接口可用,也走原有逻辑 + const reviewData = await getReviewPoints_fromApi(id, request); + if ('error' in reviewData && reviewData.error) { + return Response.json({ result: false, message: reviewData.error }); + } return Response.json({ previousRoute: previousRoute, document: reviewData.document, @@ -214,11 +290,11 @@ export async function loader({ request }: LoaderFunctionArgs) { statistics: reviewData.stats, comparison_document: reviewData.comparison_document, userInfo, - frontendJWT + frontendJWT, + flowType: 'legacy', + scoredResults: null, + scoredSummary: null }); - } else { - console.error("[Reviews Loader] 返回的评查数据格式不正确,完整数据:", JSON.stringify(reviewData, null, 2)); - return Response.json({ result: false, message: '返回的评查数据格式不正确' }); } } catch (error) { console.error('[Reviews Loader] 获取评查数据失败:', error); @@ -310,6 +386,7 @@ export default function ReviewDetails() { const [targetPage, setTargetPage] = useState(undefined); const [templateTargetPage, setTemplateTargetPage] = useState(undefined); const [charPositions, setCharPositions] = useState | undefined>(undefined); + const [textBbox, setTextBbox] = useState<{ x_min: number; y_min: number; x_max: number; y_max: number } | undefined>(undefined); const [highlightValue, setHighlightValue] = useState(undefined); const [pendingUpdate, setPendingUpdate] = useState<{ reviewPointResultId: string; @@ -475,17 +552,19 @@ export default function ReviewDetails() { setActiveTab(tabKey); }; - const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string) => { + const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string, bbox?: { x_min: number; y_min: number; x_max: number; y_max: number }) => { // 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发 if (reviewPointId === activeReviewPointResultId && page) { setTargetPage(undefined); setCharPositions(undefined); + setTextBbox(undefined); setHighlightValue(undefined); // 使用setTimeout确保状态更新后再设置新的targetPage、charPositions和highlightValue setTimeout(() => { setActiveReviewPointResultId(reviewPointId); setTargetPage(page); setCharPositions(charPos); + setTextBbox(bbox); setHighlightValue(value); }, 0); } else { @@ -493,6 +572,7 @@ export default function ReviewDetails() { setActiveReviewPointResultId(reviewPointId); setTargetPage(page); setCharPositions(charPos); + setTextBbox(bbox); setHighlightValue(value); } }; @@ -890,6 +970,7 @@ export default function ReviewDetails() { activeReviewPointResultId={activeReviewPointResultId} targetPage={targetPage} charPositions={charPositions} + textBbox={textBbox} highlightValue={highlightValue} userInfo={loaderData.userInfo} aiSuggestionReplace={aiSuggestionReplace} @@ -909,6 +990,9 @@ export default function ReviewDetails() { onStatusChange={handleReviewPointStatusChange} fileFormat={reviewData.fileInfo.fileFormat} onAiSuggestionReplace={handleAiSuggestionReplace} + flowType={reviewData.flowType} + scoredResults={reviewData.scoredResults} + scoredSummary={reviewData.scoredSummary} />
@@ -940,6 +1024,7 @@ export default function ReviewDetails() { activeReviewPointResultId={activeReviewPointResultId} targetPage={targetPage} charPositions={charPositions} + textBbox={textBbox} />