Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
wangsen
MinerU
Commits
3a42ebbf
Unverified
Commit
3a42ebbf
authored
Nov 01, 2024
by
Xiaomeng Zhao
Committed by
GitHub
Nov 01, 2024
Browse files
Merge pull request #838 from opendatalab/release-0.9.0
Release 0.9.0
parents
765c6d77
14024793
Changes
591
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1093 additions
and
0 deletions
+1093
-0
projects/web/src/pages/extract/formula/formula-detail/index.tsx
...ts/web/src/pages/extract/formula/formula-detail/index.tsx
+143
-0
projects/web/src/pages/extract/formula/formula-popover/index.module.scss
...c/pages/extract/formula/formula-popover/index.module.scss
+18
-0
projects/web/src/pages/extract/formula/formula-popover/index.tsx
...s/web/src/pages/extract/formula/formula-popover/index.tsx
+54
-0
projects/web/src/pages/extract/formula/formula-upload/index.module.scss
...rc/pages/extract/formula/formula-upload/index.module.scss
+87
-0
projects/web/src/pages/extract/formula/formula-upload/index.tsx
...ts/web/src/pages/extract/formula/formula-upload/index.tsx
+132
-0
projects/web/src/pages/extract/formula/index.module.scss
projects/web/src/pages/extract/formula/index.module.scss
+0
-0
projects/web/src/pages/extract/formula/index.tsx
projects/web/src/pages/extract/formula/index.tsx
+11
-0
projects/web/src/pages/extract/index.module.scss
projects/web/src/pages/extract/index.module.scss
+0
-0
projects/web/src/pages/extract/index.tsx
projects/web/src/pages/extract/index.tsx
+0
-0
projects/web/src/pages/extract/table/index.tsx
projects/web/src/pages/extract/table/index.tsx
+5
-0
projects/web/src/pages/extract/table/table-detail/index.tsx
projects/web/src/pages/extract/table/table-detail/index.tsx
+5
-0
projects/web/src/pages/home.module.scss
projects/web/src/pages/home.module.scss
+0
-0
projects/web/src/pages/home.tsx
projects/web/src/pages/home.tsx
+75
-0
projects/web/src/routes/index.tsx
projects/web/src/routes/index.tsx
+17
-0
projects/web/src/store/jobProgress.ts
projects/web/src/store/jobProgress.ts
+183
-0
projects/web/src/store/languageStore.ts
projects/web/src/store/languageStore.ts
+47
-0
projects/web/src/store/mdStore.ts
projects/web/src/store/mdStore.ts
+250
-0
projects/web/src/styles/variable.scss
projects/web/src/styles/variable.scss
+1
-0
projects/web/src/types/extract-task-type.ts
projects/web/src/types/extract-task-type.ts
+21
-0
projects/web/src/utils/download.ts
projects/web/src/utils/download.ts
+44
-0
No files found.
projects/web/src/pages/extract/formula/formula-detail/index.tsx
0 → 100644
View file @
3a42ebbf
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
;
projects/web/src/pages/extract/formula/formula-popover/index.module.scss
0 → 100644
View file @
3a42ebbf
.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
;
}
}
}
projects/web/src/pages/extract/formula/formula-popover/index.tsx
0 → 100644
View file @
3a42ebbf
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
;
projects/web/src/pages/extract/formula/formula-upload/index.module.scss
0 → 100644
View file @
3a42ebbf
.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
;
}
projects/web/src/pages/extract/formula/formula-upload/index.tsx
0 → 100644
View file @
3a42ebbf
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
;
projects/web/src/pages/extract/formula/index.module.scss
0 → 100644
View file @
3a42ebbf
projects/web/src/pages/extract/formula/index.tsx
0 → 100644
View file @
3a42ebbf
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
;
projects/web/src/pages/extract/index.module.scss
0 → 100644
View file @
3a42ebbf
projects/web/src/pages/extract/index.tsx
0 → 100644
View file @
3a42ebbf
projects/web/src/pages/extract/table/index.tsx
0 → 100644
View file @
3a42ebbf
const
ExtractorTable
=
()
=>
{
return
<>
ExtractorTable
</>;
};
export
default
ExtractorTable
;
projects/web/src/pages/extract/table/table-detail/index.tsx
0 → 100644
View file @
3a42ebbf
const
TableDetail
=
()
=>
{
return
<>
TableDetail
</>;
};
export
default
TableDetail
;
projects/web/src/pages/home.module.scss
0 → 100644
View file @
3a42ebbf
projects/web/src/pages/home.tsx
0 → 100644
View file @
3a42ebbf
"
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
>
);
}
projects/web/src/routes/index.tsx
0 → 100644
View file @
3a42ebbf
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
;
projects/web/src/store/jobProgress.ts
0 → 100644
View file @
3a42ebbf
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
,
};
};
projects/web/src/store/languageStore.ts
0 → 100644
View file @
3a42ebbf
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
};
}),
}));
projects/web/src/store/mdStore.ts
0 → 100644
View file @
3a42ebbf
// 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
;
projects/web/src/styles/variable.scss
0 → 100644
View file @
3a42ebbf
$page-min-witch
:
1260px
;
\ No newline at end of file
projects/web/src/types/extract-task-type.ts
0 → 100644
View file @
3a42ebbf
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
"
,
}
projects/web/src/utils/download.ts
0 → 100644
View file @
3a42ebbf
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
);
}
}
Prev
1
…
21
22
23
24
25
26
27
28
29
30
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment