33fbd6b860
When documents are processed through GraphRAG pipeline, coordinate enrichment produces text_bbox (paragraph-level coordinates) instead of char_positions (character-level OCR coordinates). Added resolveCharPositions() helper that converts text_bbox to CharPosition[] format, enabling PDF highlight rendering for GraphRAG-processed documents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
557 lines
21 KiB
TypeScript
557 lines
21 KiB
TypeScript
/**
|
||
* 文件预览组件
|
||
* 显示文档内容和评查点高亮
|
||
*/
|
||
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';
|
||
import { toastService } from '../ui/Toast';
|
||
|
||
// 直接从ReviewPointsList导入类型,避免循环依赖
|
||
import { type ReviewPoint } from './ReviewPointsList';
|
||
|
||
// 定义文档内容类型
|
||
interface FileContent {
|
||
title: string;
|
||
contractNumber: string;
|
||
path: string;
|
||
ocrResult?: {
|
||
__meta?: {
|
||
page_offset?: number;
|
||
};
|
||
}; // 添加ocrResult属性
|
||
ocr_result?:{
|
||
__meta?: {
|
||
page_offset?: number;
|
||
};
|
||
},
|
||
parties: {
|
||
partyA: {
|
||
name: string;
|
||
address: string;
|
||
representative: string;
|
||
phone: string;
|
||
};
|
||
partyB: {
|
||
name: string;
|
||
address: string;
|
||
representative: string;
|
||
phone: string;
|
||
};
|
||
};
|
||
sections: {
|
||
title: string;
|
||
content: string;
|
||
}[];
|
||
template_contract_path?: string;
|
||
}
|
||
|
||
interface FilePreviewProps {
|
||
fileContent: FileContent;
|
||
reviewPoints?: ReviewPoint[]; // 设为可选
|
||
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?: {
|
||
sub: string;
|
||
nick_name: string;
|
||
}; // 用户信息(用于 Collabora)
|
||
aiSuggestionReplace?: {
|
||
searchText: string;
|
||
replaceText: string;
|
||
pageNumber: number;
|
||
silentReplace?: boolean; // 是否静默替换(不显示面板)
|
||
}; // AI建议替换参数
|
||
isTemplate?: boolean;
|
||
}
|
||
|
||
// 暴露给父组件的接口
|
||
export interface FilePreviewHandle {
|
||
collaboraViewerRef: React.RefObject<CollaboraViewerHandle>;
|
||
}
|
||
|
||
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||
export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(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();
|
||
// 是文档类型 并且 是模板文件
|
||
const isDocx = fileExtension === 'docx';
|
||
// 是模板文件 或 是pdf文件就用pdf渲染
|
||
const isPdf = fileExtension === 'pdf';
|
||
|
||
// ✅ 将所有hooks移到条件return之前,确保遵守React Hooks规则
|
||
// Refs
|
||
const contentRef = useRef<HTMLDivElement>(null);
|
||
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>('');
|
||
const [isDocumentLoading, setIsDocumentLoading] = useState<boolean>(true); // 文档加载状态
|
||
const [isScrollingToTop, setIsScrollingToTop] = useState<boolean>(false); // 返回顶部loading状态
|
||
const [isClearingHighlights, setIsClearingHighlights] = useState<boolean>(false); // 清除高亮loading状态
|
||
|
||
// ✅ 将所有useEffect移到条件return之前
|
||
// 清除高亮:在组件卸载或文档路径变化时
|
||
useEffect(() => {
|
||
// 返回清理函数
|
||
return () => {
|
||
if (isDocx && collaboraViewerRef.current?.isReady) {
|
||
console.log('[FilePreview] 🔥 文档切换,调用 clearAllHighlights');
|
||
// 调用暴露的清除方法
|
||
collaboraViewerRef.current.clearAllHighlights().catch(error => {
|
||
console.error('[FilePreview] ✗ 清除高亮失败:', error);
|
||
});
|
||
}
|
||
};
|
||
}, [real_path, isDocx]); // 当文档路径变化时,清除旧文档的高亮
|
||
|
||
// 监听文档加载状态
|
||
useEffect(() => {
|
||
if (!isDocx) {
|
||
setIsDocumentLoading(false); // 非DOCX文件直接设为已加载
|
||
return;
|
||
}
|
||
|
||
// DOCX文件需要等待 Collabora 准备就绪
|
||
setIsDocumentLoading(true);
|
||
|
||
const checkInterval = setInterval(() => {
|
||
if (collaboraViewerRef.current?.isReady) {
|
||
setIsDocumentLoading(false);
|
||
clearInterval(checkInterval);
|
||
}
|
||
}, 200);
|
||
|
||
return () => {
|
||
clearInterval(checkInterval);
|
||
};
|
||
}, [isDocx, real_path]); // 当文档路径变化时重新检测
|
||
|
||
// DOCX 页数获取: 使用 requestPageInfo 方法
|
||
useEffect(() => {
|
||
if (!isDocx || isPdf) return; // PDF文件不需要执行
|
||
|
||
let intervalCleared = false;
|
||
|
||
// 等待 CollaboraViewer 准备就绪
|
||
const checkInterval = setInterval(() => {
|
||
if (intervalCleared) return;
|
||
|
||
if (!collaboraViewerRef.current?.isReady) {
|
||
// console.log('[FilePreview] 等待 Collabora 就绪...');
|
||
return;
|
||
}
|
||
|
||
clearInterval(checkInterval);
|
||
intervalCleared = true;
|
||
|
||
const iframeWindow = collaboraViewerRef.current.getIframeWindow?.();
|
||
if (!iframeWindow) {
|
||
console.warn('[FilePreview] 无法获取 iframe window');
|
||
return;
|
||
}
|
||
|
||
// 使用 requestPageInfo 获取页数
|
||
requestPageInfo(iframeWindow)
|
||
.then((info) => {
|
||
setNumPages(info.totalPages);
|
||
})
|
||
.catch((error) => {
|
||
console.warn('[FilePreview] 获取 DOCX 页数失败:', error.message);
|
||
});
|
||
}, 500);
|
||
|
||
// 清理定时器
|
||
return () => {
|
||
clearInterval(checkInterval);
|
||
};
|
||
}, [isDocx, isPdf]);
|
||
|
||
|
||
// 处理页面跳转
|
||
useEffect(() => {
|
||
if (isPdf) return; // PDF由PdfPreview处理
|
||
|
||
// 如果有目标页码,并且与上次相同,提示用户
|
||
if(targetPage && numPages && targetPage <= numPages && targetPage === prevTargetPageRef.current){
|
||
// toastService.success(`已跳转至目标页码`);
|
||
}
|
||
// 如果有目标页码,并且与上次不同或activeReviewPointId变化了,则执行跳转
|
||
if (targetPage && numPages && targetPage <= numPages) {
|
||
prevTargetPageRef.current = targetPage;
|
||
let newTargetPage = targetPage;
|
||
|
||
// 页码偏移量
|
||
try {
|
||
// 安全地访问ocrResult
|
||
if (fileContent.ocrResult && fileContent.ocrResult.__meta && fileContent.ocrResult.__meta.page_offset) {
|
||
// 可以根据需要使用page_offset调整目标页面
|
||
newTargetPage = targetPage + fileContent.ocrResult.__meta.page_offset;
|
||
}
|
||
} catch (error) {
|
||
console.error("访问ocrResult时出错:", error);
|
||
toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误'));
|
||
}
|
||
|
||
const pageElementId = `page-${newTargetPage}${isStructuredView ? '-structured' : ''}`;
|
||
|
||
const pageElement = document.getElementById(pageElementId);
|
||
if (pageElement) {
|
||
pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
} else {
|
||
console.warn(`未找到页面元素: ${pageElementId}`);
|
||
}
|
||
}
|
||
}, [targetPage, numPages, fileContent, activeReviewPointResultId, isStructuredView, isPdf]);
|
||
|
||
// 调试日志
|
||
// console.log('[FilePreview] 组件渲染', {
|
||
// real_path,
|
||
// fileExtension,
|
||
// isDocx,
|
||
// isPdf,
|
||
// hasPath: !!fileContent.path,
|
||
// hasTemplatePath: !!fileContent.template_contract_path
|
||
// });
|
||
|
||
// 如果是PDF文件,直接使用PdfPreview组件
|
||
if (isPdf && real_path) {
|
||
// console.log('[FilePreview] 渲染PDF预览', { real_path, targetPage, charPositions });
|
||
// console.log('[FilePreview] 渲染PDF预览', { fileContent });
|
||
const pageOffset = fileContent.ocrResult?.__meta?.page_offset || fileContent.ocr_result?.__meta?.page_offset || 0;
|
||
// console.log('pageOffset', pageOffset)
|
||
return (
|
||
<PdfPreview
|
||
filePath={real_path}
|
||
targetPage={targetPage}
|
||
charPositions={charPositions}
|
||
textBbox={textBbox}
|
||
isStructuredView={isStructuredView}
|
||
activeReviewPointResultId={activeReviewPointResultId}
|
||
pageOffset={pageOffset}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// DOCX 和其他文件类型继续使用原有逻辑
|
||
|
||
|
||
// 获取评查点对应的样式类
|
||
// const getHighlightClass = (status: string) => {
|
||
// switch (status) {
|
||
// case 'warning':
|
||
// return 'warning';
|
||
// case 'error':
|
||
// return 'error';
|
||
// case 'success':
|
||
// return 'success';
|
||
// default:
|
||
// return 'warning';
|
||
// }
|
||
// };
|
||
|
||
// 处理页码输入变化
|
||
const handlePageInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||
// 只允许输入数字
|
||
const value = e.target.value.replace(/\D/g, '');
|
||
setPageInputValue(value);
|
||
};
|
||
|
||
// 处理页码跳转(仅用于 DOCX)
|
||
const handlePageJump = async () => {
|
||
if (!pageInputValue) return;
|
||
|
||
const targetPageNum = parseInt(pageInputValue, 10);
|
||
const iframeWindow = collaboraViewerRef.current?.getIframeWindow?.();
|
||
|
||
if (!iframeWindow) {
|
||
toastService.warning('文档尚未加载完成,请稍候...');
|
||
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('请输入有效页码');
|
||
}
|
||
};
|
||
|
||
// 处理回车键跳转
|
||
const handlePageInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||
if (e.key === 'Enter') {
|
||
handlePageJump();
|
||
}
|
||
};
|
||
|
||
// 滚动到顶部(支持 PDF 和 DOCX)
|
||
const handleScrollToTop = async () => {
|
||
setIsScrollingToTop(true);
|
||
|
||
try {
|
||
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('返回顶部失败');
|
||
} finally {
|
||
// 延迟500ms后重置loading状态,给用户足够的视觉反馈
|
||
setTimeout(() => {
|
||
setIsScrollingToTop(false);
|
||
}, 500);
|
||
}
|
||
};
|
||
|
||
// 清除所有高亮(仅用于 DOCX)
|
||
const handleClearAllHighlights = async () => {
|
||
if (!collaboraViewerRef.current?.isReady) {
|
||
toastService.warning('文档尚未加载完成,请稍候...');
|
||
return;
|
||
}
|
||
|
||
setIsClearingHighlights(true);
|
||
try {
|
||
await collaboraViewerRef.current.clearAllHighlights();
|
||
console.log('[FilePreview] 已清除所有高亮');
|
||
toastService.success('已清除所有高亮');
|
||
} catch (error) {
|
||
console.error('[FilePreview] 清除高亮失败:', error);
|
||
toastService.error('清除高亮失败');
|
||
} finally {
|
||
// 延迟500ms后重置loading状态
|
||
setTimeout(() => {
|
||
setIsClearingHighlights(false);
|
||
}, 500);
|
||
}
|
||
};
|
||
|
||
// 渲染文档内容
|
||
const renderDocumentContent = () => {
|
||
// 如果路径无效,显示错误信息
|
||
if (!real_path) {
|
||
if(!fileContent.template_contract_path){
|
||
return (
|
||
<div className="text-red-500 p-4">
|
||
<p>无法加载文件:合同模板未上传</p>
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div className="text-red-500 p-4">
|
||
<p>无法加载文件:路径无效</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 根据文件类型选择不同的渲染方式
|
||
// 注意:PDF 文件已在组件开头使用 PdfPreview 组件提前返回
|
||
if (fileExtension === 'docx') {
|
||
// 使用 highlightValue 作为高亮文本(用户点击评查点时传递的实际文本值)
|
||
// 不再从 charPositions 提取,因为 charPositions 是 PDF 特有的坐标信息
|
||
const highlightText = highlightValue;
|
||
console.log('docx跳转的目标页',targetPage)
|
||
// DOCX文件使用Collabora Online预览
|
||
// 如果是模板预览,使用只读模式;否则使用编辑模式
|
||
return (
|
||
<CollaboraViewer
|
||
ref={collaboraViewerRef}
|
||
fileId={real_path}
|
||
mode={isTemplate ? "view" : "edit"}
|
||
// mode={"edit"}
|
||
userId={userInfo?.sub || 'unknown'}
|
||
userName={userInfo?.nick_name || ''}
|
||
targetPage={targetPage}
|
||
highlightText={highlightText}
|
||
aiSuggestionReplace={aiSuggestionReplace}
|
||
/>
|
||
);
|
||
} else {
|
||
// 非PDF/DOCX文件显示不支持消息
|
||
return (
|
||
<div className="text-gray-500 p-4">
|
||
<p>暂不支持预览此类型的文件:{fileExtension}</p>
|
||
</div>
|
||
);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<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 ? '模板预览' : '文件预览'}>
|
||
{isStructuredView ? '模板预览' : '文件预览'}
|
||
</span>
|
||
</div>
|
||
<div className="file-preview-actions flex items-center ml-2 min-w-0 flex-1 justify-end overflow-hidden gap-2">
|
||
{/* 返回顶部按钮 - 不在模板预览时显示 */}
|
||
{!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
|
||
? 'opacity-50 cursor-not-allowed'
|
||
: 'hover:bg-red-600 hover:border-red-600'
|
||
}`}
|
||
onClick={handleClearAllHighlights}
|
||
disabled={isClearingHighlights || isDocumentLoading}
|
||
title="清除所有高亮"
|
||
>
|
||
{isClearingHighlights ? (
|
||
<i className="ri-loader-4-line text-sm animate-spin"></i>
|
||
) : (
|
||
<i className="ri-eraser-line text-sm"></i>
|
||
)}
|
||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||
{isClearingHighlights ? '清除中...' : '清除高亮'}
|
||
</span>
|
||
</button>
|
||
)}
|
||
{/* 页码跳转控件 - 不在模板预览时显示 */}
|
||
{!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">
|
||
<i className="ri-zoom-in-line text-sm mr-1"></i>
|
||
<span className="whitespace-nowrap">Ctrl+滚轮</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="file-preview-content flex-1 overflow-auto"
|
||
ref={contentRef}
|
||
>
|
||
<div
|
||
className="pdf-interactive-container"
|
||
style={{
|
||
position: 'relative',
|
||
height: '100%',
|
||
width: '100%',
|
||
display: 'block',
|
||
textAlign: 'center',
|
||
padding: 0
|
||
}}
|
||
>
|
||
{renderDocumentContent()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
});
|