"examples/avsr/models/conformer_rnnt.py" did not exist on "29deb085f097f584223e0e276050b867577693d7"
Commit 791e653c authored by dechen lin's avatar dechen lin
Browse files

feat: add web project

parent 9d689790
import { EXTRACTOR_TYPE_LIST } from "@/types/extract-task-type";
import odlLogo from "@/assets/pdf/odl-logo.svg";
import labelLLMLogo from "@/assets/pdf/label-llm.svg";
import labelULogo from "@/assets/pdf/labelU.svg";
export default {
"extractor.side.tabList": [
{
label: "PDF Extraction",
type: EXTRACTOR_TYPE_LIST.pdf,
},
// {
// label: "Formula Extraction",
// type: EXTRACTOR_TYPE_LIST.formula,
// },
],
"extractor.side.guide_list": [
{
type: "odl",
icon: odlLogo,
title: "OpenDataLab",
desc: "Covers a huge amount of high-quality, multimodal datasets",
goToText: "Go Now",
link: "https://opendatalab.com",
},
{
type: "labelU",
icon: labelULogo,
title: "Label U Labeling Tool",
desc: "Lightweight open source annotation tools",
goToText: "github",
link: "https://github.com/opendatalab/labelU",
},
{
type: "labelLLM",
icon: labelLLMLogo,
[`zh-CN-title`]: "LabelLLM Labeling Tool",
title: "LabelLLM Labeling Tool",
desc: "Specializing in dialogue annotation for large language models",
goToText: "github",
link: "https://github.com/opendatalab/LabelLLM",
},
],
};
import { EXTRACTOR_TYPE_LIST } from "@/types/extract-task-type";
import odlLogo from "@/assets/pdf/odl-logo.svg";
import labelLLMLogo from "@/assets/pdf/label-llm.svg";
import labelULogo from "@/assets/pdf/labelU.svg";
export default {
"extractor.side.tabList": [
{
label: "PDF文档提取",
type: EXTRACTOR_TYPE_LIST.pdf,
},
// {
// label: "公式检测与识别",
// type: EXTRACTOR_TYPE_LIST.formula,
// },
],
"extractor.side.guide_list": [
{
type: "odl",
icon: odlLogo,
title: "OpenDataLab",
desc: "涵盖海量优质、多模态数据集",
goToText: "立即前往",
link: "https://opendatalab.com",
},
{
type: "labelU",
icon: labelULogo,
title: "Label U 标注工具",
desc: "轻量级开源标注工具",
goToText: "github",
link: "https://github.com/opendatalab/labelU",
},
{
type: "labelLLM",
icon: labelLLMLogo,
title: "LabelLLM 标注工具",
desc: "专攻于大模型的对话标注",
goToText: "github",
link: "https://github.com/opendatalab/LabelLLM",
},
],
};
{
"extractor.common.upload": "点击上传文件",
"extractor.common.try": "试一试:",
"extractor.home": "首页",
"extractor.button.download": "下载",
"extractor.button.lineWrap": "换行",
"extractor.button.fullScreen": "全屏",
"extractor.button.exitFullScreen": "退出全屏",
"extractor.button.showLayer": "显示识别结果",
"extractor.button.hiddenLayer": "隐藏识别结果",
"extractor.button.reUpload": "重新上传",
"extractor.error": "提取失败",
"extractor.common.loading": "加载中",
"extractor.law": "请确保您上传的文件合法合规,我们不承担因文件内容产生的法律责任。《信息保护政策》 《儿童信息保护政策》《服务协议》|© All Rights Reserved.沪ICP备2021009351号-21",
"extractor.failed": "不可提取,暂无可展示数据",
"extractor.common.extracting": "提取中,请稍等",
"extractor.common.extracting.queue": "正在排队提取,当前排在第 {id} 位",
"extractor.common.pdf.demo1": "示例1.pdf",
"extractor.common.pdf.demo2": "示例2.pdf",
"extractor.common.formula.detect.demo1": "公式检测1.jpg",
"extractor.common.formula.extract.demo1": "公式识别1.jpg",
"extractor.common.login.desc": "登录后可使用完整功能",
"extractor.markdown.preview": "预览",
"extractor.markdown.code": "代码",
"extractor.home.title": "欢迎使用 Miner U",
"extractor.home.subTitle": "上传文档,智能提取为 Markdown 格式",
"extractor.side.extractTask": "提取任务",
"extractor.side.extractTask.title": "请上传 5M 以内的 PDF 文档 ( 10 页以内)或 JPG/PNG 图片",
"extractor.pdf.title": "PDF文档提取",
"extractor.pdf.subTitle": "支持文本/扫描型 PDF 解析,识别各类版面元素并转换为多模态 Markdown 格式",
"extractor.common.pdf.upload.tip": "请上传 PDF 文档",
"extractor.pdf.ocr": "OCR 识别模式",
"extractor.pdf.ocr.popover": " 默认将自动识别PDF类型(文本型、扫描型),并根据识别结果选择采用文本识别或者OCR识别方式。 如开启,将对所有类型PDF采用OCR识别方式。",
"extractor.formula.title": "定位图片中的行内、行间公式,生成边界框",
"extractor.formula.title2": "将图片中的数学公式识别为 laTex 格式,支持多行公式、手写公式识别",
"extractor.formula.upload.text": "点击上传图片",
"extractor.formula.popover.extract": "为获得最佳的公式识别效果,请上传清晰、无水印的包含数学公式的图片,如下图",
"extractor.formula.popover.detect": "为获得最佳的公式识别效果,请裁剪图片,聚焦公式部分,上传清晰、无水印的数学公式图片,如下图",
"extractor.formula.upload.accept": "请上传 5M 以内的JPG/PNG 图片",
"extractor.formula.upload.try": "请上传包含数学公式的图片 示例:",
"extractor.guide.title": "欢迎使用更多开源产品 🎉",
"extractor.queue": "提取记录",
"extractor.queue.delete": "确认删除此文件?",
"extractor.queue.extracting": "提取中",
"extractor.feedback.title1": "您对整体提取效果是否满意 ?",
"extractor.feedback.title3": "期待您的建议,帮助我们更好的优化",
"extractor.feedback.up.title": "您期望看到哪些改进?",
"extractor.feedback.down.title": "您感到不满意的原因是?",
"extractor.feedback.up": "满意",
"extractor.feedback.down": "不满意",
"extractor.feedback.input.placeholder": "请输入您的改进建议",
"extractor.feedback.input.submit": "提交",
"extractor.feedback.success": "感谢你的反馈",
"extractor.queue.delete.success": "删除成功"
}
\ No newline at end of file
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
@import '@/styles/variable.scss';
.gradientBtn {
width: 179px;
height: 37px;
border-radius: 4px;
font-size: 14px;
color: rgba(255, 255, 255, 0.95);
// background: linear-gradient(110deg, #38A0FF -33.56%, #0D53DE 32.84%, #5246FF 102.05%);
background: #3477EB;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: #3477EB;
}
}
.linearBlue {
// TIP: 这里为啥用bg呢,因为ui稿给的参数是假的
background: url('@/assets/pdf/pdf-upload.png');
background-size: cover;
}
.tryText {
font-size: 13px;
line-height: 20px;
background: linear-gradient(107deg, #38A0FF -24.14%, #0D53DE 30.09%, #5246FF 86.61%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.extractorContainer {
min-width: $page-min-witch;
}
\ No newline at end of file
import DarkLogo from "@/assets/svg/logo.svg";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import commonStyles from "./index.module.scss";
import { EXTRACTOR_TYPE_LIST } from "@/types/extract-task-type";
import extractorPdfIcon from "@/assets/pdf/extractor-pdf.svg";
import extractorTableIcon from "@/assets/pdf/extractor-table.svg";
import extractorFormulaIcon from "@/assets/pdf/extractor-formula.svg";
import { useIntl } from "react-intl";
import cls from "classnames";
import ExtractorGuide from "@/pages/extract/components/extractor-guide";
import ExtractorQueue from "@/pages/extract/components/extractor-queue";
import ExtractorLang from "@/pages/extract/components/extractor-lang";
interface IExtractorSideProps {
className?: string;
}
interface TabItem {
label: string;
type: string;
}
export const ExtractorSide = ({ className = "" }: IExtractorSideProps) => {
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const { messages } = useIntl();
console.log("test-params-jobID", params.jobID);
const menuClass =
"px-2 py-2.5 mb-1 text-[0.875rem] text-[#121316]/[0.8] font-semibold rounded h-10 flex items-center cursor-pointer hover:bg-[#0d53de]/[0.05]";
const handleMenuClick = (type: string) => {
navigate(`/OpenSourceTools/Extractor/${type}`);
};
const goToOpenSource = () => {
navigate("/OpenSourceTools/Extractor/");
};
const tabList =
(messages?.["extractor.side.tabList"] as unknown[] as TabItem[]) || [];
const getIconStyle = (type: string) => {
const activeClassName = "!bg-[#0d53de]/[0.05] !text-[#0D53DE]";
const path = location.pathname;
const regex = /\/Extractor\/([^/]+)(\/|$)/;
const match = params?.jobID ? "" : path.match(regex)?.[1] || "/";
const getIcon = () => {
switch (type) {
case EXTRACTOR_TYPE_LIST.pdf:
return extractorPdfIcon;
case EXTRACTOR_TYPE_LIST.table:
return extractorTableIcon;
case EXTRACTOR_TYPE_LIST.formula:
return extractorFormulaIcon;
}
};
return {
icon: getIcon(),
tabClassName: match === type ? activeClassName : "",
};
};
return (
<div
className={cls(
`w-[240px] min-w-[240px] h-full px-4 py-6 flex flex-col justify-start border-r-[1px] border-y-0 border-l-0 border-solid border-[#EBECF0] select-none`,
commonStyles.linearBlue,
className
)}
>
<div className={""}>
<div className="h-[2rem] mb-6 flex justify-between items-center">
<img
className="h-full cursor-pointer"
src={DarkLogo}
alt=""
onClick={goToOpenSource}
/>
<ExtractorGuide />
</div>
{/* tab-list */}
<div className="mb-2">
{tabList.map((i) => (
<div
key={i.type}
className={cls(menuClass, getIconStyle(i.type)?.tabClassName)}
onClick={() => handleMenuClick(i.type)}
>
<img src={getIconStyle(i.type).icon} className="mr-2 w-6 h-6" />
{i.label}
</div>
))}
</div>
</div>
<div className="bg-[#0d53de]/[0.08] w-full h-[1px] mt-2 mb-4"></div>
<ExtractorQueue className="flex-1 overflow-y-auto mb-6" />
<ExtractorLang className="absolute bottom-6" />
</div>
);
};
.extractorGuide {
:global {
.ant-popover-content, .ant-popover-inner {
border-radius: 12px !important;
overflow: hidden;
box-shadow: 0px 8px 26px 0px rgba(0, 0, 0, 0.12);
}
.ant-popover-inner-content {
padding: 24px !important;
}
.ant-popover-arrow {
display: none !important;
}
}
}
import { Popover } from "antd";
import guideToolsSvg from "@/assets/pdf/guideTools.svg";
import style from "./index.module.scss";
import { useIntl } from "react-intl";
import IconFont from "@/components/icon-font";
import { windowOpen } from "@/utils/windowOpen";
interface GuideItem {
type: string;
icon: string;
"zh-CN-title": string;
title: string;
desc: string;
goToText: string;
link: string;
}
const ExtractorGuide = () => {
const { formatMessage, messages } = useIntl();
const EXTRACTOR_GUIDE_ITEM_LIST = (messages?.["extractor.side.guide_list"] ||
[]) as unknown as GuideItem[];
console.log("test-EXTRACTOR_GUIDE_ITEM_LIST", EXTRACTOR_GUIDE_ITEM_LIST);
const content = (
<div>
<div className="text-[1.25rem] font-semibold mt-3 mb-2 ml-4">
{formatMessage({
id: "extractor.guide.title",
})}
</div>
<hgroup>
{EXTRACTOR_GUIDE_ITEM_LIST?.map((i) => {
return (
<div
key={i.type}
className="flex p-4 items-center cursor-pointer hover:bg-[#F4F5F9] rounded group h-[6.5rem]"
onClick={() => windowOpen(i.link)}
>
<img
src={i.icon}
alt=""
className="w-[1.5rem] h-[1.5rem] transition-all mr-[0.75rem]"
/>
<div className="">
<div className="font-semibold transition-all text-[1rem]">
{i.title}
</div>
<div className="text-base text-[13px] text-[#121316]/[0.6] transition-all ">
{i.desc}
</div>
<div className="h-0 mt-2 overflow-hidden !text-[13px] text-[#121316]/[0.8] transition-all group-hover:h-auto">
{i.goToText}
<IconFont type="icon-ArrowRightOutlined" className="ml-1" />
</div>
</div>
</div>
);
})}
</hgroup>
</div>
);
return (
<Popover
overlayClassName={style.extractorGuide}
content={content}
showArrow={false}
placement="right"
>
<img
className="w-[1.32rem] h-[1.32rem] p-0.5 hover:rotate-45 transition-all cursor-pointer rounded"
src={guideToolsSvg}
alt="guideToolsSvg"
/>
</Popover>
);
};
export default ExtractorGuide;
import LangChangeIcon from "@/assets/pdf/lang-change.svg";
import { useLanguageStore } from "@/store/languageStore";
import cls from "classnames";
interface ExtractorLangProps {
className?: string;
}
const ExtractorLang: React.FC<ExtractorLangProps> = ({ className }) => {
const { toggleLanguage } = useLanguageStore();
const changeLang = () => {
toggleLanguage?.();
};
return (
<>
<img
onClick={() => changeLang()}
src={LangChangeIcon}
alt="LangChangeIcon"
className={cls(
"w-[1.5rem] h-[1.5rem] cursor-pointer object-cover hover:bg-[#0D53DE]/[0.1] rounded cursor-pointer",
className
)}
/>
</>
);
};
export default ExtractorLang;
import IconFont from "@/components/icon-font";
import { useIntl } from "react-intl";
import extractorQueueSvg from "@/assets/pdf/extractor-queue.svg";
import { useNavigate, useParams } from "react-router-dom";
import {
EXTRACTOR_TYPE_LIST,
ExtractTaskType,
} from "@/types/extract-task-type";
import cls from "classnames";
import { useLatest, useRequest } from "ahooks";
import { deleteExtractJob, getExtractorHistory } from "@/api/extract";
import { message, Popconfirm, Tooltip } from "antd";
import { useEffect } from "react";
import { ADD_TASK_LIST, UPDATE_TASK_LIST } from "@/constant/event";
import { findIndex } from "lodash";
import { TextTooltip } from "@/components/text-tooltip";
interface ExtractorQueueProps {
className?: string;
}
const ExtractorQueue: React.FC<ExtractorQueueProps> = ({ className }) => {
const { formatMessage, locale } = useIntl();
const navigate = useNavigate();
const params = useParams();
console.log("test-params", params);
const { data: taskList, mutate } = useRequest(() => {
return getExtractorHistory({
pageNo: 1,
pageSize: 100,
}).then((res) => {
return res?.list?.filter((i) => !!i.id && !!i.type) || [];
});
});
let timeout: NodeJS.Timeout | null = null;
const activeClassName = "!bg-[#0d53de]/[0.05] !text-[#0D53DE]";
const handleExtractor = (originType: ExtractTaskType, id: string) => {
const type = originType?.split("-")[0];
const detailType = originType?.split("-")[1];
if (type === EXTRACTOR_TYPE_LIST.formula.toLowerCase()) {
navigate(`/OpenSourceTools/Extractor/formula/${id}?type=${detailType}`);
} else if (type === EXTRACTOR_TYPE_LIST.pdf.toLowerCase()) {
navigate(`/OpenSourceTools/Extractor/PDF/${id}`);
} else if (type === EXTRACTOR_TYPE_LIST.table.toLocaleLowerCase()) {
navigate(`/OpenSourceTools/Extractor/table/${id}`);
}
return;
};
const cancel = (e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
e?.preventDefault();
};
const confirm = (id: string) => {
const deleteIndex = findIndex(taskList, (i) => i.id === id);
const nextJob = taskList?.[deleteIndex + 1]
? taskList?.[deleteIndex + 1]
: taskList?.[deleteIndex - 1];
mutate(taskList?.filter((i) => i.id !== id));
console.log("test-next-job", nextJob);
deleteExtractJob(id).then(() => {
console.log("test-delete-job", id);
message.success(formatMessage({ id: "extractor.queue.delete.success" }));
});
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
handleExtractor(nextJob?.type, nextJob?.id);
}, 10);
};
const taskListRef = useLatest(taskList);
const handleAddList = ({ detail }: CustomEvent) => {
const taskData = detail as any;
mutate(
[
{
fileName: taskData?.fileName,
id: taskData?.id,
type: taskData?.type,
state: taskData?.state, // 提取状态
},
].concat(taskListRef?.current)
);
};
useEffect(() => {
const handleUpdateList = ({ detail }: CustomEvent) => {
const taskData = detail as any;
taskListRef?.current?.forEach((i) => {
if (i.id === taskData?.id) {
i.state = taskData?.state || taskData?.state;
}
});
mutate(taskListRef?.current);
};
document.addEventListener(
UPDATE_TASK_LIST,
handleUpdateList as EventListener
);
document.addEventListener(ADD_TASK_LIST, handleAddList as EventListener);
return () => {
document.removeEventListener(
UPDATE_TASK_LIST,
handleUpdateList as EventListener
);
document.removeEventListener(
ADD_TASK_LIST,
handleAddList as EventListener
);
};
}, []);
useEffect(() => {
mutate(taskListRef?.current);
}, [locale]);
console.log("test-dd", params);
return (
<div className={cls("w-full flex flex-col mb-3", className)}>
<header className="flex items-center px-2 py-[0.625rem] text-[#121316]/[0.8] text-[0.875rem] font-semibold">
<img
src={extractorQueueSvg}
className="w-6 h-6 mr-2 "
alt="extractorQueueSvg"
/>
{formatMessage({
id: "extractor.queue",
})}
</header>
<hgroup className="overflow-auto flex-1 scrollbar-thin">
{taskList?.map((i, index) => {
return (
<div
className={cls(
"group h-[2.5rem] flex items-center px-4 py-2.5 mb-1 text-[#121316]/[0.8] pl-10 text-sm rounded h-10 flex items-center cursor-pointer hover:bg-[#0d53de]/[0.05]",
params?.jobID === String(i?.id) && activeClassName
)}
key={i?.fileName + index + i?.id}
onClick={() => handleExtractor(i.type as any, i.id)}
>
<span className="truncate mr-2 max-w-[calc(100%-2rem)]">
<TextTooltip trigger="hover" str={i?.fileName} />
</span>
<>
{i?.state === "failed" && (
<Tooltip
title={formatMessage({
id: "extractor.error",
})}
>
<IconFont
type={"icon-attentionFilled"}
className="text-[#FF8800] mr-1"
/>
</Tooltip>
)}
<Popconfirm
title={formatMessage({ id: "extractor.queue.delete" })}
description={<div className="my-4"></div>}
onConfirm={(e) => {
e?.stopPropagation();
e?.preventDefault();
confirm(i.id);
}}
onCancel={cancel}
okText={formatMessage({ id: "common.confirm" })}
cancelText={formatMessage({ id: "common.cancel" })}
okButtonProps={{
style: {
backgroundColor: "#F5483B",
},
}}
>
<IconFont
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
type="icon-shanchu"
className="hidden ml-auto text-[1rem] text-[#121316]/[0.8] hover:text-[#0D53DE] group-hover:block"
/>
</Popconfirm>
</>
</div>
);
})}
</hgroup>
</div>
);
};
export default ExtractorQueue;
.githubBtn {
position: relative;
width: 100%;
cursor: pointer;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.25rem;
overflow: hidden;
border-radius: 8px;
cursor: pointer;
filter: blur(0px);
z-index: 0;
&::before {
width: 100%;
height: 100%;
display: block;
content: "";
position: absolute;
top: 0;
left: 0;
background: linear-gradient(to bottom, rgba(185,214,246,1) -100%, rgba(244,247,254,) 100%);
z-index: 0;
}
& > span {
border-radius: 7px;
display:inline-flex;
width: calc(100% - 2px);
height: calc(100% - 2px);
background: linear-gradient(180deg, #5C93FF1F -160.94%, rgba(255, 255, 255, 1) 80%);
z-index: 1;
filter: blur(0px);
justify-content: center;
align-items: center;
font-size: 16px;
&:hover {
background: linear-gradient(180deg, #5C93FF1F -60.94%, rgba(255, 255, 255, 1) 80%);
filter: blur(0px);
}
span:nth-child(3){
color: var(--80-text-4, rgba(18, 19, 22, 0.80));
-webkit-background-clip: text;
background-clip: text;
}
}
}
.githubText {
/* 正文/加粗text-1-semibold */
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 21px; /* 150% */
color: #121316;
}
import githubSvg from "@/assets/pdf/github.svg";
import { windowOpen } from "@/utils/windowOpen";
import styles from "./index.module.scss";
import cls from "classnames";
const ExtractorRepo = () => {
return (
<div
className={cls(styles.githubBtn)}
onClick={() =>
windowOpen("https://github.com/opendatalab/MinerU", "_blank")
}
>
<span className="text-sm ">
<img src={githubSvg} className="mr-2" />
<span className="!text-[14px] ml-[0.5rem]">🎉</span>
</span>
</div>
);
};
export default ExtractorRepo;
import LoadingAnimation from "@/components/loading-animation";
import { ExclamationCircleFilled } from "@ant-design/icons";
import cls from "classnames";
export const IframeLoading = ({
filename,
type,
text,
errorElement,
classNameTitle = "",
showHeader,
}: {
filename?: string;
type: "loading" | "error";
text?: string;
errorElement?: React.ReactElement;
classNameTitle?: string;
showHeader?: boolean;
}) => {
return (
<div className="flex flex-col h-full text-sm text-[#121316]/[0.8] whitespace-nowrap ">
{showHeader && (
<div
className={cls(
"h-[47px] border-0 border-solid border-b-[1px] border-[#EBECF0] w-full pl-[24px]",
classNameTitle
)}
>
{filename}
</div>
)}
<div className="flex-1 flex justify-center items-center">
{type === "error" ? (
errorElement ? (
errorElement
) : (
<>
<ExclamationCircleFilled
style={{ color: "#FF8800" }}
rotate={180}
/>
<span className="ml-2.5">上传失败,请</span>
<span className="text-[#0D53DE] ml-1 cursor-pointer">
重新上传
</span>
</>
)
) : (
<>
<LoadingAnimation />
<span className="ml-2.5">{text || "PDF 上传中,请稍等..."}</span>
</>
)}
</div>
</div>
);
};
import React, { useEffect, useRef, useState, useMemo, forwardRef, useImperativeHandle, useCallback } from 'react';
import cls from 'classnames';
import { isObjEqual } from '@/utils/render';
import { useSize } from 'ahooks';
interface IImageLayersViewerProps {
imageUrl: string;
imageWidth: number;
imageHeight: number;
layout: Array<{
category_id: number;
poly: number[];
score: number;
latex?: string;
}>;
layerVisible?: boolean;
disableZoom?: boolean;
className?: string;
onChange?: (data: { scale: number }) => void;
}
export interface ImageLayerViewerRef {
containerRef: HTMLDivElement | null;
zoomIn: () => void;
zoomOut: () => void;
scale: number;
updateScaleAndPosition: () => void;
}
const ImageLayerViewer = forwardRef<ImageLayerViewerRef, IImageLayersViewerProps>(
({ imageUrl, imageHeight, imageWidth, onChange, layout, disableZoom, className = '', layerVisible = true }, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
const imageCanvasRef = useRef<HTMLCanvasElement>(null);
const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef<number | null>(null);
const containerSize = useSize(containerRef);
const [scale, setScale] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [padding, setPadding] = useState({ left: 0, top: 0 });
const minZoom = 0.1;
const maxZoom = 3;
const zoomSensitivity = 0.001;
const zoomStep = 0.1;
const dpr = useMemo(() => window.devicePixelRatio || 1, []);
const image = useMemo(() => {
const img = new Image();
img.src = imageUrl;
return img;
}, [imageUrl]);
const calculateInitialScaleAndPosition = useCallback(() => {
if (!containerRef.current) return { initialScale: 1, initialPosition: { x: 0, y: 0 } };
const containerWidth = containerRef.current.clientWidth;
const containerHeight = containerRef.current.clientHeight;
const scaleX = containerWidth / imageWidth;
const scaleY = containerHeight / imageHeight;
const initialScale = Math.min(scaleX, scaleY, 1); // Ensure it doesn't scale up initially
const scaledWidth = imageWidth * initialScale;
const scaledHeight = imageHeight * initialScale;
const initialPosition = {
x: (containerWidth - scaledWidth) / 2,
y: (containerHeight - scaledHeight) / 2
};
return { initialScale, initialPosition };
}, [imageWidth, imageHeight]);
const updateScaleAndPosition = useCallback(() => {
const { initialScale, initialPosition } = calculateInitialScaleAndPosition();
setScale(initialScale);
setPosition(initialPosition);
setPadding({ left: 0, top: 0 });
}, [calculateInitialScaleAndPosition]);
useEffect(() => {
updateScaleAndPosition();
}, [imageWidth, imageHeight]);
const drawImage = useCallback(() => {
const ctx = imageCanvasRef.current?.getContext('2d');
if (!ctx || !image.complete) return;
const scaledWidth = imageWidth * scale;
const scaledHeight = imageHeight * scale;
ctx.canvas.width = scaledWidth * dpr;
ctx.canvas.height = scaledHeight * dpr;
ctx.canvas.style.width = `${scaledWidth}px`;
ctx.canvas.style.height = `${scaledHeight}px`;
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, scaledWidth, scaledHeight);
ctx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
}, [image, imageWidth, imageHeight, scale, dpr]);
const drawLayout = useCallback(() => {
const ctx = overlayCanvasRef.current?.getContext('2d');
if (!ctx) return;
const scaledWidth = imageWidth * scale;
const scaledHeight = imageHeight * scale;
ctx.canvas.width = scaledWidth * dpr;
ctx.canvas.height = scaledHeight * dpr;
ctx.canvas.style.width = `${scaledWidth}px`;
ctx.canvas.style.height = `${scaledHeight}px`;
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, scaledWidth, scaledHeight);
layout?.forEach((item) => {
const [x1, y1, x2, y2, x3, y3, x4, y4] = item.poly.map((coord) => coord * scale);
switch (item.category_id) {
case 9:
ctx.fillStyle = 'rgba(230, 113, 230, 0.4)';
ctx.strokeStyle = 'rgba(230, 113, 230, 1)';
break;
case 8:
ctx.fillStyle = 'rgba(240, 240, 124, 0.4)';
ctx.strokeStyle = 'rgba(240, 240, 124, 1)';
break;
case 13:
ctx.fillStyle = 'rgba(150, 232, 172, 0.4)';
ctx.strokeStyle = 'rgba(150, 232, 172, 1)';
break;
case 14:
ctx.fillStyle = 'rgba(230, 122, 171, 0.4)';
ctx.strokeStyle = 'rgba(230, 122, 171, 1)';
break;
default:
ctx.fillStyle = 'transparent';
ctx.strokeStyle = 'transparent';
}
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.lineTo(x4, y4);
ctx.closePath();
ctx.fill();
ctx.stroke();
});
}, [layout, scale, dpr]);
const updateScale = useCallback(
(newScale: number, clientX: number, clientY: number) => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const containerWidth = rect.width;
const containerHeight = rect.height;
const x = clientX - rect.left;
const y = clientY - rect.top;
const prevScaledWidth = imageWidth * scale;
const prevScaledHeight = imageHeight * scale;
const newScaledWidth = imageWidth * newScale;
const newScaledHeight = imageHeight * newScale;
let newPosition = {
x: position.x - ((x - position.x) * (newScaledWidth - prevScaledWidth)) / prevScaledWidth,
y: position.y - ((y - position.y) * (newScaledHeight - prevScaledHeight)) / prevScaledHeight
};
// Center the image if it's smaller than the container
if (newScaledWidth < containerWidth) {
newPosition.x = (containerWidth - newScaledWidth) / 2;
}
if (newScaledHeight < containerHeight) {
newPosition.y = (containerHeight - newScaledHeight) / 2;
}
setScale(newScale);
setPosition(newPosition);
// Calculate new padding
const newPadding = {
left: Math.max(0, -newPosition.x),
top: Math.max(0, -newPosition.y)
};
setPadding(newPadding);
}
},
[scale, position, imageWidth, imageHeight]
);
const handleZoom = useCallback(
(delta: number, clientX: number, clientY: number) => {
const newScale = scale * Math.exp(-delta * zoomSensitivity);
const boundedNewScale = Math.max(minZoom, Math.min(newScale, maxZoom));
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
updateScale(boundedNewScale, clientX, clientY);
});
},
[scale, updateScale]
);
const handleCenterZoom = useCallback(
(zoomIn: boolean) => {
const newScale = zoomIn ? scale * (1 + zoomStep) : scale / (1 + zoomStep);
const boundedNewScale = Math.max(minZoom, Math.min(newScale, maxZoom));
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
updateScale(boundedNewScale, centerX, centerY);
}
},
[scale, updateScale]
);
const zoomIn = useCallback(() => {
handleCenterZoom(true);
}, [handleCenterZoom]);
const zoomOut = useCallback(() => {
handleCenterZoom(false);
}, [handleCenterZoom]);
useImperativeHandle(
ref,
() => ({
containerRef: containerRef.current,
zoomIn,
zoomOut,
scale,
updateScaleAndPosition
}),
[zoomIn, zoomOut, scale]
);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
handleZoom(e.deltaY * 4.8, e.clientX, e.clientY);
}
};
container.addEventListener('wheel', handleWheel, { passive: false });
return () => {
container.removeEventListener('wheel', handleWheel);
};
}, [handleZoom]);
useEffect(() => {
if (containerRef?.current) {
containerRef.current?.scrollTo({
left: padding.left,
top: padding.top
});
}
}, [padding]);
useEffect(() => {
const draw = () => {
drawImage();
drawLayout();
};
if (image.complete) {
draw();
} else {
image.onload = draw;
}
}, [image, drawImage, drawLayout]);
useEffect(() => {
if (overlayCanvasRef.current) {
overlayCanvasRef.current.style.opacity = layerVisible ? '1' : '0';
}
}, [layerVisible]);
useEffect(() => {
onChange?.({ scale });
}, [scale]);
console.log('test-render');
return (
<div className={cls(className, 'w-full h-full overflow-auto scrollbar-thin relative')} ref={containerRef}>
<div
style={{
paddingLeft: `${padding.left}px`,
paddingTop: `${padding.top}px`
}}
>
<div
className="absolute"
style={{
width: `${imageWidth * scale}px`,
height: `${imageHeight * scale}px`,
transform: `translate(${position.x}px, ${position.y}px)`
}}
>
<canvas
ref={imageCanvasRef}
style={{
width: `${imageWidth * scale}px`,
height: `${imageHeight * scale}px`
}}
/>
<canvas
ref={overlayCanvasRef}
className="absolute top-0 left-0"
style={{
width: `${imageWidth * scale}px`,
height: `${imageHeight * scale}px`
}}
/>
</div>
</div>
</div>
);
}
);
export default React.memo(ImageLayerViewer, isObjEqual);
// @import '../../../../global.scss';
.customStyle {
padding: 2rem;
padding-top: 0rem;
& > div {
max-width: 100%;
max-height: 100%;
// @include scrollBar(red);
}
.katex-display {
margin-top: 0px !important;
// @include scrollBar(red);
}
}
import React from 'react';
import 'katex/dist/katex.min.css';
import { BlockMath } from 'react-katex';
import style from './index.module.scss';
import classNames from 'classnames';
interface LatexRendererProps {
formula: string;
className?: string;
'aria-label'?: string;
title?: string;
}
function LatexRenderer({ formula, className = '', 'aria-label': ariaLabel, title }: LatexRendererProps) {
try {
return (
<div
className={`${className} max-w-[100%] max-h-[100%] scrollbar-thin ${style.customStyle}`}
aria-label={ariaLabel}
>
<BlockMath math={formula} className="scrollbar-thin" />
</div>
);
} catch (error) {
console.error('Error rendering Latex:', error);
return <div>Unable to render Latex formula.</div>;
}
}
export default LatexRenderer;
$circle-width: 16px;
.container {
position: relative;
width:4 * $circle-width;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.dot-pulse {
position: relative;
left: -9999px;
width: $circle-width;
height: $circle-width;
border-radius: 50%;
background-color: var(--color);
color: var(--color);
box-shadow: 9999px 0 0 -3px;
animation: dot-pulse 1.5s infinite linear;
animation-delay: 0.25s;
}
.dot-pulse::before,
.dot-pulse::after {
content: '';
display: inline-block;
position: absolute;
top: 0;
width: $circle-width;
height: $circle-width;
border-radius: 50%;
background-color: var(--color);
color: var(--color);
}
.dot-pulse::before {
box-shadow: 9974px 0 0 -3px;
animation: dot-pulse-before 1.5s infinite linear;
animation-delay: 0s;
}
.dot-pulse::after {
box-shadow: 10024px 0 0 -3px;
animation: dot-pulse-after 1.5s infinite linear;
animation-delay: 0.5s;
}
@keyframes dot-pulse-before {
0% {
box-shadow: 9974px 0 0 -3px;
}
30% {
box-shadow: 9974px 0 0 2px;
}
60%,
100% {
box-shadow: 9974px 0 0 -3px;
}
}
@keyframes dot-pulse {
0% {
box-shadow: 9999px 0 0 -3px;
}
30% {
box-shadow: 9999px 0 0 3px;
}
60%,
100% {
box-shadow: 9999px 0 0 -3px;
}
}
@keyframes dot-pulse-after {
0% {
box-shadow: 10024px 0 0 -3px;
}
30% {
box-shadow: 10024px 0 0 2px;
}
60%,
100% {
box-shadow: 10024px 0 0 -3px;
}
}
import classNames from "classnames";
import style from "./index.module.scss";
const LoadingIcon = ({
color,
className,
}: {
color: string;
className?: string;
}) => {
return (
<div className={classNames(style.container, className)}>
<div
className={style.dotPulse}
style={{ "--color": color || "grey" } as any}
></div>
</div>
);
};
export default LoadingIcon;
/*light*/
// 自定义滚动跳
.scrollBar {
// 火狐
scrollbar-color: #EBECF0 transparent;
scrollbar-width: thin;
// 定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸
&::-webkit-scrollbar {
width: 4px;
height: 6px;
background-color: transparent;
}
// 定义滚动条轨道 内阴影+圆角
&::-webkit-scrollbar-track {
background-color: #fff;
border-radius: 10px;
box-shadow: transparent;
}
// 定义滑块 内阴影+圆角
&::-webkit-scrollbar-thumb {
background: #EBECF0;
border-radius: 10px;
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.6);
}
}
.mdViewerWrap {
font-size: 10px;
}
.mdViewerWrap {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: #1f2328;
background-color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
scroll-behavior: auto;
}
.mdViewerWrap .octicon {
display: inline-block;
fill: currentColor;
vertical-align: text-bottom;
}
.mdViewerWrap h1:hover .anchor .octicon-link:before,
.mdViewerWrap h2:hover .anchor .octicon-link:before,
.mdViewerWrap h3:hover .anchor .octicon-link:before,
.mdViewerWrap h4:hover .anchor .octicon-link:before,
.mdViewerWrap h5:hover .anchor .octicon-link:before,
.mdViewerWrap h6:hover .anchor .octicon-link:before {
width: 16px;
height: 16px;
content: " ";
display: inline-block;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
}
.mdViewerWrap details,
.mdViewerWrap figcaption,
.mdViewerWrap figure {
display: block;
}
.mdViewerWrap summary {
display: list-item;
}
.mdViewerWrap [hidden] {
display: none !important;
}
.mdViewerWrap a {
background-color: transparent;
color: #0969da;
text-decoration: none;
}
.mdViewerWrap abbr[title] {
border-bottom: none;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
.mdViewerWrap b,
.mdViewerWrap strong {
font-weight: 600;
}
.mdViewerWrap dfn {
font-style: italic;
}
.mdViewerWrap h1 {
margin: 0.67em 0;
font-weight: 600;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid #d0d7deb3;
}
.mdViewerWrap mark {
background-color: #fff8c5;
color: #1f2328;
}
.mdViewerWrap small {
font-size: 90%;
}
.mdViewerWrap sub,
.mdViewerWrap sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.mdViewerWrap sub {
bottom: -0.25em;
}
.mdViewerWrap sup {
top: -0.5em;
}
.mdViewerWrap img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: #ffffff;
}
.mdViewerWrap code,
.mdViewerWrap kbd,
.mdViewerWrap pre,
.mdViewerWrap samp {
font-family: monospace;
font-size: 1em;
}
.mdViewerWrap figure {
margin: 1em 40px;
}
.mdViewerWrap hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid #d0d7deb3;
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #d0d7de;
border: 0;
}
.mdViewerWrap input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.mdViewerWrap [type="button"],
.mdViewerWrap [type="reset"],
.mdViewerWrap [type="submit"] {
-webkit-appearance: button;
appearance: button;
}
.mdViewerWrap [type="checkbox"],
.mdViewerWrap [type="radio"] {
box-sizing: border-box;
padding: 0;
}
.mdViewerWrap [type="number"]::-webkit-inner-spin-button,
.mdViewerWrap [type="number"]::-webkit-outer-spin-button {
height: auto;
}
.mdViewerWrap [type="search"]::-webkit-search-cancel-button,
.mdViewerWrap [type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
.mdViewerWrap ::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
.mdViewerWrap ::-webkit-file-upload-button {
-webkit-appearance: button;
appearance: button;
font: inherit;
}
.mdViewerWrap a:hover {
text-decoration: underline;
}
.mdViewerWrap ::placeholder {
color: #636c76;
opacity: 1;
}
.mdViewerWrap hr::before {
display: table;
content: "";
}
.mdViewerWrap hr::after {
display: table;
clear: both;
content: "";
}
.mdViewerWrap table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.mdViewerWrap td,
.mdViewerWrap th {
padding: 0;
}
.mdViewerWrap details summary {
cursor: pointer;
}
.mdViewerWrap details:not([open]) > *:not(summary) {
display: none;
}
.mdViewerWrap a:focus,
.mdViewerWrap [role="button"]:focus,
.mdViewerWrap input[type="radio"]:focus,
.mdViewerWrap input[type="checkbox"]:focus {
outline: 2px solid #0969da;
outline-offset: -2px;
box-shadow: none;
}
.mdViewerWrap a:focus:not(:focus-visible),
.mdViewerWrap [role="button"]:focus:not(:focus-visible),
.mdViewerWrap input[type="radio"]:focus:not(:focus-visible),
.mdViewerWrap input[type="checkbox"]:focus:not(:focus-visible) {
outline: solid 1px transparent;
}
.mdViewerWrap a:focus-visible,
.mdViewerWrap [role="button"]:focus-visible,
.mdViewerWrap input[type="radio"]:focus-visible,
.mdViewerWrap input[type="checkbox"]:focus-visible {
outline: 2px solid #0969da;
outline-offset: -2px;
box-shadow: none;
}
.mdViewerWrap a:not([class]):focus,
.mdViewerWrap a:not([class]):focus-visible,
.mdViewerWrap input[type="radio"]:focus,
.mdViewerWrap input[type="radio"]:focus-visible,
.mdViewerWrap input[type="checkbox"]:focus,
.mdViewerWrap input[type="checkbox"]:focus-visible {
outline-offset: 0;
}
.mdViewerWrap kbd {
display: inline-block;
padding: 3px 5px;
font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
line-height: 10px;
color: #1f2328;
vertical-align: middle;
background-color: #f6f8fa;
border: solid 1px #afb8c133;
border-bottom-color: #afb8c133;
border-radius: 6px;
box-shadow: inset 0 -1px 0 #afb8c133;
}
.mdViewerWrap h1,
.mdViewerWrap h2,
.mdViewerWrap h3,
.mdViewerWrap h4,
.mdViewerWrap h5,
.mdViewerWrap h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.mdViewerWrap h2 {
font-weight: 600;
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid #d0d7deb3;
}
.mdViewerWrap h3 {
font-weight: 600;
font-size: 1.25em;
}
.mdViewerWrap h4 {
font-weight: 600;
font-size: 1em;
}
.mdViewerWrap h5 {
font-weight: 600;
font-size: 0.875em;
}
.mdViewerWrap h6 {
font-weight: 600;
font-size: 0.85em;
color: #636c76;
}
.mdViewerWrap p {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.25em;
}
.mdViewerWrap blockquote {
margin: 0;
padding: 0 1em;
color: #636c76;
border-left: 0.25em solid #d0d7de;
}
.mdViewerWrap ul,
.mdViewerWrap ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.mdViewerWrap ol ol,
.mdViewerWrap ul ol {
list-style-type: lower-roman;
}
.mdViewerWrap ul ul ol,
.mdViewerWrap ul ol ol,
.mdViewerWrap ol ul ol,
.mdViewerWrap ol ol ol {
list-style-type: lower-alpha;
}
.mdViewerWrap dd {
margin-left: 0;
}
.mdViewerWrap tt,
.mdViewerWrap code,
.mdViewerWrap samp {
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
font-size: 12px;
}
.mdViewerWrap pre {
margin-top: 0;
margin-bottom: 0;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
font-size: 12px;
word-wrap: normal;
}
.mdViewerWrap .octicon {
display: inline-block;
overflow: visible !important;
vertical-align: text-bottom;
fill: currentColor;
}
.mdViewerWrap input::-webkit-outer-spin-button,
.mdViewerWrap input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.mdViewerWrap .mr-2 {
margin-right: 0.5rem !important;
}
.mdViewerWrap::before {
display: table;
content: "";
}
.mdViewerWrap::after {
display: table;
clear: both;
content: "";
}
.mdViewerWrap > *:first-child {
margin-top: 0 !important;
}
.mdViewerWrap > *:last-child {
margin-bottom: 0 !important;
}
.mdViewerWrap a:not([href]) {
color: inherit;
text-decoration: none;
}
.mdViewerWrap .absent {
color: #d1242f;
}
.mdViewerWrap .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.mdViewerWrap .anchor:focus {
outline: none;
}
.mdViewerWrap p,
.mdViewerWrap blockquote,
.mdViewerWrap ul,
.mdViewerWrap ol,
.mdViewerWrap dl,
.mdViewerWrap table,
.mdViewerWrap pre,
.mdViewerWrap details {
margin-top: 0;
margin-bottom: 16px;
}
.mdViewerWrap blockquote > :first-child {
margin-top: 0;
}
.mdViewerWrap blockquote > :last-child {
margin-bottom: 0;
}
.mdViewerWrap h1 .octicon-link,
.mdViewerWrap h2 .octicon-link,
.mdViewerWrap h3 .octicon-link,
.mdViewerWrap h4 .octicon-link,
.mdViewerWrap h5 .octicon-link,
.mdViewerWrap h6 .octicon-link {
color: #1f2328;
vertical-align: middle;
visibility: hidden;
}
.mdViewerWrap h1:hover .anchor,
.mdViewerWrap h2:hover .anchor,
.mdViewerWrap h3:hover .anchor,
.mdViewerWrap h4:hover .anchor,
.mdViewerWrap h5:hover .anchor,
.mdViewerWrap h6:hover .anchor {
text-decoration: none;
}
.mdViewerWrap h1:hover .anchor .octicon-link,
.mdViewerWrap h2:hover .anchor .octicon-link,
.mdViewerWrap h3:hover .anchor .octicon-link,
.mdViewerWrap h4:hover .anchor .octicon-link,
.mdViewerWrap h5:hover .anchor .octicon-link,
.mdViewerWrap h6:hover .anchor .octicon-link {
visibility: visible;
}
.mdViewerWrap h1 tt,
.mdViewerWrap h1 code,
.mdViewerWrap h2 tt,
.mdViewerWrap h2 code,
.mdViewerWrap h3 tt,
.mdViewerWrap h3 code,
.mdViewerWrap h4 tt,
.mdViewerWrap h4 code,
.mdViewerWrap h5 tt,
.mdViewerWrap h5 code,
.mdViewerWrap h6 tt,
.mdViewerWrap h6 code {
padding: 0 0.2em;
font-size: inherit;
}
.mdViewerWrap summary h1,
.mdViewerWrap summary h2,
.mdViewerWrap summary h3,
.mdViewerWrap summary h4,
.mdViewerWrap summary h5,
.mdViewerWrap summary h6 {
display: inline-block;
}
.mdViewerWrap summary h1 .anchor,
.mdViewerWrap summary h2 .anchor,
.mdViewerWrap summary h3 .anchor,
.mdViewerWrap summary h4 .anchor,
.mdViewerWrap summary h5 .anchor,
.mdViewerWrap summary h6 .anchor {
margin-left: -40px;
}
.mdViewerWrap summary h1,
.mdViewerWrap summary h2 {
padding-bottom: 0;
border-bottom: 0;
}
.mdViewerWrap ul.no-list,
.mdViewerWrap ol.no-list {
padding: 0;
list-style-type: none;
}
.mdViewerWrap ol[type="a s"] {
list-style-type: lower-alpha;
}
.mdViewerWrap ol[type="A s"] {
list-style-type: upper-alpha;
}
.mdViewerWrap ol[type="i s"] {
list-style-type: lower-roman;
}
.mdViewerWrap ol[type="I s"] {
list-style-type: upper-roman;
}
.mdViewerWrap ol[type="1"] {
list-style-type: decimal;
}
.mdViewerWrap div > ol:not([type]) {
list-style-type: decimal;
}
.mdViewerWrap ul ul,
.mdViewerWrap ul ol,
.mdViewerWrap ol ol,
.mdViewerWrap ol ul {
margin-top: 0;
margin-bottom: 0;
}
.mdViewerWrap li > p {
margin-top: 16px;
}
.mdViewerWrap li + li {
margin-top: 0.25em;
}
.mdViewerWrap dl {
padding: 0;
}
.mdViewerWrap dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
.mdViewerWrap dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.mdViewerWrap table th {
font-weight: 600;
}
.mdViewerWrap table th,
.mdViewerWrap table td {
padding: 6px 13px;
border: 1px solid #d0d7de;
}
.mdViewerWrap table td > :last-child {
margin-bottom: 0;
}
.mdViewerWrap table tr {
background-color: #ffffff;
border-top: 1px solid #d0d7deb3;
}
.mdViewerWrap table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.mdViewerWrap table img {
background-color: transparent;
}
.mdViewerWrap img[align="right"] {
padding-left: 20px;
}
.mdViewerWrap img[align="left"] {
padding-right: 20px;
}
.mdViewerWrap .emoji {
max-width: none;
vertical-align: text-top;
background-color: transparent;
}
.mdViewerWrap span.frame {
display: block;
overflow: hidden;
}
.mdViewerWrap span.frame > span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid #d0d7de;
}
.mdViewerWrap span.frame span img {
display: block;
float: left;
}
.mdViewerWrap span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: #1f2328;
}
.mdViewerWrap span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.mdViewerWrap span.align-center > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.mdViewerWrap span.align-center span img {
margin: 0 auto;
text-align: center;
}
.mdViewerWrap span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.mdViewerWrap span.align-right > span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.mdViewerWrap span.align-right span img {
margin: 0;
text-align: right;
}
.mdViewerWrap span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.mdViewerWrap span.float-left span {
margin: 13px 0 0;
}
.mdViewerWrap span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.mdViewerWrap span.float-right > span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.mdViewerWrap code,
.mdViewerWrap tt {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: #afb8c133;
border-radius: 6px;
}
.mdViewerWrap code br,
.mdViewerWrap tt br {
display: none;
}
.mdViewerWrap del code {
text-decoration: inherit;
}
.mdViewerWrap samp {
font-size: 85%;
}
.mdViewerWrap pre code {
font-size: 100%;
}
.mdViewerWrap pre > code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.mdViewerWrap .highlight {
margin-bottom: 16px;
}
.mdViewerWrap .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.mdViewerWrap .highlight pre,
.mdViewerWrap pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
color: #1f2328;
background-color: #f6f8fa;
border-radius: 6px;
}
.mdViewerWrap pre code,
.mdViewerWrap pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.mdViewerWrap .csv-data td,
.mdViewerWrap .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.mdViewerWrap .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: #ffffff;
border: 0;
}
.mdViewerWrap .csv-data tr {
border-top: 0;
}
.mdViewerWrap .csv-data th {
font-weight: 600;
background: #f6f8fa;
border-top: 0;
}
.mdViewerWrap [data-footnote-ref]::before {
content: "[";
}
.mdViewerWrap [data-footnote-ref]::after {
content: "]";
}
.mdViewerWrap .footnotes {
font-size: 12px;
color: #636c76;
border-top: 1px solid #d0d7de;
}
.mdViewerWrap .footnotes ol {
padding-left: 16px;
}
.mdViewerWrap .footnotes ol ul {
display: inline-block;
padding-left: 16px;
margin-top: 16px;
}
.mdViewerWrap .footnotes li {
position: relative;
}
.mdViewerWrap .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid #0969da;
border-radius: 6px;
}
.mdViewerWrap .footnotes li:target {
color: #1f2328;
}
.mdViewerWrap .footnotes .data-footnote-backref g-emoji {
font-family: monospace;
}
.mdViewerWrap .pl-c {
color: #57606a;
}
.mdViewerWrap .pl-c1,
.mdViewerWrap .pl-s .pl-v {
color: #0550ae;
}
.mdViewerWrap .pl-e,
.mdViewerWrap .pl-en {
color: #6639ba;
}
.mdViewerWrap .pl-smi,
.mdViewerWrap .pl-s .pl-s1 {
color: #24292f;
}
.mdViewerWrap .pl-ent {
color: #0550ae;
}
.mdViewerWrap .pl-k {
color: #cf222e;
}
.mdViewerWrap .pl-s,
.mdViewerWrap .pl-pds,
.mdViewerWrap .pl-s .pl-pse .pl-s1,
.mdViewerWrap .pl-sr,
.mdViewerWrap .pl-sr .pl-cce,
.mdViewerWrap .pl-sr .pl-sre,
.mdViewerWrap .pl-sr .pl-sra {
color: #0a3069;
}
.mdViewerWrap .pl-v,
.mdViewerWrap .pl-smw {
color: #953800;
}
.mdViewerWrap .pl-bu {
color: #82071e;
}
.mdViewerWrap .pl-ii {
color: #f6f8fa;
background-color: #82071e;
}
.mdViewerWrap .pl-c2 {
color: #f6f8fa;
background-color: #cf222e;
}
.mdViewerWrap .pl-sr .pl-cce {
font-weight: bold;
color: #116329;
}
.mdViewerWrap .pl-ml {
color: #3b2300;
}
.mdViewerWrap .pl-mh,
.mdViewerWrap .pl-mh .pl-en,
.mdViewerWrap .pl-ms {
font-weight: bold;
color: #0550ae;
}
.mdViewerWrap .pl-mi {
font-style: italic;
color: #24292f;
}
.mdViewerWrap .pl-mb {
font-weight: bold;
color: #24292f;
}
.mdViewerWrap .pl-md {
color: #82071e;
background-color: #ffebe9;
}
.mdViewerWrap .pl-mi1 {
color: #116329;
background-color: #dafbe1;
}
.mdViewerWrap .pl-mc {
color: #953800;
background-color: #ffd8b5;
}
.mdViewerWrap .pl-mi2 {
color: #eaeef2;
background-color: #0550ae;
}
.mdViewerWrap .pl-mdr {
font-weight: bold;
color: #8250df;
}
.mdViewerWrap .pl-ba {
color: #57606a;
}
.mdViewerWrap .pl-sg {
color: #8c959f;
}
.mdViewerWrap .pl-corl {
text-decoration: underline;
color: #0a3069;
}
.mdViewerWrap [role="button"]:focus:not(:focus-visible),
.mdViewerWrap [role="tabpanel"][tabindex="0"]:focus:not(:focus-visible),
.mdViewerWrap button:focus:not(:focus-visible),
.mdViewerWrap summary:focus:not(:focus-visible),
.mdViewerWrap a:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
}
.mdViewerWrap [tabindex="0"]:focus:not(:focus-visible),
.mdViewerWrap details-dialog:focus:not(:focus-visible) {
outline: none;
}
.mdViewerWrap g-emoji {
display: inline-block;
min-width: 1ch;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1em;
font-style: normal !important;
font-weight: 400;
line-height: 1;
vertical-align: -0.075em;
}
.mdViewerWrap g-emoji img {
width: 1em;
height: 1em;
}
.mdViewerWrap .task-list-item {
list-style-type: none;
}
.mdViewerWrap .task-list-item label {
font-weight: 400;
}
.mdViewerWrap .task-list-item.enabled label {
cursor: pointer;
}
.mdViewerWrap .task-list-item + .task-list-item {
margin-top: 0.25rem;
}
.mdViewerWrap .task-list-item .handle {
display: none;
}
.mdViewerWrap .task-list-item-checkbox {
margin: 0 0.2em 0.25em -1.4em;
vertical-align: middle;
}
.mdViewerWrap .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em 0.25em 0.2em;
}
.mdViewerWrap .contains-task-list {
position: relative;
}
.mdViewerWrap .contains-task-list:hover .task-list-item-convert-container,
.mdViewerWrap
.contains-task-list:focus-within
.task-list-item-convert-container {
display: block;
width: auto;
height: 24px;
overflow: visible;
clip: auto;
}
.mdViewerWrap ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}
.mdViewerWrap .markdown-alert {
padding: 0.5rem 1rem;
margin-bottom: 1rem;
color: inherit;
border-left: 0.25em solid #d0d7de;
}
.mdViewerWrap .markdown-alert > :first-child {
margin-top: 0;
}
.mdViewerWrap .markdown-alert > :last-child {
margin-bottom: 0;
}
.mdViewerWrap .markdown-alert .markdown-alert-title {
display: flex;
font-weight: 500;
align-items: center;
line-height: 1;
}
.mdViewerWrap .markdown-alert.markdown-alert-note {
border-left-color: #0969da;
}
.mdViewerWrap .markdown-alert.markdown-alert-note .markdown-alert-title {
color: #0969da;
}
.mdViewerWrap .markdown-alert.markdown-alert-important {
border-left-color: #8250df;
}
.mdViewerWrap .markdown-alert.markdown-alert-important .markdown-alert-title {
color: #8250df;
}
.mdViewerWrap .markdown-alert.markdown-alert-warning {
border-left-color: #bf8700;
}
.mdViewerWrap .markdown-alert.markdown-alert-warning .markdown-alert-title {
color: #9a6700;
}
.mdViewerWrap .markdown-alert.markdown-alert-tip {
border-left-color: #1a7f37;
}
.mdViewerWrap .markdown-alert.markdown-alert-tip .markdown-alert-title {
color: #1a7f37;
}
.mdViewerWrap .markdown-alert.markdown-alert-caution {
border-left-color: #cf222e;
}
.mdViewerWrap .markdown-alert.markdown-alert-caution .markdown-alert-title {
color: #d1242f;
}
.mdViewerWrap > *:first-child > .heading-element:first-child {
margin-top: 0 !important;
}
import { useEffect, useRef, useState } from "react";
import { Tooltip } from "antd";
import cls from "classnames";
import styles from "./index.module.scss";
import { useDeepCompareEffect, useHover } from "ahooks";
import IconFont from "@/components/icon-font";
import { downloadFileUseAScript } from "@/utils/download";
import { MD_DRIVE_PDF } from "@/constant/event";
import { useIntl } from "react-intl";
import LazyUrlMarkdown from "../url-markdown";
import exitFullScreenSvg from "@/assets/pdf/exitFullScreen.svg";
import fullScreenSvg from "@/assets/pdf/fullScreen.svg";
import { MD_PREVIEW_TYPE } from "@/types/extract-task-type";
import _ from "lodash";
import { TaskIdResItem } from "@/api/extract";
import useMdStore from "@/store/mdStore";
import CodeMirror from "@/components/code-mirror";
import { useParams } from "react-router-dom";
interface IMdViewerProps {
md?: string;
className?: string;
filename?: string;
url?: string;
taskInfo: TaskIdResItem;
curPage: number;
fullScreen?: boolean;
setFullScreen?: (value?: boolean) => void;
}
const MdViewer: React.FC<IMdViewerProps> = ({
fullScreen,
setFullScreen,
taskInfo,
className = "",
curPage,
}) => {
const mdViewerPef = useRef<HTMLDivElement>(null);
const url = taskInfo?.fullMdLink || "";
const containerRef = useRef<HTMLDivElement>(null);
const isHovering = useHover(containerRef);
const { formatMessage } = useIntl();
const [displayType, setDisplayType] = useState(MD_PREVIEW_TYPE.preview);
const params = useParams();
const {
setAllMdContentWithAnchor,
allMdContentWithAnchor,
setMdUrlArr,
mdContents,
} = useMdStore();
const [lineWrap, setLineWrap] = useState(false);
const threshold = 562 - 427;
const menuList = [
{
name: formatMessage({ id: "extractor.markdown.preview" }),
code: MD_PREVIEW_TYPE.preview,
},
{
name: formatMessage({ id: "extractor.markdown.code" }),
code: MD_PREVIEW_TYPE.code,
},
];
const getVisibleFromType = (str: string, type: string) => {
return str === type
? "relative w-full h-full"
: "w-0 h-0 overflow-hidden hidden";
};
const pushMdViewerScroll = (scrollType?: "instant" | "smooth") => {
const container = document.getElementById(`md-container`);
// md渲染的时候用一个元素包括anchor
const element =
displayType === MD_PREVIEW_TYPE.preview
? document.getElementById(`md-anchor-${curPage - 1}`)?.parentElement
: document.getElementById(`code-${curPage - 1}`);
if (element && container) {
container.scrollTo({
top: element.offsetTop - 124,
behavior: scrollType || "smooth",
});
}
};
useEffect(() => {
if (isHovering) return;
pushMdViewerScroll();
}, [curPage, isHovering]);
useEffect(() => {
pushMdViewerScroll("instant");
}, [displayType]);
useEffect(() => {
if (!isHovering) return;
const handleScroll = () => {
if (!containerRef.current) return;
taskInfo?.markdownUrl?.forEach((page, index) => {
const element =
displayType === MD_PREVIEW_TYPE.preview
? document.getElementById(`md-anchor-${index}`)?.parentElement
: document.getElementById(`code-${index}`);
if (element) {
const rect = element.getBoundingClientRect();
if (rect.top <= threshold) {
document.dispatchEvent(
new CustomEvent(MD_DRIVE_PDF, {
detail: index,
})
);
}
}
});
};
const container = containerRef.current;
if (container) {
container.addEventListener("scroll", handleScroll);
}
return () => {
if (container) {
container?.removeEventListener("scroll", handleScroll);
}
};
}, [taskInfo, isHovering, displayType]);
useDeepCompareEffect(() => {
if (taskInfo?.markdownUrl) {
setMdUrlArr(taskInfo?.markdownUrl);
}
}, [taskInfo?.markdownUrl, params?.jobID]);
const handleContentChange = (val: string) => {
setAllMdContentWithAnchor(val);
};
return (
<div className={cls(className)} ref={mdViewerPef}>
<div
className={cls(
"h-[49px] px-6 border-0 border-solid border-b-[1px] border-[#EBECF0] w-full pl-[24px] flex justify-between items-center"
)}
>
<ul className="p-1 list-none mb-0 inline-block rounded-sm mr-auto bg-[#F4F5F9] select-none">
{menuList.map((item) => (
<li
key={item.code}
className={`mx-[0.125rem] px-2 leading-[25px] inline-block rounded-sm text-[14px] cursor-pointer text-color ${
displayType === item.code && "bg-white text-primary"
}`}
onClick={() => setDisplayType(item.code)}
>
{item.name}
</li>
))}
</ul>
{displayType === "code" && (
<>
<Tooltip
title={
fullScreen
? formatMessage({ id: "extractor.button.exitFullScreen" })
: formatMessage({
id: "extractor.button.lineWrap",
})
}
>
<IconFont
type="icon-line-wrap"
className={cls(
"text-lg text-[#464a53] leading-0 ml-[1rem] cursor-pointer hover:bg-[#F4F5F9] p-1 rounded",
lineWrap && "!text-[#0D53DE]"
)}
onClick={() => setLineWrap?.(!lineWrap)}
/>
</Tooltip>
<span className="w-[1px] h-[0.75rem] bg-[#D7D8DD] mx-[1rem]"></span>
</>
)}
<Tooltip
title={
fullScreen
? formatMessage({ id: "extractor.button.exitFullScreen" })
: formatMessage({
id: "extractor.button.fullScreen",
})
}
>
<span
className="cursor-pointer w-[1.5rem] user-select-none flex items-center justify-center h-[1.5rem] hover:bg-[#F4F5F9] rounded "
onClick={() => setFullScreen?.(!fullScreen)}
>
{!fullScreen ? (
<img
className=" w-[1.125rem] h-[1.125rem] "
src={fullScreenSvg}
/>
) : (
<img
className=" w-[1.125rem] h-[1.125rem] "
src={exitFullScreenSvg}
/>
)}
</span>
</Tooltip>
<span className="w-[1px] h-[0.75rem] bg-[#D7D8DD] ml-[1rem]"></span>
<Tooltip title={formatMessage({ id: "extractor.button.download" })}>
<IconFont
type="icon-xiazai"
className="text-lg text-[#464a53] leading-0 ml-[1rem] cursor-pointer hover:bg-[#F4F5F9] p-1 rounded"
onClick={() =>
downloadFileUseAScript(
url,
`${_(taskInfo?.fileName).split(".").slice(0, -1).join(".")}.md`
)
}
/>
</Tooltip>
</div>
<div
className={cls(
"bg-white !h-[calc(100%-60px)] px-6 py-8 overflow-auto w-full max-w-[100%]",
styles.scrollBar
)}
id="md-container"
ref={containerRef}
>
<div
className={cls(
getVisibleFromType(displayType, MD_PREVIEW_TYPE.preview)
)}
>
<LazyUrlMarkdown
markdownClass={"relative"}
content={allMdContentWithAnchor}
/>
</div>
<div
className={cls(getVisibleFromType(displayType, MD_PREVIEW_TYPE.code))}
>
{taskInfo?.markdownUrl?.map((url: string, index: number) => {
const md = mdContents[url]?.content || "";
if (!md) return null;
return (
<div key={url} id={`code-${index}`} className="opacity-1 z-[-1]">
<CodeMirror
value={md}
lineWrapping={lineWrap}
onChange={handleContentChange}
editable
className="w-full h-full"
/>
</div>
);
})}
</div>
</div>
</div>
);
};
export default MdViewer;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment