Unverified Commit 3a42ebbf authored by Xiaomeng Zhao's avatar Xiaomeng Zhao Committed by GitHub
Browse files

Merge pull request #838 from opendatalab/release-0.9.0

Release 0.9.0
parents 765c6d77 14024793
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,
};
};
import { create } from "zustand";
import { Language } from "@/constant";
import { LOCALE_STORAGE_KEY } from "@/constant/storage";
type LanguageType = (typeof Language)[keyof typeof Language];
type LanguageStore = {
language: LanguageType;
setLanguage: (language: LanguageType) => void;
toggleLanguage: () => void;
};
const getInitialLanguage = (): LanguageType => {
// Try to get language setting from localStorage
const savedLanguage = localStorage.getItem(
LOCALE_STORAGE_KEY
) as LanguageType;
if (savedLanguage && Object.values(Language).includes(savedLanguage)) {
return savedLanguage;
}
// If no valid language setting in localStorage, try to get browser language
const browserLanguage = navigator.language.toLowerCase();
if (browserLanguage.startsWith("zh")) {
return Language.ZH_CN;
} else if (browserLanguage.startsWith("en")) {
return Language.EN_US;
}
// Default to Chinese
return Language.ZH_CN;
};
export const useLanguageStore = create<LanguageStore>((set) => ({
language: getInitialLanguage(),
setLanguage: (language) => {
localStorage.setItem(LOCALE_STORAGE_KEY, language);
set({ language });
},
toggleLanguage: () =>
set((state) => {
const newLanguage =
state.language === Language.ZH_CN ? Language.EN_US : Language.ZH_CN;
localStorage.setItem(LOCALE_STORAGE_KEY, newLanguage);
return { language: newLanguage };
}),
}));
// mdStore.ts
import { create } from "zustand";
import axios from "axios";
import { updateMarkdownContent, UpdateMarkdownRequest } from "@/api/extract"; // 确保路径正确
interface MdContent {
content: string;
isLoading: boolean;
}
type AnchorType =
| "span"
| "div"
| "comment"
| "data-attribute"
| "hr"
| "mark"
| "p";
interface AnchorOptions {
type: AnchorType;
prefix?: string;
style?: string;
className?: string;
customAttributes?: Record<string, string>;
}
const defaultAnchorOptions: AnchorOptions = {
type: "span",
prefix: "md-anchor-",
style: "display:none;",
className: "",
customAttributes: {},
};
interface MdState {
mdContents: Record<string, MdContent>;
allMdContent: string;
allMdContentWithAnchor: string;
error: Error | null;
currentRequestId: number;
setMdUrlArr: (urls: string[]) => Promise<void>;
getAllMdContent: (data: string[]) => string;
setAllMdContent: (val?: string) => void;
setAllMdContentWithAnchor: (val?: string) => void;
getContentWithAnchors: (
data: string[],
options?: Partial<AnchorOptions>
) => string;
jumpToAnchor: (anchorId: string) => number;
reset: () => void;
updateMdContent: (
fileKey: string,
pageNumber: string | number,
newContent: string
) => Promise<void>;
}
const MAX_CONCURRENT_REQUESTS = 2;
const initialState = {
mdContents: {},
allMdContent: "",
allMdContentWithAnchor: "",
error: null,
currentRequestId: 0,
};
const useMdStore = create<MdState>((set, get) => ({
...initialState,
reset: () => {
set(initialState);
},
setAllMdContent: (value?: string) => {
set(() => ({
allMdContent: value,
}));
},
setAllMdContentWithAnchor: (value?: string) => {
set(() => ({
allMdContentWithAnchor: value,
}));
},
setMdUrlArr: async (urls: string[]) => {
const requestId = get().currentRequestId + 1;
set((state) => ({ currentRequestId: requestId, error: null }));
const fetchContent = async (url: string): Promise<[string, string]> => {
try {
const response = await axios.get<string>(url);
return [url, response.data];
} catch (error) {
if (get().currentRequestId === requestId) {
set((state) => ({ error: error as Error }));
}
return [url, ""];
}
};
const fetchWithConcurrency = async (
urls: string[]
): Promise<[string, string][]> => {
const queue = [...urls];
const results: [string, string][] = [];
const inProgress = new Set<Promise<[string, string]>>();
while (queue.length > 0 || inProgress.size > 0) {
while (inProgress.size < MAX_CONCURRENT_REQUESTS && queue.length > 0) {
const url = queue.shift()!;
const promise = fetchContent(url);
inProgress.add(promise);
promise.then((result) => {
results.push(result);
inProgress.delete(promise);
});
}
if (inProgress.size > 0) {
await Promise.race(inProgress);
}
}
return results;
};
const results = await fetchWithConcurrency(urls);
if (get().currentRequestId === requestId) {
const newMdContents: Record<string, MdContent> = {};
results.forEach(([url, content]) => {
newMdContents[url] = { content, isLoading: false };
});
set((state) => ({
mdContents: newMdContents,
allMdContent: state.getAllMdContent(results.map((i) => i[1])),
allMdContentWithAnchor: state.getContentWithAnchors(
results.map((i) => i[1])
),
}));
}
},
getAllMdContent: (data) => {
return data?.join("\n\n");
},
getContentWithAnchors: (data: string[], options?: Partial<AnchorOptions>) => {
const opts = { ...defaultAnchorOptions, ...options };
const generateAnchorTag = (index: number) => {
const id = `${opts.prefix}${index}`;
const attributes = Object.entries(opts.customAttributes || {})
.map(([key, value]) => `${key}="${value}"`)
.join(" ");
switch (opts.type) {
case "span":
case "div":
case "mark":
case "p":
return `<${opts.type} id="${id}" style="${opts.style}" class="${opts.className}" ${attributes}></${opts.type}>`;
case "comment":
return `<!-- anchor: ${id} -->`;
case "data-attribute":
return `<span data-anchor="${id}" style="${opts.style}" class="${opts.className}" ${attributes}></span>`;
case "hr":
return `<hr id="${id}" style="${opts.style}" class="${opts.className}" ${attributes}>`;
default:
return `<span id="${id}" style="${opts.style}" class="${opts.className}" ${attributes}></span>`;
}
};
return data
?.map((content, index) => {
const anchorTag = generateAnchorTag(index);
return `${anchorTag}\n\n${content}`;
})
.join("\n\n");
},
jumpToAnchor: (anchorId: string) => {
const { mdContents } = get();
const contentArray = Object.values(mdContents).map(
(content) => content.content
);
let totalLength = 0;
for (let i = 0; i < contentArray.length; i++) {
if (anchorId === `md-anchor-${i}`) {
return totalLength;
}
totalLength += contentArray[i].length + 2; // +2 for "\n\n"
}
return -1; // Anchor not found
},
updateMdContent: async (
fileKey: string,
pageNumber: string,
newContent: string
) => {
try {
const params: UpdateMarkdownRequest = {
file_key: fileKey,
data: {
[pageNumber]: newContent,
},
};
const result = await updateMarkdownContent(params);
if (result && result.success) {
// 更新本地状态
set((state) => {
const updatedMdContents = { ...state.mdContents };
if (updatedMdContents[fileKey]) {
updatedMdContents[fileKey] = {
...updatedMdContents[fileKey],
content: newContent,
};
}
// 重新计算 allMdContent 和 allMdContentWithAnchor
const contentArray = Object.values(updatedMdContents).map(
(content) => content.content
);
const newAllMdContent = state.getAllMdContent(contentArray);
const newAllMdContentWithAnchor =
state.getContentWithAnchors(contentArray);
return {
mdContents: updatedMdContents,
allMdContent: newAllMdContent,
allMdContentWithAnchor: newAllMdContentWithAnchor,
};
});
} else {
throw new Error("Failed to update Markdown content");
}
} catch (error) {
set({ error: error as Error });
throw error;
}
},
}));
export default useMdStore;
$page-min-witch: 1260px;
\ No newline at end of file
export type ExtractTaskType =
| "pdf"
| "formula-detect"
| "formula-extract"
| "table-recogn";
export const EXTRACTOR_TYPE_LIST = {
table: "table",
formula: "formula",
pdf: "PDF",
};
export enum FORMULA_TYPE {
extract = "extract",
detect = "detect",
}
export enum MD_PREVIEW_TYPE {
preview = "preview",
code = "code",
}
export async function downloadFileUseAScript(
url: string,
filename?: string
): Promise<void> {
try {
// 发起请求获取文件
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 获取文件内容的 Blob
const blob = await response.blob();
// 创建一个 Blob URL
const blobUrl = window.URL.createObjectURL(blob);
// 创建一个隐藏的<a>元素
const link = document.createElement("a");
link.style.display = "none";
link.href = blobUrl;
// 设置下载的文件名
const contentDisposition = response.headers.get("Content-Disposition");
const fileName =
filename ||
(contentDisposition
? contentDisposition.split("filename=")[1].replace(/['"]/g, "")
: url.split("/").pop() || "download");
link.download = fileName;
// 将链接添加到文档中并触发点击
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error("Download failed:", error);
}
}
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