fix: 1.接入ai_suggestion.
2. 接入合同起草功能。
This commit is contained in:
@@ -279,8 +279,8 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
||||
<div className="flex justify-center items-center h-full min-h-[600px]">
|
||||
<div className="text-center text-red-500">
|
||||
<i className="ri-error-warning-line text-4xl mb-2"></i>
|
||||
<p className="text-lg">{error || '加载配置失败'}</p>
|
||||
<p className="text-sm text-gray-500 mt-2">请刷新页面重试或联系管理员</p>
|
||||
<p className="text-lg">{error}</p>
|
||||
{/* <p className="text-sm text-gray-500 mt-2">请刷新页面重试或联系管理员</p> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { COLLABORA_URL } from '~/config/api-config';
|
||||
import { toastService } from '../ui/Toast';
|
||||
import {
|
||||
unoScrollToTop,
|
||||
unoReplaceAll
|
||||
} from './lib';
|
||||
import type { CollaboraConfig } from './types';
|
||||
|
||||
@@ -131,14 +132,30 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}, [iframeRef]);
|
||||
|
||||
/**
|
||||
* 替换所有匹配项
|
||||
* @param searchText 要搜索的文本
|
||||
* @param replaceText 替换后的文本
|
||||
*/
|
||||
const replaceAll = useCallback(async (searchText: string, replaceText: string) => {
|
||||
if (!iframeRef.current?.contentWindow) {
|
||||
console.warn('[UNO] iframe 不可用');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[UNO] 替换全部:', searchText, '->', replaceText);
|
||||
await unoReplaceAll(iframeRef.current.contentWindow, searchText, replaceText);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}, [iframeRef]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
scrollToTop
|
||||
scrollToTop,
|
||||
replaceAll
|
||||
}),
|
||||
[
|
||||
scrollToTop
|
||||
scrollToTop,
|
||||
replaceAll
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface HighlightOptions {
|
||||
/** 高亮颜色 (LibreOffice Decimal 格式), 默认 16776960 = 黄色 */
|
||||
color?: number;
|
||||
/** 目标页码 (从1开始), 默认第1页 */
|
||||
page?: number;
|
||||
page?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ export interface HighlightResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
highlightedCount?: number;
|
||||
page?: number;
|
||||
page?: number | null;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export async function highlightText(
|
||||
options?: HighlightOptions
|
||||
): Promise<HighlightResponse> {
|
||||
const color = options?.color ?? 16776960; // 默认黄色
|
||||
const page = options?.page ?? 1; // 默认第1页
|
||||
const page = options?.page ?? null; // 默认第1页
|
||||
|
||||
console.log('[HighlightSelectText] 调用 Python 脚本高亮文本:', {
|
||||
text,
|
||||
|
||||
@@ -53,6 +53,9 @@ export interface CollaboraViewerHandle {
|
||||
/** UNO 命令方法集合 */
|
||||
unoCommands: {
|
||||
scrollToTop: () => Promise<void>;
|
||||
replaceAll?: (searchText: string, replaceText: string) => Promise<void>;
|
||||
find?: (searchText: string) => Promise<void>;
|
||||
search?: (searchText: string) => Promise<void>;
|
||||
};
|
||||
/** 文档是否已加载完成 */
|
||||
isReady: boolean;
|
||||
|
||||
@@ -71,13 +71,13 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 下载原文件按钮 */}
|
||||
{/* 下载文件按钮 */}
|
||||
<button
|
||||
onClick={handleDownloadFile}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<i className="fas fa-download mr-1.5"></i>
|
||||
下载原文件
|
||||
下载
|
||||
</button>
|
||||
|
||||
{/* 导出评查报告按钮 */}
|
||||
|
||||
@@ -527,7 +527,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
<span className="ml-2 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap flex-shrink-0">
|
||||
比例:{zoomLevel}%
|
||||
</span>
|
||||
<button
|
||||
{/* <button
|
||||
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 flex-shrink-0 ${dragMode ? 'active bg-green-300' : ''}`}
|
||||
title="切换拖拽模式"
|
||||
aria-pressed={dragMode}
|
||||
@@ -537,7 +537,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline truncate max-w-[80px]">
|
||||
拖拽模式{dragMode ? '(已激活)' : ''}
|
||||
</span>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1809,6 +1809,26 @@ export function ReviewPointsList({
|
||||
value: string;
|
||||
char_positions?: CharPosition[];
|
||||
}>;
|
||||
ai_suggestion?: {
|
||||
summary?: string;
|
||||
analysis?: {
|
||||
failure_reason?: string;
|
||||
solution_approach?: string;
|
||||
rule_understanding?: string;
|
||||
};
|
||||
suggestions?: Record<string, {
|
||||
reason: string;
|
||||
source: {
|
||||
page: number | null;
|
||||
type: string;
|
||||
field: string | null;
|
||||
};
|
||||
priority: string;
|
||||
confidence: number;
|
||||
suggested_value: string | null;
|
||||
}>;
|
||||
generated_at?: string;
|
||||
};
|
||||
message?: string;
|
||||
res?: boolean;
|
||||
} | undefined;
|
||||
@@ -1934,10 +1954,10 @@ export function ReviewPointsList({
|
||||
// 渲染AI模型返回的评估消息
|
||||
if (config.message) {
|
||||
// 检查message是否为对象,如果是则转换为字符串
|
||||
const messageContent = typeof config.message === 'object'
|
||||
? JSON.stringify(config.message)
|
||||
const messageContent = typeof config.message === 'object'
|
||||
? JSON.stringify(config.message)
|
||||
: String(config.message);
|
||||
|
||||
|
||||
// 添加模型评估消息区域,使用蓝色背景突出显示
|
||||
fieldElements.push(
|
||||
<div key="message" className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||||
@@ -1949,6 +1969,57 @@ export function ReviewPointsList({
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染AI建议(ai_suggestion)
|
||||
if (config.ai_suggestion?.suggestions && Object.keys(config.ai_suggestion.suggestions).length > 0) {
|
||||
// 遍历suggestions对象的key-value对
|
||||
Object.entries(config.ai_suggestion.suggestions).forEach(([key, suggestionValue], index) => {
|
||||
// 检查建议值是否存在(null 或有值都要渲染)
|
||||
const hasSuggestedValue = suggestionValue.suggested_value !== null && suggestionValue.suggested_value.trim() !== '';
|
||||
|
||||
fieldElements.push(
|
||||
<div key={`ai-suggestion-${index}`} className="mb-3">
|
||||
{/* 字段名称标签 */}
|
||||
<div className="text-xs text-gray-600 mb-2 font-medium">
|
||||
<i className="ri-lightbulb-line text-yellow-500 mr-1"></i>
|
||||
AI建议修改 - {key}
|
||||
</div>
|
||||
|
||||
{/* 原因说明 */}
|
||||
<div className="mb-2 p-2 bg-amber-50 rounded border border-amber-200 text-xs text-gray-700">
|
||||
<div className="flex items-start">
|
||||
<i className="ri-information-line text-amber-600 mr-1 mt-0.5"></i>
|
||||
<div>
|
||||
<span>{suggestionValue.reason}</span>
|
||||
{suggestionValue.source.page !== null && (
|
||||
<span className="ml-2 text-gray-500">
|
||||
(来源页码: {suggestionValue.source.page})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 建议内容显示 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* 文本输入框 */}
|
||||
<textarea
|
||||
value={suggestionValue.suggested_value || ''}
|
||||
readOnly
|
||||
disabled={!hasSuggestedValue}
|
||||
className={`flex-1 p-2 border rounded text-xs resize-none overflow-y-auto ${
|
||||
hasSuggestedValue
|
||||
? 'border-gray-200 bg-gray-50 text-gray-700 cursor-not-allowed'
|
||||
: 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label={`${key}的AI建议内容`}
|
||||
placeholder={!hasSuggestedValue ? '暂无建议值' : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 返回包含所有元素的React片段
|
||||
return <>{fieldElements}</>;
|
||||
};
|
||||
|
||||
@@ -207,20 +207,20 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
|
||||
// 如果是省局访问
|
||||
// if(isPort51707){
|
||||
// if (selectedModuleName === '智慧法务大模型'){
|
||||
// if (selectedModuleName === '智慧法务助手'){
|
||||
// return item.path && item.path.startsWith('/chat-with-llm')
|
||||
// }
|
||||
// return item.path && item.path.startsWith('/cross-checking')
|
||||
// }
|
||||
|
||||
// 🔑 如果选择了"智慧法务大模型",显示 /chat-with-llm 和 /dataset-manager 相关菜单
|
||||
if (selectedModuleName === '智慧法务大模型') {
|
||||
// 🔑 如果选择了"智慧法务助手",显示 /chat-with-llm 和 /dataset-manager 相关菜单
|
||||
if (selectedModuleName === '智慧法务助手') {
|
||||
return item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')
|
||||
}
|
||||
|
||||
// 🔑 如果选择了包含"合同"的模块
|
||||
if (selectedModuleName.includes('合同')) {
|
||||
// 排除智慧法务大模型专属菜单
|
||||
// 排除智慧法务助手专属菜单
|
||||
if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) {
|
||||
return false;
|
||||
}
|
||||
@@ -229,7 +229,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
}
|
||||
|
||||
// 🔑 其他模块:排除特殊菜单
|
||||
// 排除智慧法务大模型专属菜单
|
||||
// 排除智慧法务助手专属菜单
|
||||
if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) {
|
||||
return false;
|
||||
}
|
||||
@@ -321,7 +321,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
<div className={`flex items-center ${collapsed ? 'justify-center' : ''}`}>
|
||||
{selectedModulePicPath && (
|
||||
<img
|
||||
src={selectedModuleName === '智慧法务大模型' || selectedModuleName === '交叉评查' ? selectedModulePicPath : `${DOCUMENT_URL}${selectedModulePicPath}`}
|
||||
src={selectedModuleName === '智慧法务助手' || selectedModuleName === '交叉评查' ? selectedModulePicPath : `${DOCUMENT_URL}${selectedModulePicPath}`}
|
||||
alt={selectedModuleName}
|
||||
className={`${collapsed ? 'w-8 h-8' : 'w-6 h-6 mr-3'}`}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 文件预览组件
|
||||
* 显示文档内容和评查点高亮
|
||||
*/
|
||||
import { useState, useEffect, useRef, ChangeEvent } from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useImperativeHandle, ChangeEvent } from 'react';
|
||||
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
|
||||
import { requestPageInfo, customGotoPage } from '~/components/collabora/lib';
|
||||
import { PdfPreview } from './previewComponents/PdfPreview';
|
||||
@@ -64,14 +64,22 @@ interface FilePreviewProps {
|
||||
replaceText: string;
|
||||
pageNumber: number;
|
||||
}; // AI建议替换参数
|
||||
isTemplate?: boolean;
|
||||
}
|
||||
|
||||
// 暴露给父组件的接口
|
||||
export interface FilePreviewHandle {
|
||||
collaboraViewerRef: React.RefObject<CollaboraViewerHandle>;
|
||||
}
|
||||
|
||||
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||||
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace }: FilePreviewProps) {
|
||||
export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) {
|
||||
// 获取文件类型
|
||||
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
||||
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
||||
// 是文档类型 并且 是模板文件
|
||||
const isDocx = fileExtension === 'docx';
|
||||
// 是模板文件 或 是pdf文件就用pdf渲染
|
||||
const isPdf = fileExtension === 'pdf';
|
||||
|
||||
// ✅ 将所有hooks移到条件return之前,确保遵守React Hooks规则
|
||||
@@ -80,6 +88,11 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
const collaboraViewerRef = useRef<CollaboraViewerHandle>(null);
|
||||
const prevTargetPageRef = useRef<number | undefined>(undefined);
|
||||
|
||||
// 暴露 collaboraViewerRef 给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
collaboraViewerRef
|
||||
}));
|
||||
|
||||
// States
|
||||
const [numPages, setNumPages] = useState<number | null>(null);
|
||||
const [pageInputValue, setPageInputValue] = useState<string>('');
|
||||
@@ -265,17 +278,20 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
return;
|
||||
}
|
||||
|
||||
// if (targetPageNum > 0 && (!numPages || targetPageNum <= numPages)) {
|
||||
if (targetPageNum > 0) {
|
||||
try {
|
||||
await customGotoPage(iframeWindow, targetPageNum);
|
||||
setPageInputValue('');
|
||||
// toastService.success(`已跳转至第 ${targetPageNum} 页`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
toastService.error(`跳转失败: ${errorMessage}`);
|
||||
}
|
||||
// } else if (numPages && targetPageNum > numPages) {
|
||||
// toastService.warning(`页码不能超过总页数 ${numPages}`);
|
||||
} else {
|
||||
toastService.warning('请输入有效页码');
|
||||
setPageInputValue('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,17 +302,55 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
}
|
||||
};
|
||||
|
||||
// 滚动到顶部(仅用于 DOCX)
|
||||
// 滚动到顶部(支持 PDF 和 DOCX)
|
||||
const handleScrollToTop = async () => {
|
||||
if (!collaboraViewerRef.current?.isReady) {
|
||||
toastService.warning('文档尚未加载完成,请稍候...');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsScrollingToTop(true);
|
||||
|
||||
try {
|
||||
await collaboraViewerRef.current?.unoCommands.scrollToTop();
|
||||
console.log('[FilePreview] 已返回顶部');
|
||||
if (isPdf) {
|
||||
// PDF文件:滚动到第一个页面元素
|
||||
const firstPage = document.querySelector('[data-page-number="1"]');
|
||||
if (firstPage) {
|
||||
firstPage.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
// console.log('[FilePreview] PDF已返回顶部');
|
||||
} else if (contentRef.current) {
|
||||
// 如果找不到页面元素,则滚动容器到顶部
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
// console.log('[FilePreview] PDF容器已返回顶部');
|
||||
}
|
||||
} else if (isDocx) {
|
||||
// DOCX文件:模板预览模式尝试多种滚动方式,编辑模式使用Collabora命令
|
||||
if (isTemplate) {
|
||||
// 模板预览模式:尝试多种滚动方式
|
||||
console.log('[FilePreview] 尝试返回顶部...');
|
||||
|
||||
// 1. 尝试滚动 contentRef 容器
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
console.log('[FilePreview] 已滚动 contentRef 容器');
|
||||
}
|
||||
|
||||
// 2. 尝试滚动外层的 file-preview-isolation 容器
|
||||
const isolationContainer = document.querySelector('.file-preview-isolation');
|
||||
if (isolationContainer) {
|
||||
isolationContainer.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
console.log('[FilePreview] 已滚动 isolation 容器');
|
||||
}
|
||||
|
||||
// 3. 最后滚动整个窗口
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
console.log('[FilePreview] 已滚动窗口');
|
||||
} else {
|
||||
// 编辑模式:使用Collabora UNO命令
|
||||
if (!collaboraViewerRef.current?.isReady) {
|
||||
toastService.warning('文档尚未加载完成,请稍候...');
|
||||
setIsScrollingToTop(false);
|
||||
return;
|
||||
}
|
||||
await collaboraViewerRef.current?.unoCommands.scrollToTop();
|
||||
console.log('[FilePreview] DOCX已返回顶部(UNO命令)');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[FilePreview] 返回顶部失败:', error);
|
||||
toastService.error('返回顶部失败');
|
||||
@@ -357,11 +411,13 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
const highlightText = highlightValue;
|
||||
|
||||
// DOCX文件使用Collabora Online预览
|
||||
// 如果是模板预览,使用只读模式;否则使用编辑模式
|
||||
return (
|
||||
<CollaboraViewer
|
||||
ref={collaboraViewerRef}
|
||||
fileId={real_path}
|
||||
mode="edit"
|
||||
mode={isTemplate ? "view" : "edit"}
|
||||
// mode={"edit"}
|
||||
userId={userInfo?.sub || 'guest'}
|
||||
userName={userInfo?.nick_name || ''}
|
||||
targetPage={targetPage}
|
||||
@@ -380,8 +436,8 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-preview">
|
||||
<div className="file-preview-header px-2 text-xs sm:text-xs md:text-sm max-w-full flex items-center justify-between min-w-0">
|
||||
<div className="file-preview h-full flex flex-col">
|
||||
<div className="file-preview-header px-2 text-xs sm:text-xs md:text-sm max-w-full flex items-center justify-between min-w-0 flex-shrink-0">
|
||||
<div className="flex items-center min-w-0 flex-shrink-0">
|
||||
<i className={`${isStructuredView ? 'ri-file-list-line' : 'ri-file-text-line'} text-primary mr-2 flex-shrink-0`}></i>
|
||||
<span className="font-medium text-primary truncate max-w-[120px]" title={isStructuredView ? '模板预览' : '文件预览'}>
|
||||
@@ -389,27 +445,30 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
</span>
|
||||
</div>
|
||||
<div className="file-preview-actions flex items-center ml-2 min-w-0 flex-1 justify-end overflow-hidden gap-2">
|
||||
<button
|
||||
className={`flex items-center justify-center px-2 py-1 text-xs text-gray-700 bg-white border border-gray-300 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
||||
isScrollingToTop || isDocumentLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-50 hover:border-primary hover:text-primary'
|
||||
}`}
|
||||
onClick={handleScrollToTop}
|
||||
disabled={isScrollingToTop || isDocumentLoading}
|
||||
title="返回顶部"
|
||||
>
|
||||
{isScrollingToTop ? (
|
||||
<i className="ri-loader-4-line text-sm animate-spin"></i>
|
||||
) : (
|
||||
<i className="ri-arrow-up-double-line text-sm"></i>
|
||||
)}
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||||
{isScrollingToTop ? '返回中...' : '返回顶部'}
|
||||
</span>
|
||||
</button>
|
||||
{/* 清除高亮按钮 - 仅在DOCX文档时显示 */}
|
||||
{isDocx && (
|
||||
{/* 返回顶部按钮 - 不在模板预览时显示 */}
|
||||
{!isTemplate && (
|
||||
<button
|
||||
className={`flex items-center justify-center px-2 py-1 text-xs text-gray-700 bg-white border border-gray-300 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
||||
isScrollingToTop || isDocumentLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-50 hover:border-primary hover:text-primary'
|
||||
}`}
|
||||
onClick={handleScrollToTop}
|
||||
disabled={isScrollingToTop || isDocumentLoading}
|
||||
title="返回顶部"
|
||||
>
|
||||
{isScrollingToTop ? (
|
||||
<i className="ri-loader-4-line text-sm animate-spin"></i>
|
||||
) : (
|
||||
<i className="ri-arrow-up-double-line text-sm"></i>
|
||||
)}
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||||
{isScrollingToTop ? '返回中...' : '返回顶部'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{/* 清除高亮按钮 - 仅在DOCX文档时显示,且不是模板预览 */}
|
||||
{isDocx && !isTemplate && (
|
||||
<button
|
||||
className={`flex items-center justify-center px-2 py-1 text-xs text-white bg-red-500 border border-red-500 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
||||
isClearingHighlights || isDocumentLoading
|
||||
@@ -430,31 +489,39 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{/* 页码跳转控件 */}
|
||||
<div className="inline-flex items-center flex-shrink-0 gap-1">
|
||||
<input
|
||||
type="text"
|
||||
className="w-12 h-7 px-2 text-xs text-center text-gray-700 bg-white border border-gray-300 rounded outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="页码"
|
||||
value={pageInputValue}
|
||||
onChange={handlePageInputChange}
|
||||
onKeyDown={handlePageInputKeyDown}
|
||||
disabled={isDocumentLoading}
|
||||
/>
|
||||
<button
|
||||
className="flex items-center justify-center w-7 h-7 text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:border-primary hover:text-primary transition-colors duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white disabled:hover:border-gray-300 disabled:hover:text-gray-700"
|
||||
onClick={handlePageJump}
|
||||
disabled={!numPages || isDocumentLoading}
|
||||
title="跳转到页面"
|
||||
>
|
||||
<i className="ri-arrow-right-line text-sm"></i>
|
||||
</button>
|
||||
{numPages && (
|
||||
<span className="ml-1 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||||
/ {numPages}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 页码跳转控件 - 不在模板预览时显示 */}
|
||||
{!isTemplate && (
|
||||
<div className="inline-flex items-center bg-white border border-gray-300 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 flex-shrink-0 overflow-hidden">
|
||||
<div className="flex items-center px-2 py-1">
|
||||
<i className="ri-file-list-line text-sm text-gray-400 mr-1.5"></i>
|
||||
<input
|
||||
type="text"
|
||||
className="w-10 h-5 px-1 text-xs text-center text-gray-700 bg-transparent border-0 outline-none focus:text-primary transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="页码"
|
||||
value={pageInputValue}
|
||||
onChange={handlePageInputChange}
|
||||
onKeyDown={handlePageInputKeyDown}
|
||||
disabled={isDocumentLoading}
|
||||
/>
|
||||
{/* {numPages && (
|
||||
<span className="text-xs text-gray-400 mx-0.5">/</span>
|
||||
)}
|
||||
{numPages && (
|
||||
<span className="text-xs text-gray-500 font-medium min-w-[1.5rem] text-center">
|
||||
{numPages}
|
||||
</span>
|
||||
)} */}
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center justify-center h-7 px-2.5 text-white bg-primary hover:bg-primary-hover transition-colors duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary border-l border-primary-hover/20"
|
||||
onClick={handlePageJump}
|
||||
disabled={!numPages || isDocumentLoading}
|
||||
title="跳转"
|
||||
>
|
||||
<i className="ri-skip-forward-mini-line text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* 缩放提示 - 仅在DOCX文档时显示 */}
|
||||
{isDocx && (
|
||||
<div className="flex items-center px-2 py-1 text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded flex-shrink-0">
|
||||
@@ -465,13 +532,8 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="file-preview-content"
|
||||
className="file-preview-content flex-1 overflow-auto"
|
||||
ref={contentRef}
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 150px)',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'auto',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="pdf-interactive-container"
|
||||
@@ -489,4 +551,4 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1251,8 +1251,9 @@ 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)
|
||||
if (chain[0].data.page) {
|
||||
console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data)
|
||||
// console.log('点击了短链1左', chain[0].data.char_positions, 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);
|
||||
@@ -1504,7 +1505,26 @@ export function ReviewPointsList({
|
||||
value: string;
|
||||
char_positions?: CharPosition[];
|
||||
}>;
|
||||
ai_suggestion?: Record<string,string>;
|
||||
ai_suggestion?: {
|
||||
summary?: string;
|
||||
analysis?: {
|
||||
failure_reason?: string;
|
||||
solution_approach?: string;
|
||||
rule_understanding?: string;
|
||||
};
|
||||
suggestions?: Record<string, {
|
||||
reason: string;
|
||||
source: {
|
||||
page: number | null;
|
||||
type: string;
|
||||
field: string | null;
|
||||
};
|
||||
priority: string;
|
||||
confidence: number;
|
||||
suggested_value: string | null;
|
||||
}>;
|
||||
generated_at?: string;
|
||||
};
|
||||
message?: string;
|
||||
res?: boolean;
|
||||
} | undefined;
|
||||
@@ -1536,8 +1556,6 @@ export function ReviewPointsList({
|
||||
// 遍历fields,获取每个字段的值并生成对应的JSX元素
|
||||
if (config.fields) {
|
||||
Object.entries(config.fields).forEach(([key, value], index) => {
|
||||
if (key == '合同正文-附件序号、标题') {value.value = '签订', value.page = 1}
|
||||
if (key == '合同附件-序号、标题') {value.value = '电话', value.page = 1}
|
||||
const res = value.value.trim() !== '';
|
||||
fieldElements.push(
|
||||
<button
|
||||
@@ -1648,85 +1666,99 @@ export function ReviewPointsList({
|
||||
);
|
||||
}
|
||||
|
||||
config.ai_suggestion = {
|
||||
'合同正文-附件序号、标题': '签订-一致啊一致',
|
||||
'合同附件-序号、标题': '电话-明确啊明确'
|
||||
}
|
||||
|
||||
// 渲染AI建议(ai_suggestion)
|
||||
if (config.ai_suggestion) {
|
||||
// 遍历ai_suggestion对象
|
||||
Object.entries(config.ai_suggestion).forEach(([key, value], index) => {
|
||||
// 只渲染value不为空的项
|
||||
if (value && value.trim() !== '') {
|
||||
// 判断是否为PDF文档(禁用替换按钮)
|
||||
fileFormat = fileFormat?.replace(/\./g,'')
|
||||
const isPDF = fileFormat?.toUpperCase() === 'PDF';
|
||||
if (config.ai_suggestion?.suggestions && Object.keys(config.ai_suggestion.suggestions).length > 0) {
|
||||
// 判断是否为PDF文档(禁用替换按钮)
|
||||
fileFormat = fileFormat?.replace(/\./g,'')
|
||||
const isPDF = fileFormat?.toUpperCase() === 'PDF';
|
||||
|
||||
fieldElements.push(
|
||||
<div key={`ai-suggestion-${index}`} className="mb-3">
|
||||
{/* 字段名称标签 */}
|
||||
<div className="text-xs text-gray-600 mb-2 font-medium">
|
||||
<i className="ri-lightbulb-line text-yellow-500 mr-1"></i>
|
||||
AI建议修改 - {key}
|
||||
</div>
|
||||
// 遍历suggestions对象的key-value对
|
||||
Object.entries(config.ai_suggestion.suggestions).forEach(([key, suggestionValue], index) => {
|
||||
// 检查建议值是否存在(null 或有值都要渲染)
|
||||
const hasSuggestedValue = suggestionValue.suggested_value !== null && suggestionValue.suggested_value.trim() !== '';
|
||||
const isReplaceDisabled = !hasSuggestedValue || isPDF;
|
||||
|
||||
{/* 建议内容和替换按钮 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* 禁用编辑的文本输入框 */}
|
||||
<textarea
|
||||
value={value}
|
||||
readOnly
|
||||
className="flex-1 p-2 border border-gray-200 rounded text-xs bg-gray-50 text-gray-700 resize-none cursor-not-allowed overflow-y-auto"
|
||||
aria-label={`${key}的AI建议内容`}
|
||||
/>
|
||||
|
||||
{/* 意见替换按钮 */}
|
||||
{ !isPDF &&
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isPDF && onAiSuggestionReplace && config.fields) {
|
||||
// 从 config.fields[key] 中获取对应的字段信息
|
||||
const fieldData = config.fields[key];
|
||||
if (fieldData) {
|
||||
// 调用回调函数,传递搜索文本、替换文本和页码
|
||||
onAiSuggestionReplace(
|
||||
fieldData.value || '', // 搜索文本(原文)
|
||||
value || '', // 替换文本(AI建议)
|
||||
Number(fieldData.page) || 1 // 页码
|
||||
);
|
||||
// toastService.success(`已触发替换操作: ${key}`);
|
||||
} else {
|
||||
toastService.error(`未找到字段 ${key} 的原始数据`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={isPDF}
|
||||
className={`px-3 py-2 text-xs rounded whitespace-nowrap transition-colors
|
||||
${isPDF
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-primary text-white hover:bg-primary-hover active:bg-primary'
|
||||
}`}
|
||||
title={isPDF ? 'PDF文档不支持替换' : '点击执行一键替换'}
|
||||
aria-label={`替换${key}的内容`}
|
||||
>
|
||||
<i className="ri-exchange-line mr-1"></i>
|
||||
替换
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* PDF禁用提示 */}
|
||||
{/* {isPDF && (
|
||||
<div className="mt-1 text-xs text-gray-500 flex items-center">
|
||||
<i className="ri-information-line mr-1"></i>
|
||||
PDF文档不支持替换功能
|
||||
</div>
|
||||
)} */}
|
||||
fieldElements.push(
|
||||
<div key={`ai-suggestion-${index}`} className="mb-3">
|
||||
{/* 字段名称标签 */}
|
||||
<div className="text-xs text-gray-600 mb-2 font-medium">
|
||||
<i className="ri-lightbulb-line text-yellow-500 mr-1"></i>
|
||||
AI建议修改 - {key}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* 原因说明 */}
|
||||
<div className="mb-2 p-2 bg-amber-50 rounded border border-amber-200 text-xs text-gray-700">
|
||||
<div className="flex items-center">
|
||||
<i className="ri-information-line text-amber-600 mr-1 mt-0.5"></i>
|
||||
<div>
|
||||
<span>{suggestionValue.reason}</span>
|
||||
{suggestionValue.source.page !== null && (
|
||||
<span className="ml-2 text-gray-500">
|
||||
(来源页码: {suggestionValue.source.page})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 建议内容和替换按钮 */}
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* 文本输入框 */}
|
||||
<textarea
|
||||
value={suggestionValue.suggested_value || ''}
|
||||
readOnly
|
||||
disabled={!hasSuggestedValue}
|
||||
className={`flex-1 p-2 border rounded text-xs resize-none overflow-y-auto ${
|
||||
hasSuggestedValue
|
||||
? 'border-gray-200 bg-gray-50 text-gray-700 cursor-not-allowed'
|
||||
: 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label={`${key}的AI建议内容`}
|
||||
placeholder={!hasSuggestedValue ? '暂无建议值' : ''}
|
||||
/>
|
||||
|
||||
{/* 意见替换按钮 */}
|
||||
{ !isPDF &&
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isReplaceDisabled && onAiSuggestionReplace && config.fields) {
|
||||
// 从 config.fields[key] 中获取对应的字段信息
|
||||
const fieldData = config.fields[key];
|
||||
if (fieldData) {
|
||||
// 调用回调函数,传递搜索文本(原文)、替换文本(AI建议)和页码
|
||||
onAiSuggestionReplace(
|
||||
key, // 搜索文本(使用 suggestions 的 key)
|
||||
suggestionValue.suggested_value || '', // 替换文本(AI建议的 suggested_value)
|
||||
Number(fieldData.page) || 1 // 页码
|
||||
);
|
||||
} else {
|
||||
toastService.error(`未找到字段 ${key} 的原始数据`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={isReplaceDisabled}
|
||||
className={`px-3 py-2 text-xs rounded whitespace-nowrap transition-colors
|
||||
${isReplaceDisabled
|
||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-primary text-white hover:bg-primary-hover active:bg-primary'
|
||||
}`}
|
||||
title={
|
||||
isPDF
|
||||
? 'PDF文档不支持替换'
|
||||
: !hasSuggestedValue
|
||||
? '暂无建议值,无法替换'
|
||||
: '点击执行一键替换'
|
||||
}
|
||||
aria-label={`替换${key}的内容`}
|
||||
>
|
||||
<i className="ri-exchange-line mr-1"></i>
|
||||
替换
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -277,10 +277,10 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
||||
<i className="ri-arrow-left-line mr-1"></i> {isNavigating ? '返回中...' : '返回'}
|
||||
</button>
|
||||
<button
|
||||
className="ant-btn ant-btn-default flex items-center my-2"
|
||||
className="ant-btn ant-btn-default inline-flex items-center my-2"
|
||||
onClick={handleDownloadFile}
|
||||
>
|
||||
<i className="ri-file-download-line mr-1"></i> 下载原文件
|
||||
<i className="ri-file-download-line mr-1"></i> 下载
|
||||
</button>
|
||||
{/* <button
|
||||
className="ant-btn ant-btn-default flex items-center"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { DiffEditor } from "@monaco-editor/react";
|
||||
import { DiffEditor, loader } from "@monaco-editor/react";
|
||||
import type { editor } from "monaco-editor";
|
||||
import { pdfjs } from 'react-pdf';
|
||||
import mammoth from 'mammoth';
|
||||
@@ -23,6 +23,33 @@ import { DOCUMENT_URL } from '~/config/api-config';
|
||||
// Setup PDF.js worker
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||
|
||||
// 配置 Monaco Editor 使用本地资源(避免 CDN 加载超时)
|
||||
// Monaco Editor 资源已通过 npm run copy-monaco 复制到 public/monaco-editor
|
||||
if (typeof window !== 'undefined') {
|
||||
console.log('[Monaco] 使用本地资源加载');
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
vs: '/monaco-editor/vs'
|
||||
}
|
||||
});
|
||||
|
||||
// 添加加载超时监控和错误处理
|
||||
const initTimeout = setTimeout(() => {
|
||||
console.error('[Monaco] 加载超时(30秒)');
|
||||
toastService.error('代码编辑器加载超时,请刷新页面重试');
|
||||
}, 30000);
|
||||
|
||||
loader.init().then(() => {
|
||||
clearTimeout(initTimeout);
|
||||
console.log('[Monaco] ✅ 加载成功');
|
||||
}).catch((error: Error) => {
|
||||
clearTimeout(initTimeout);
|
||||
console.error('[Monaco] ❌ 加载失败:', error);
|
||||
toastService.error(`代码编辑器加载失败: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Document type enum
|
||||
type DocumentType = 'pdf' | 'docx' | 'unknown';
|
||||
|
||||
@@ -600,7 +627,7 @@ export function ComparePreview({ doc1Path, doc2Path }: ComparePreviewProps): JSX
|
||||
<strong>差异高亮说明:</strong>
|
||||
<span style={{ marginLeft: '8px' }}>
|
||||
<span style={{ color: '#dc3545', fontWeight: 'bold' }}>左侧红色</span>:原始版本 |
|
||||
<span style={{ color: '#28a745', fontWeight: 'bold', marginLeft: '8px' }}>右侧绿色</span>:修改版本 |
|
||||
<span style={{ color: '#28a745', fontWeight: 'bold', marginLeft: '8px' }}>右侧绿色</span>:比对版本 |
|
||||
<span style={{ color: '#666', fontWeight: 'bold', marginLeft: '8px' }}>深色高亮</span>:字符差异
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -389,7 +389,7 @@ export function PdfPreview({
|
||||
>
|
||||
<i className="ri-zoom-out-line text-sm"></i>
|
||||
</button>
|
||||
{/* 页码跳转控件 */}
|
||||
{/* 页码跳转控件
|
||||
<div className="inline-flex items-center flex-shrink-0 gap-1">
|
||||
<input
|
||||
type="text"
|
||||
@@ -412,7 +412,38 @@ export function PdfPreview({
|
||||
/ {numPages}
|
||||
</span>
|
||||
)}
|
||||
</div> */}
|
||||
{/* 页码跳转控件 */}
|
||||
<div className="inline-flex items-center bg-white border border-gray-300 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 flex-shrink-0 overflow-hidden">
|
||||
<div className="flex items-center px-2 py-1">
|
||||
<i className="ri-file-list-line text-sm text-gray-400 mr-1.5"></i>
|
||||
<input
|
||||
type="text"
|
||||
className="w-10 h-5 px-1 text-xs text-center text-gray-700 bg-transparent border-0 outline-none focus:text-primary transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="页码"
|
||||
value={pageInputValue}
|
||||
onChange={handlePageInputChange}
|
||||
onKeyDown={handlePageInputKeyDown}
|
||||
/>
|
||||
{numPages && (
|
||||
<span className="text-xs text-gray-400 mx-0.5">/</span>
|
||||
)}
|
||||
{numPages && (
|
||||
<span className="text-xs text-gray-500 font-medium min-w-[1.5rem] text-center">
|
||||
{numPages}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center justify-center h-7 px-2.5 text-white bg-primary hover:bg-primary-hover transition-colors duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary border-l border-primary-hover/20"
|
||||
onClick={handlePageJump}
|
||||
disabled={!numPages}
|
||||
title="跳转"
|
||||
>
|
||||
<i className="ri-skip-forward-mini-line text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap flex-shrink-0">
|
||||
比例:{zoomLevel}%
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user