"test/vscode:/vscode.git/clone" did not exist on "9a7bb6d233a13be69b1db43adc6a6be9aac7cd4d"
Commit dff11700 authored by myhloli's avatar myhloli
Browse files

feat: update project list in README files to reflect compatibility with version 2.0

parent d41179da
import ReactMarkdown from "react-markdown";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import remarkGfm from "remark-gfm";
import styles from "./index.module.scss";
import { useRef } from "react";
import cls from "classnames";
interface IMarkdownProps {
content?: string;
markdownClass?: string;
markdownId?: string;
}
const LazyUrlMarkdown: React.FC<IMarkdownProps> = ({
content,
markdownClass = "",
}) => {
const ref = useRef<HTMLDivElement>(null);
return (
<div ref={ref} className="min-h-[100px]">
<div className={styles.mdViewerWrap}>
<ReactMarkdown
className={cls("bg-white text-[0.75rem]", markdownClass)}
remarkPlugins={[
remarkMath,
[remarkGfm, { singleTilde: false }, { strict: "ignore" }],
]}
rehypePlugins={[[rehypeKatex, { strict: "ignore" }], rehypeRaw]}
components={{
code(props) {
const { children, className, node, ...rest } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
PreTag="div"
className="rounded-md"
// eslint-disable-next-line react/no-children-prop
children={String(children).replace(/\n$/, "")}
language={match[1]}
/>
) : (
<code
{...rest}
className="p-4 my-2 bg-[#f6f8fa] !bg-black rounded-md block"
>
{children}
</code>
);
},
}}
>
{content}
</ReactMarkdown>
</div>
</div>
);
};
export default LazyUrlMarkdown;
.item {
border-radius: 12px;
border: 1px solid rgba(198, 217, 255, 0.20);
background: linear-gradient(155deg, rgba(92, 147, 255, 0.10) -130.23%, rgba(255, 255, 255, 0.00) 83.57%);
filter: blur(0px);
padding: 42px 20px;
cursor: pointer;
position: relative;
&:hover {
border-radius: 12px;
border: 1px solid rgba(198, 217, 255, 0.20);
background: linear-gradient(155deg, rgba(92, 147, 255, 0.20) -83.23%, rgba(255, 255, 255, 0.00) 83.57%);
box-shadow: 0px 8px 26px 0px rgba(0, 0, 0, 0.12);
}
}
.itemComingSoon_zh-CN {
&:hover {
backdrop-filter: blur(-100px);
& > .itemContent {
opacity: 0;
z-index: 0;
}
&::before {
display: inline-block;
content: '敬请期待';
width: 100%;
height: 100%;
line-height: 184px;
position: absolute;
background-image: url('@/assets/pdf/comingSoonLayer.svg');
background-color: rgba(255, 255, 255, 0.8);
background-size: contain;
top: 0;
left: 0;
border-radius: 12px;
// background:white;
-webkit-backdrop-filter: blur(2030px) brightness(110%);
backdrop-filter: blur(2030px) brightness(110%);
color: var(--60-text-3, rgba(18, 19, 22, 0.60));
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
z-index: 1;
}
}
}
.itemComingSoon_en-US {
&:hover {
backdrop-filter: blur(-100px);
& > .itemContent {
opacity: 0;
z-index: 0;
}
&::before {
display: inline-block;
content: 'Stay Tuned';
width: 100%;
height: 100%;
line-height: 224px;
position: absolute;
background-image: url('@/assets/pdf/comingSoonLayer.svg');
background-color: rgba(255, 255, 255, 0.8);
background-size: contain;
top: 0;
left: 0;
border-radius: 12px;
// background:white;
-webkit-backdrop-filter: blur(2030px) brightness(110%);
backdrop-filter: blur(2030px) brightness(110%);
color: var(--60-text-3, rgba(18, 19, 22, 0.60));
text-align: center;
font-feature-settings: 'liga' off, 'clig' off;
z-index: 1;
}
}
}
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 style from "./index.module.scss";
import cls from "classnames";
import { EXTRACTOR_TYPE_LIST } from "@/types/extract-task-type";
import { useNavigate } from "react-router-dom";
import { useIntl } from "react-intl";
const ITEM_LIST = [
{
id: 1,
icon: extractorPdfIcon,
[`zh-CN-title`]: "PDF文档提取",
[`en-US-title`]: "PDF Document Extraction",
type: EXTRACTOR_TYPE_LIST.pdf,
[`zh-CN-desc`]:
"支持文本/扫描型 pdf 解析,识别各类版面元素并转换为多模态 Markdown 格式",
[`en-US-desc`]:
"Supports text/scanned PDF parsing, identifies various layout elements and converts them into multimodal Markdown format",
},
{
id: 2,
icon: extractorFormulaIcon,
[`zh-CN-title`]: "公式检测与识别",
[`en-US-title`]: "Formula Detection and Recognition",
type: EXTRACTOR_TYPE_LIST.formula,
[`zh-CN-desc`]:
"对行内、行间公式进行检测,对数学公式进行识别并转换为 Latex 格式",
[`en-US-desc`]:
"Detect formulas within and between lines, identify mathematical formulas and convert them into Latex format",
},
{
id: 3,
icon: extractorTableIcon,
[`zh-CN-title`]: "表格识别",
[`en-US-title`]: "Table recognition",
type: EXTRACTOR_TYPE_LIST.table,
[`zh-CN-desc`]: "对表格进行检测并转换为 Markdown 格式",
[`en-US-desc`]: "Detect and convert tables to Markdown format",
comingSoon: true,
},
] as Record<string, any>;
const ExtractorHome = () => {
const navigate = useNavigate();
const { formatMessage, locale } = useIntl();
const defaultLocale = "zh-CN";
return (
<div className="flex flex-col items-center justify-center h-full">
<div className="mb-4 font-semibold text-base !text-[2rem] leading-[3rem]">
{formatMessage({ id: "extractor.home.title" })}
</div>
<div className="mb-12 text-[1.25rem] leading-[2rem]">
{formatMessage({ id: "extractor.home.subTitle" })}
</div>
<div className="flex mx-[5.25rem]">
{ITEM_LIST?.map((i: Record<string, any>) => {
return (
<div
className={cls(
style.item,
i?.comingSoon &&
style?.[`itemComingSoon_${locale || defaultLocale}`],
"mx-4 basis-4/12"
)}
key={i.desc}
onClick={() => {
if (i?.comingSoon) return;
navigate(`/OpenSourceTools/Extractor/${i?.type}`);
}}
>
<div className={style.itemContent}>
<div className="text-center leading-[1.5rem] flex items-center justify-center">
<img
src={i.icon}
alt={""}
className="w-[2.25rem] h-[2.25rem]"
/>
<span className="text-[#121316]/[0.8] ml-2 text-[20px] font-semibold">
{i?.[`${locale}-title`]}
</span>
</div>
<div className="text-center text-[#121316]/[0.6] mt-4 text-[14px] leading-[1.5rem]">
{i?.[`${locale}-desc`]}
</div>
</div>
</div>
);
})}
</div>
<div className="absolute bottom-[1.5rem] text-[13px] text-[#121316]/[0.35] text-center leading-[20px] max-w-[64rem]">
{formatMessage({
id: "extractor.law",
})}
</div>
</div>
);
};
export default ExtractorHome;
import showLayerIcon from "@/assets/pdf/extractor-show-layer.svg";
import hiddenLayerIcon from "@/assets/pdf/extractor-hidden-layer.svg";
import { useRef, useState } from "react";
import IconFont from "@/components/icon-font";
import ImageLayerViewer, {
ImageLayerViewerRef,
} from "../../components/image-layer-viwer";
import { useUpdate } from "ahooks";
import { TaskIdProgress, TaskIdResItem } from "@/api/extract";
import { Tooltip } from "antd";
import { useIntl } from "react-intl";
interface IFormulaDetailLeftProps {
taskInfo: TaskIdProgress & TaskIdResItem;
}
const FormulaDetailLeft = ({ taskInfo }: IFormulaDetailLeftProps) => {
const imageRef = useRef<ImageLayerViewerRef>(null);
const { formatMessage } = useIntl();
const [layerVisible, setLayerVisible] = useState(true);
const update = useUpdate();
if (!taskInfo?.fileInfo?.height || !taskInfo?.fileInfo?.width) {
console.info(
"formula extractor interface error: the picture size is invalid"
);
}
return (
<div className="w-full h-full">
<div
className={`flex border-solid border-0 !border-b-[1px] border-[#EBECF0] items-center px-4 h-[48px]`}
>
<Tooltip
title={
<>
{layerVisible
? formatMessage({
id: "extractor.button.hiddenLayer",
})
: formatMessage({
id: "extractor.button.showLayer",
})}
</>
}
>
<span
className="ml-auto mr-2 cursor-pointer hover:bg-[#f4f5f9] w-6 text-center inline-block rounded leading-normal"
onClick={() => setLayerVisible(!layerVisible)}
>
{taskInfo?.type === "formula-detect" ? null : layerVisible ? (
<img src={hiddenLayerIcon} alt="Hide Layer" />
) : (
<img src={showLayerIcon} alt="Show Layer" />
)}
</span>
</Tooltip>
{taskInfo?.type === "formula-detect" ? null : (
<span className="w-[1px] leading-normal h-[12px] bg-[#D7D8DD] mr-1"></span>
)}
<div className="select-none w-[7.8rem] flex justify-center">
<IconFont
className="rounded mx-2 cursor-pointer hover:bg-[#F4F5F9]"
type="icon-SubtractOutlined"
onClick={() => {
imageRef?.current?.zoomOut();
}}
/>
<span className="mx-2">
{((imageRef?.current?.scale || 0) * 100 || 1).toFixed(0)}%
</span>
<IconFont
className="rounded mx-2 cursor-pointer hover:bg-[#F4F5F9]"
type="icon-PlusOutlined"
onClick={() => {
imageRef?.current?.zoomIn();
}}
/>
</div>
</div>
<ImageLayerViewer
imageHeight={taskInfo?.fileInfo?.height || 0}
imageWidth={taskInfo?.fileInfo?.width || 0}
layout={taskInfo.content as any[]}
ref={imageRef}
onChange={() => {
// imageRef?.current?.scale为了这个更新
update();
}}
className={"!h-[calc(100%-48px)]"}
layerVisible={
taskInfo?.type === "formula-detect" ? false : layerVisible
}
imageUrl={taskInfo?.url}
/>
</div>
);
};
export default FormulaDetailLeft;
import ImageLayerViewer from "../../components/image-layer-viwer";
import exitFullScreenSvg from "@/assets/pdf/exitFullScreen.svg";
import fullScreenSvg from "@/assets/pdf/fullScreen.svg";
import { Tooltip } from "antd";
import { useIntl } from "react-intl";
import { TaskIdProgress, TaskIdResItem } from "@/api/extract";
import IconFont from "@/components/icon-font";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { useEffect, useMemo, useRef, useState } from "react";
import { message } from "antd";
import { MD_PREVIEW_TYPE } from "@/types/extract-task-type";
import CodeMirror from "@/components/code-mirror";
import LatexRenderer from "../../components/latex-renderer";
import { useParams } from "react-router-dom";
interface IImageOriginViewerProps {
fullScreen?: boolean;
setFullScreen?: (val: boolean) => void;
taskInfo: TaskIdProgress & TaskIdResItem;
}
const FormulaDetailRight = ({
fullScreen,
setFullScreen,
taskInfo,
}: IImageOriginViewerProps) => {
const CONTROL_BAR_HEIGHT = 48;
const { formatMessage } = useIntl();
const [displayType, setDisplayType] = useState(MD_PREVIEW_TYPE.preview);
const imageViewerRef = useRef<any>();
const formulaType = taskInfo?.type;
const params = useParams();
const jobID = params?.jobID;
const formulaLateX = useMemo(() => {
return taskInfo?.content?.map((i: any) => i?.latex + "\\\\\n").join("");
}, [taskInfo?.content]);
const handleCopy = () => {};
const menuList = [
{
name: formatMessage({ id: "extractor.markdown.preview" }),
code: MD_PREVIEW_TYPE.preview,
},
{
name: formatMessage({ id: "extractor.markdown.code" }),
code: MD_PREVIEW_TYPE.code,
},
];
useEffect(() => {
imageViewerRef?.current?.updateScaleAndPosition();
}, [fullScreen]);
useEffect(() => {
setDisplayType(MD_PREVIEW_TYPE.preview);
}, [jobID]);
return (
<div className="w-full h-full">
<header
className={`flex border-solid border-0 !border-b-[1px] border-[#EBECF0] px-4 items-center h-[${CONTROL_BAR_HEIGHT}px]`}
>
{formulaType === "formula-extract" && (
<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>
)}
<Tooltip
title={
fullScreen
? formatMessage({ id: "extractor.button.exitFullScreen" })
: formatMessage({
id: "extractor.button.fullScreen",
})
}
>
<span
className="cursor-pointer ml-auto w-[1.5rem] 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>
{formulaType === "formula-extract" && (
<div className="flex items-center">
<span className="w-[1px] h-[0.75rem] bg-[#D7D8DD] mx-[1rem]"></span>
<Tooltip title={formatMessage({ id: "common.copy" })}>
<CopyToClipboard
text={formulaLateX}
onCopy={() => {
message.success(formatMessage({ id: "common.copySuccess" }));
}}
>
<span>
<IconFont
type="icon-copy"
className="text-[#464a53] !text-[1.32rem] leading-0 cursor-pointer hover:bg-[#F4F5F9] rounded"
onClick={() => handleCopy()}
/>
</span>
</CopyToClipboard>
</Tooltip>
</div>
)}
</header>
{displayType === MD_PREVIEW_TYPE.preview ? (
formulaType === "formula-extract" ? (
<div className="w-full h-[calc(100%-48px)] flex items-center justify-center scrollbar-thin-layer overflow-auto">
<LatexRenderer
formula={formulaLateX}
className="text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl"
/>
</div>
) : (
<ImageLayerViewer
imageHeight={taskInfo?.fileInfo?.height || 0}
imageWidth={taskInfo?.fileInfo?.width || 0}
layout={
taskInfo?.type === "formula-extract"
? []
: (taskInfo.content as any[])
}
className={"!h-[calc(100%-48px)]"}
imageUrl={taskInfo?.url}
ref={imageViewerRef}
/>
)
) : (
<div
className={
"!h-[calc(100%-48px)] flex items-center justify-center w-full px-4 scroll-thin overflow-auto"
}
>
<CodeMirror className="w-full" value={formulaLateX} />
</div>
)}
</div>
);
};
export default FormulaDetailRight;
import cls from "classnames";
import { useEffect, useState } from "react";
import LoadingIcon from "../../components/loading-icon";
import { SubmitRes } from "@/api/extract";
import emptySvg from "@/assets/svg/empty.svg";
import { FormattedMessage } from "react-intl";
import FormulaDetailLeft from "../formula-detail-left";
import FormulaDetailRight from "../formula-detail-right";
import { useIntl } from "react-intl";
import { ExtractorUploadButton } from "../../components/pdf-upload-button";
import useExtractorJobProgress from "@/store/jobProgress";
interface IPdfExtractionProps {
setUploadShow: (bool: boolean) => void;
className?: string;
}
const FormulaDetail = ({ className = "" }: IPdfExtractionProps) => {
const {
taskInfo,
queueLoading,
interfaceError: compileError,
refreshQueue,
jobID,
} = useExtractorJobProgress();
const [fullScreen, setFullScreen] = useState<boolean>(false);
const { formatMessage } = useIntl();
const isQueueAndExtract = queueLoading;
const hiddenQueuePage = !isQueueAndExtract ? "opacity-0 " : "";
const hiddenResultPage = isQueueAndExtract ? "z-[-1] opacity-0" : "";
const getLayoutClassName = (_fullScreen?: boolean) => {
return {
left: _fullScreen ? "w-0 overflow-hidden" : "w-[50%] max-w-[50%]",
right: _fullScreen ? "w-full " : "w-[50%] max-w-[50%]",
};
};
const afterUploadSuccess = (data: SubmitRes) => {
refreshQueue();
};
const afterAsyncCheck = () => {
return Promise.resolve(true);
};
useEffect(() => {
setFullScreen(false);
}, [jobID]);
return (
<>
<div
className={cls(
"flex flex-col justify-center items-center h-[60px] w-[300px] bg-white h-full w-full absolute top-0 left-0",
hiddenQueuePage
)}
>
<LoadingIcon className="w-12" color={"#0D53DE"} />
<div className="text-base text-[#121316]/[0.8] mt-4">
{taskInfo?.rank > 1 ? (
<FormattedMessage
id="extractor.common.extracting.queue"
values={{
id: taskInfo?.rank || 0,
}}
/>
) : taskInfo.state === "done" || taskInfo?.state === "unknown" ? (
formatMessage({
id: "extractor.common.loading",
})
) : (
formatMessage({
id: "extractor.common.extracting",
})
)}
</div>
</div>
<div
className={cls("h-full w-full relative", className, hiddenResultPage)}
>
{!compileError ? (
<div className="w-full flex h-full">
<div className={cls("h-full", getLayoutClassName(fullScreen).left)}>
<FormulaDetailLeft taskInfo={taskInfo} />
</div>
<div
className={cls(
"!overflow-auto",
getLayoutClassName(fullScreen).right
)}
style={{
borderLeft: "1px solid #EBECF0",
}}
>
<FormulaDetailRight
fullScreen={fullScreen}
setFullScreen={setFullScreen}
taskInfo={taskInfo}
/>
</div>
</div>
) : (
<div className="ml-[50%] translate-x-[-50%] !h-[calc(100%-70px)] flex-1 flex items-center h-[110px] flex-col justify-center">
<img src={emptySvg} alt="emptySvg" />
<span className="text-[#121316]/[0.8] mt-2">
{formatMessage({
id: "extractor.failed",
})}
</span>
<ExtractorUploadButton
className="!mb-0 !w-[120px] !m-6"
accept="image/png, image/jpg, .png ,.jpg"
afterUploadSuccess={afterUploadSuccess}
taskType="extract"
afterAsyncCheck={afterAsyncCheck}
extractType={taskInfo?.type}
submitType="reUpload"
showIcon={false}
text={
<span className="text-white">
{formatMessage({
id: "extractor.button.reUpload",
})}
</span>
}
/>
</div>
)}
</div>
</>
);
};
export default FormulaDetail;
.formulaPopover {
: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 React, { ReactNode } from "react";
import { Popover } from "antd";
import IconFont from "@/components/icon-font";
import { useIntl } from "react-intl";
import style from "./index.module.scss";
interface IFormulaPopoverProps {
type: string;
text?: string | ReactNode;
}
const FormulaPopover = ({ type, text }: IFormulaPopoverProps) => {
const { formatMessage } = useIntl();
const content = (
<div className="flex flex-col w-[20rem] items-center">
{/* 顺序反了 */}
{formatMessage({
id:
type === "detect"
? "extractor.formula.popover.extract"
: "extractor.formula.popover.detect",
})}
<img
className="w-full mt-4"
src={
type === "extract"
? "https://static.openxlab.org.cn/opendatalab/assets/pdf/svg/extract-formula-extract.svg"
: "https://static.openxlab.org.cn/opendatalab/assets/pdf/svg/extract-formula-detect.svg"
}
alt="formula-popover"
/>
</div>
);
return (
<span className={""}>
<Popover
content={content}
placement="right"
showArrow={false}
overlayClassName={style.formulaPopover}
>
<span className="group inline-flex items-center">
{text}
<IconFont
type="icon-QuestionCircleOutlined"
className="text-[#121316]/[0.6] text-[15px] mt-[2px] leading-[1rem] group-hover:text-[#0D53DE]"
/>
</span>
</Popover>
</span>
);
};
export default FormulaPopover;
.uploadText {
font-feature-settings: 'liga' off, 'clig' off;
font-family: "PingFang SC";
font-size: 18px;
font-style: normal;
font-weight: 600;
line-height: 24px; /* 133.333% */
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;
}
.uploadDescText {
font-size: 13px;
line-height: 20px;
font-weight: 400;
background: linear-gradient(107deg, rgba(18,19,22,0.6) -24.14%, rgba(18,19,22,0.6) 100.09% );
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 1rem;
margin-top: 0.5rem;
}
.linearText {
font-size: 13px;
line-height: 20px;
font-weight: 400;
background: linear-gradient(107deg, rgba(18,19,22,0.6) -24.14%, rgba(18,19,22,0.6) 100.09% );
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
&-item {
font-weight: 400;
font-size: 13px;
line-height: 20px;
margin-right: 1rem;
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;
&:hover {
background: #3477EB;
background: linear-gradient(107deg, #3477EB -24.14%, #3477EB 100.09% );
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
}
.uploadSection {
border-radius: 12px;
border: 1px dashed var(---Brand1-6, #0D53DE);
background: linear-gradient(180deg, rgba(92, 147, 255, 0.10) -130.23%, rgba(255, 255, 255, 1) 83.57%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
filter: blur(0px);
height: 280px !important;
width: 600px !important;
&:hover {
background: linear-gradient(180deg, rgb(245, 248, 255) -130.23%, rgb(245, 248, 255) 83.57%);
}
}
.textBtn {
background-image: none !important;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background: linear-gradient(111deg, #0D53DE -21.44%, #5246FF 102%) !important;
background-clip: text !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
height: 1.5rem !important;
font-weight: 600;
height: 280px !important;
width: 600px !important;
overflow: hidden;
}
import { useIntl } from "react-intl";
import IconFont from "@/components/icon-font";
import { useState } from "react";
import cls from "classnames";
import { ExtractorUploadButton } from "../../components/pdf-upload-button";
import UploadBg from "@/assets/imgs/online.experience/file-upload-bg.svg";
import style from "./index.module.scss";
import { SubmitRes } from "@/api/extract";
import { ADD_TASK_LIST } from "@/constant/event";
import { FORMULA_TYPE } from "@/types/extract-task-type";
import { useNavigate } from "react-router-dom";
const FORMULA_ITEM_LIST = [
{
type: FORMULA_TYPE.detect,
[`zh-CN-name`]: "公式检测",
[`en-US-name`]: "Formula Detection",
},
{
type: FORMULA_TYPE.extract,
[`zh-CN-name`]: "公式识别",
[`en-US-name`]: "Formula Recognition",
},
];
const FormulaUpload = () => {
const navigate = useNavigate();
const { formatMessage, locale } = useIntl();
const [formulaType, setFormulaType] = useState(FORMULA_TYPE.detect);
const afterUploadSuccess = (data: SubmitRes) => {
navigate(`/OpenSourceTools/Extractor/formula/${data?.id}`);
setTimeout(() => {
document.dispatchEvent(
new CustomEvent(ADD_TASK_LIST, {
detail: data,
})
);
}, 10);
};
const afterAsyncCheck = () => {
return Promise.resolve(true);
};
return (
<div className="relative w-full h-full flex flex-col items-center justify-center ">
<div
className="absolute top-10 left-8 hover:!text-[#0D53DE] cursor-pointer"
onClick={() => navigate("/OpenSourceTools/Extractor")}
>
<IconFont type="icon-fanhui" className="mr-2" />
<span>{formatMessage({ id: "extractor.home" })}</span>
</div>
<div className="translate-y-[-60px] flex flex-col items-center ">
<div className="mb-[2.25rem]">
{FORMULA_ITEM_LIST.map((i) => {
return (
<span
key={i.type}
onClick={() => setFormulaType(i?.type)}
className={cls(
"relative text-[1.5rem] text-[#121316] cursor-pointer mx-[1.5rem]",
formulaType === i?.type && "!text-[#0D53DE] font-semibold"
)}
>
{i?.[`${locale || "zh-CN"}-name` as "en-US-name"]}
{formulaType === i?.type && (
<span className="absolute bottom-[-0.75rem] right-[50%] translate-x-[50%] w-[3rem] bg-[#0D53DE] rounded-[2px] h-[0.25rem]"></span>
)}
</span>
);
})}
</div>
<div className="text-[1.25rem] text-[#121316]/[0.8] mb-[3rem] text-center w-max-[50rem]">
{formatMessage({
id:
formulaType === "extract"
? "extractor.formula.title2"
: "extractor.formula.title",
})}
</div>
<ExtractorUploadButton
accept="image/png, image/jpg, .png ,.jpg"
afterUploadSuccess={afterUploadSuccess}
taskType="extract"
afterAsyncCheck={afterAsyncCheck}
extractType={
formulaType === FORMULA_TYPE.extract
? "formula-extract"
: "formula-detect"
}
className={style.textBtn}
showIcon={false}
text={
<div
className={cls(
style.uploadSection,
"border-[1px] border-dashed border-[#0D53DE] rounded-xl flex flex-col items-center justify-center"
)}
>
<img src={UploadBg} className="mb-4" />
<span
className={cls(style.uploadText, "text-[18px] leading-[20px]")}
>
{formatMessage({ id: "extractor.formula.upload.text" })}
</span>
<span className={cls(style.uploadDescText)}>
{formatMessage({ id: "extractor.formula.upload.accept" })}
</span>
<div>
<span className={cls(style.linearText, "cursor-pointer")}>
{formatMessage({
id: "extractor.formula.upload.try",
})}
</span>
</div>
</div>
}
></ExtractorUploadButton>
</div>
<div className="absolute bottom-[1.5rem] text-[13px] text-[#121316]/[0.35] text-center leading-[20px] max-w-[64rem]">
{formatMessage({
id: "extractor.law",
})}
</div>
</div>
);
};
export default FormulaUpload;
import { Outlet } from "react-router-dom";
const Formula = () => {
return (
<div className="relative w-full h-full flex flex-col items-center justify-center ">
<Outlet />
</div>
);
};
export default Formula;
const ExtractorTable = () => {
return <>ExtractorTable</>;
};
export default ExtractorTable;
const TableDetail = () => {
return <>TableDetail</>;
};
export default TableDetail;
"use client";
import ErrorBoundary from "@/components/error-boundary";
import styles from "./home.module.scss";
import { SlotID, Path } from "@/constant/route";
import {
BrowserRouter,
Routes,
Route,
Outlet,
Navigate,
useLocation,
HashRouter,
} from "react-router-dom";
import { ExtractorSide } from "./extract-side";
import { LanguageProvider } from "@/context/language-provider";
import PDFUpload from "@/pages/extract/components/pdf-upload";
import PDFExtractionJob from "@/pages/extract/components/pdf-extraction";
export function WindowContent() {
const location = useLocation();
const isHome = location.pathname === Path.Home;
return (
<>
<ExtractorSide className={isHome ? styles["sidebar-show"] : ""} />
<div className="flex-1">
<Outlet />
</div>
</>
);
}
function Screen() {
const renderContent = () => {
return (
<div className="w-full h-full flex" id={SlotID.AppBody}>
<Routes>
<Route path="/" element={<WindowContent />}>
<Route
index
element={<Navigate to="/OpenSourceTools/Extractor/PDF" replace />}
/>
<Route
path="/OpenSourceTools/Extractor/PDF"
element={<PDFUpload />}
/>
<Route
path="/OpenSourceTools/Extractor/PDF/:jobID"
element={<PDFExtractionJob />}
/>
<Route
path="*"
element={<Navigate to="/OpenSourceTools/Extractor/PDF" replace />}
/>
</Route>
</Routes>
</div>
);
};
return <>{renderContent()}</>;
}
export function Home() {
return (
<ErrorBoundary>
<LanguageProvider>
<HashRouter>
<Screen />
</HashRouter>
</LanguageProvider>
</ErrorBoundary>
);
}
import { Routes, Route } from "react-router-dom";
import PDFUpload from "@/pages/extract/components/pdf-upload";
import PDFExtractionJob from "@/pages/extract/components/pdf-extraction";
function AppRoutes() {
return (
<>
<Route path="/OpenSourceTools/Extractor/PDF" element={<PDFUpload />} />
<Route
path="/OpenSourceTools/Extractor/PDF/:jobID"
element={<PDFExtractionJob />}
/>
</>
);
}
export default AppRoutes;
import {
getExtractTaskIdProgress,
getPdfExtractQueue,
TaskIdResItem,
} from "@/api/extract";
import { create } from "zustand";
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { UPDATE_TASK_LIST } from "@/constant/event";
import { useQuery } from "@tanstack/react-query";
interface ExtractorState {
taskInfo: TaskIdResItem;
queueLoading: boolean | null;
interfaceError: boolean;
setTaskInfo: (taskInfo: TaskIdResItem) => void;
setQueueLoading: (loading: boolean | null) => void;
setInterfaceError: (error: boolean) => void;
}
const defaultTaskInfo: TaskIdResItem = {
id: 0,
rank: 0,
state: "pending",
url: "",
type: "unknown",
queues: -1,
};
const useExtractorStore = create<ExtractorState>((set) => ({
taskInfo: defaultTaskInfo,
queueLoading: null,
interfaceError: false,
setTaskInfo: (taskInfo: any) => set({ taskInfo }),
setQueueLoading: (loading) => set({ queueLoading: loading }),
setInterfaceError: (error) => set({ interfaceError: error }),
}));
export const useJobExtraction = () => {
const { jobID } = useParams<{ jobID: string }>();
const {
setTaskInfo,
setQueueLoading,
queueLoading,
taskInfo,
interfaceError,
setInterfaceError,
} = useExtractorStore();
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const [isPolling, setIsPolling] = useState(true);
const stopTaskLoading = () => {
setQueueLoading(false);
};
// Query for task progress
const taskProgressQuery = useQuery({
queryKey: ["taskProgress", jobID],
queryFn: () => {
setQueueLoading(true);
setIsPolling(true);
return getExtractTaskIdProgress(jobID!)
.then((res) => {
if (res?.state === "done" || res?.state === "failed") {
stopTaskLoading();
document.dispatchEvent(
new CustomEvent("UPDATE_TASK_LIST", {
detail: { state: res.state, id: jobID },
})
);
}
if (res) {
setTaskInfo(res);
}
return res;
})
.catch(() => {
stopTaskLoading();
setTaskInfo({ state: "failed" });
});
},
enabled: false,
});
// Query for queue status
const queueStatusQuery = useQuery({
queryKey: ["queueStatus", jobID],
queryFn: async () => {
setQueueLoading(true);
const response = await getPdfExtractQueue(jobID).then((res) => {
// setTaskInfo({ rand: "failed" });
if (res) {
const targetPendingRunningJob = res?.filter(
(i) => String(i.id) === jobID
)?.[0];
if (targetPendingRunningJob) {
setTaskInfo(targetPendingRunningJob);
} else {
setIsPolling(false);
setQueueLoading(false);
getExtractTaskIdProgress(jobID!).then((res) => {
setTaskInfo(res as any);
});
}
}
return res;
});
return response;
},
enabled:
isPolling &&
(taskProgressQuery?.data?.state === "running" ||
taskProgressQuery?.data?.state === "pending"),
refetchInterval: 2000, // Poll every 2 seconds
});
useEffect(() => {
if (taskProgressQuery.data?.state === "done") {
stopTaskLoading();
setInterfaceError(false);
setIsPolling(false);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
} else {
timeoutRef.current = setTimeout(() => {
document.dispatchEvent(
new CustomEvent(UPDATE_TASK_LIST, {
detail: { state: "done", jobID },
})
);
}, 10);
}
} else if (taskProgressQuery.data?.state === "failed") {
stopTaskLoading();
setInterfaceError(true);
setIsPolling(false);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
} else {
timeoutRef.current = setTimeout(() => {
document.dispatchEvent(
new CustomEvent(UPDATE_TASK_LIST, {
detail: { state: "failed", jobID },
})
);
}, 10);
}
}
// TIP这里得用taskInfo
}, [taskProgressQuery.data]);
const refreshQueue = () => {
// stop last ID polling
setIsPolling(false);
setTaskInfo(defaultTaskInfo);
taskProgressQuery.refetch();
};
useEffect(() => {
if (jobID) {
// stop last ID polling d
setTaskInfo(defaultTaskInfo);
taskProgressQuery.refetch();
}
}, [jobID]);
return {
taskInfo: taskInfo,
isLoading: queueLoading,
isError:
interfaceError || taskProgressQuery.isError || queueStatusQuery.isError,
refreshQueue,
};
};
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