From 76377060b64e33e3dc4050859b6bb2f57f71577d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E7=92=87?= Date: Fri, 6 Jun 2025 13:55:53 +0800 Subject: [PATCH] feat: optimize code --- .../components/dialog/image-dialog/index.js | 4 +- .../components/dialog/move-dirent-dialog.js | 6 +- .../src/components/file-view/file-view.js | 24 +- .../components/toolbar/table-files-toolbar.js | 3 +- frontend/src/hooks/index.js | 3 +- frontend/src/hooks/metadata-ai-operation.js | 97 +++-- frontend/src/hooks/metadata-middleware.js | 38 ++ frontend/src/hooks/metadata-status.js | 18 +- frontend/src/index.js | 7 +- frontend/src/metadata/api.js | 9 - .../dialog/ocr-result-dialog/index.css | 8 + .../dialog/ocr-result-dialog/index.js | 139 ++++++ .../components/metadata-details/ai-icon.js | 98 ++--- .../components/metadata-details/constants.js | 1 - .../src/metadata/constants/column/common.js | 1 - .../src/metadata/constants/column/private.js | 4 - .../src/metadata/constants/event-bus-type.js | 1 - .../src/metadata/hooks/metadata-details.js | 32 +- frontend/src/metadata/hooks/metadata-view.js | 45 +- frontend/src/metadata/utils/column/index.js | 4 - .../views/face-recognition/peoples/index.js | 2 +- frontend/src/metadata/views/kanban/index.js | 2 +- .../src/metadata/views/table/context-menu.js | 39 +- frontend/src/metadata/views/table/index.js | 8 +- .../table/masks/interaction-masks/index.js | 2 +- .../views/table/table-main/records/body.js | 1 + .../table-main/records/group-body/index.js | 1 + .../views/table/table-main/records/index.js | 3 +- .../lib-content-view/lib-content-view.js | 411 +++++++++--------- frontend/src/tag/views/tag-files/index.js | 4 +- frontend/src/view-file-sdoc.js | 12 +- seahub/ai/apis.py | 90 +--- seahub/ai/urls.py | 12 + seahub/repo_metadata/apis.py | 15 +- seahub/repo_metadata/metadata_server_api.py | 2 - seahub/repo_metadata/utils.py | 26 -- seahub/urls.py | 17 +- 37 files changed, 610 insertions(+), 579 deletions(-) create mode 100644 frontend/src/hooks/metadata-middleware.js create mode 100644 frontend/src/metadata/components/dialog/ocr-result-dialog/index.css create mode 100644 frontend/src/metadata/components/dialog/ocr-result-dialog/index.js create mode 100644 seahub/ai/urls.py diff --git a/frontend/src/components/dialog/image-dialog/index.js b/frontend/src/components/dialog/image-dialog/index.js index 9aa5058c2e..ef461d0bde 100644 --- a/frontend/src/components/dialog/image-dialog/index.js +++ b/frontend/src/components/dialog/image-dialog/index.js @@ -16,7 +16,7 @@ const SIDE_PANEL_EXPANDED_WIDTH = 300; const ImageDialog = ({ repoID, repoInfo, enableRotate: oldEnableRotate = true, imageItems, imageIndex, closeImagePopup, moveToPrevImage, moveToNextImage, onDeleteImage, onRotateImage, isCustomPermission }) => { const [expanded, setExpanded] = useState(false); - const { enableOCR, enableMetadata, canModify, onOCR: onOCRAPI, OCRSuccessCallBack } = useMetadataAIOperations(); + const { enableOCR, enableMetadata, canModify, onOCRByImageDialog } = useMetadataAIOperations(); const downloadImage = useCallback((url) => { location.href = url; @@ -52,7 +52,7 @@ const ImageDialog = ({ repoID, repoInfo, enableRotate: oldEnableRotate = true, i const isSystemFolder = SYSTEM_FOLDERS.find(folderPath => mainImg.parentDir.startsWith(folderPath)); let onOCR = null; if (enableOCR && enableMetadata && canModify && !isSystemFolder) { - onOCR = () => onOCRAPI({ parentDir: mainImg.parentDir, fileName: mainImg.name }, { success_callback: OCRSuccessCallBack }); + onOCR = () => onOCRByImageDialog({ parentDir: mainImg.parentDir, fileName: mainImg.name }); } const renderSidePanel = () => { diff --git a/frontend/src/components/dialog/move-dirent-dialog.js b/frontend/src/components/dialog/move-dirent-dialog.js index 6cd2f86fa6..980d44c278 100644 --- a/frontend/src/components/dialog/move-dirent-dialog.js +++ b/frontend/src/components/dialog/move-dirent-dialog.js @@ -22,7 +22,7 @@ const propTypes = { onAddFolder: PropTypes.func, }; -class MoveDirent extends React.Component { +class MoveDirentDialog extends React.Component { constructor(props) { super(props); @@ -382,6 +382,6 @@ class MoveDirent extends React.Component { } } -MoveDirent.propTypes = propTypes; +MoveDirentDialog.propTypes = propTypes; -export default MoveDirent; +export default MoveDirentDialog; diff --git a/frontend/src/components/file-view/file-view.js b/frontend/src/components/file-view/file-view.js index 34e5b50ed2..a623306194 100644 --- a/frontend/src/components/file-view/file-view.js +++ b/frontend/src/components/file-view/file-view.js @@ -13,9 +13,7 @@ import FileToolbar from './file-toolbar'; import CommentPanel from './comment-panel'; import OnlyofficeFileToolbar from './onlyoffice-file-toolbar'; import EmbeddedFileDetails from '../dirent-detail/embedded-file-details'; -import { MetadataStatusProvider } from '../../hooks'; -import { CollaboratorsProvider } from '../../metadata'; -import { TagsProvider } from '../../tag/hooks'; +import { MetadataMiddlewareProvider, MetadataStatusProvider } from '../../hooks'; import Loading from '../loading'; import '../../css/file-view.css'; @@ -180,17 +178,15 @@ class FileView extends React.Component { } {isDetailsPanelOpen && ( - - - - - + + + )} diff --git a/frontend/src/components/toolbar/table-files-toolbar.js b/frontend/src/components/toolbar/table-files-toolbar.js index dc839e7f9c..b7d97e8157 100644 --- a/frontend/src/components/toolbar/table-files-toolbar.js +++ b/frontend/src/components/toolbar/table-files-toolbar.js @@ -84,6 +84,7 @@ const TableFilesToolbar = ({ repoID }) => { const isDescribableFile = canModifyRow && Utils.isDescriptionSupportedFile(fileName); const isImage = Utils.imageCheck(fileName); const isVideo = Utils.videoCheck(fileName); + const isPDF = Utils.pdfCheck(fileName); const descriptionColumn = getColumnByKey(columns, PRIVATE_COLUMN_KEY.FILE_DESCRIPTION); const aiOptions = []; @@ -95,7 +96,7 @@ const TableFilesToolbar = ({ repoID }) => { aiOptions.push(GENERATE_DESCRIPTION); } - if (enableOCR && isImage) { + if (enableOCR && (isImage || isPDF)) { aiOptions.push(OCR); } diff --git a/frontend/src/hooks/index.js b/frontend/src/hooks/index.js index 6037889dc7..0ede4cad11 100644 --- a/frontend/src/hooks/index.js +++ b/frontend/src/hooks/index.js @@ -1,2 +1,3 @@ -export { MetadataStatusProvider, useMetadataStatus } from './metadata-status'; export { DownloadFileProvider } from './download-file'; +export { MetadataMiddlewareProvider } from './metadata-middleware'; +export { MetadataStatusProvider, useMetadataStatus } from './metadata-status'; diff --git a/frontend/src/hooks/metadata-ai-operation.js b/frontend/src/hooks/metadata-ai-operation.js index 04c3d01f7d..e4894189aa 100644 --- a/frontend/src/hooks/metadata-ai-operation.js +++ b/frontend/src/hooks/metadata-ai-operation.js @@ -1,9 +1,11 @@ -import React, { useContext, useCallback, useMemo } from 'react'; +import React, { useContext, useCallback, useMemo, useState, useRef } from 'react'; import metadataAPI from '../metadata/api'; import { Utils } from '../utils/utils'; import toaster from '../components/toast'; import { PRIVATE_COLUMN_KEY, EVENT_BUS_TYPE } from '../metadata/constants'; import { gettext, lang } from '../utils/constants'; +import OCRResultDialog from '../metadata/components/dialog/ocr-result-dialog'; +import FileTagsDialog from '../metadata/components/dialog/file-tags-dialog'; // This hook provides content related to metadata ai operation const MetadataAIOperationsContext = React.createContext(null); @@ -17,33 +19,48 @@ export const MetadataAIOperationsProvider = ({ repoInfo, children }) => { + const [isOcrResultDialogShow, setOcrResultDialogShow] = useState(false); + const [isFileTagsDialogShow, setFileTagsDialogShow] = useState(false); + + const recordRef = useRef(null); + const opCallBack = useRef(null); + const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]); const canModify = useMemo(() => permission === 'rw', [permission]); - const OCRSuccessCallBack = useCallback(({ parentDir, fileName, ocrResult } = {}) => { - if (!ocrResult) return; - const update = { [PRIVATE_COLUMN_KEY.OCR]: ocrResult }; - metadataAPI.modifyRecord(repoID, { parentDir, fileName }, update).then(res => { - const eventBus = window?.sfMetadataContext?.eventBus; - eventBus && eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, { parentDir, fileName }, update); - }); - }, [repoID]); + const closeFileTagsDialog = useCallback(() => { + recordRef.current = null; + opCallBack.current = null; + setFileTagsDialogShow(false); + }, []); - const onOCR = useCallback(({ parentDir, fileName }, { success_callback, fail_callback } = {}) => { - const filePath = Utils.joinPath(parentDir, fileName); - const inProgressToaster = toaster.notifyInProgress(gettext('Extracting text by AI...'), { duration: null }); - metadataAPI.ocr(repoID, filePath).then(res => { - const ocrResult = res.data.ocr_result; - const validResult = Array.isArray(ocrResult) && ocrResult.length > 0 ? JSON.stringify(ocrResult) : null; - inProgressToaster.close(); - toaster.success(gettext('Text extracted')); - success_callback && success_callback({ parentDir, fileName, ocrResult: validResult }); - }).catch(error => { - inProgressToaster.close(); - const errorMessage = gettext('Failed to extract text'); - toaster.danger(errorMessage); - fail_callback && fail_callback(); - }); + const closeOcrResultDialog = useCallback(() => { + recordRef.current = null; + opCallBack.current = null; + setOcrResultDialogShow(false); + }, []); + + const onOCR = useCallback((record, { success_callback }) => { + recordRef.current = record; + opCallBack.current = success_callback; + setOcrResultDialogShow(true); + }, []); + + const onOCRByImageDialog = useCallback(({ parentDir, fileName } = {}) => { + recordRef.current = { + [PRIVATE_COLUMN_KEY.PARENT_DIR]: parentDir, + [PRIVATE_COLUMN_KEY.FILE_NAME]: fileName, + }; + + opCallBack.current = (description) => { + const update = { [PRIVATE_COLUMN_KEY.FILE_DESCRIPTION]: description }; + metadataAPI.modifyRecord(repoID, { parentDir, fileName }, update).then(res => { + const eventBus = window?.sfMetadataContext?.eventBus; + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, { parentDir, fileName }, update); + eventBus && eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, { parentDir, fileName }, update); + }); + }; + setOcrResultDialogShow(true); }, [repoID]); const generateDescription = useCallback(({ parentDir, fileName }, { success_callback, fail_callback } = {}) => { @@ -106,23 +123,11 @@ export const MetadataAIOperationsProvider = ({ }); }, [repoID]); - - const extractText = useCallback(({ parentDir, fileName }, { success_callback, fail_callback } = {}) => { - const filePath = Utils.joinPath(parentDir, fileName); - const inProgressToaster = toaster.notifyInProgress(gettext('Extracting text by AI...'), { duration: null }); - metadataAPI.extractText(repoID, filePath).then(res => { - console.log(res) - const extractedText = res?.data?.text || res.data.text || ''; - inProgressToaster.close(); - success_callback && success_callback({ parentDir, fileName, extractedText }); - }).catch(error => { - inProgressToaster.close(); - const errorMessage = gettext('Failed to extract text'); - toaster.danger(errorMessage); - fail_callback && fail_callback(); - }); - }, [repoID]); - + const generateFileTags = useCallback((record, { success_callback }) => { + recordRef.current = record; + opCallBack.current = success_callback; + setFileTagsDialogShow(true); + }, []); return ( {children} + {isFileTagsDialogShow && ( + + )} + {isOcrResultDialogShow && ( + + )} ); }; diff --git a/frontend/src/hooks/metadata-middleware.js b/frontend/src/hooks/metadata-middleware.js new file mode 100644 index 0000000000..82401e4e14 --- /dev/null +++ b/frontend/src/hooks/metadata-middleware.js @@ -0,0 +1,38 @@ +import React, { useContext } from 'react'; +import { MetadataAIOperationsProvider } from './metadata-ai-operation'; +import { TagsProvider } from '../tag/hooks'; +import { CollaboratorsProvider } from '../metadata'; +import { useMetadataStatus } from './metadata-status'; + +const MetadataMiddlewareContext = React.createContext(null); + +export const MetadataMiddlewareProvider = ({ repoID, currentPath, repoInfo, onTreeNodeClick, tagsChangedCallback, children }) => { + const { enableMetadata, enableOCR, enableTags, tagsLang } = useMetadataStatus(); + + return ( + + + + + {children} + + + + + ); +}; + +export const useMetadataMiddleware = () => { + const context = useContext(MetadataMiddlewareContext); + if (!context) { + throw new Error('\'MetadataMiddlewareContext\' is null'); + } + return context; +}; diff --git a/frontend/src/hooks/metadata-status.js b/frontend/src/hooks/metadata-status.js index d5e1f5f6dd..95d30acee1 100644 --- a/frontend/src/hooks/metadata-status.js +++ b/frontend/src/hooks/metadata-status.js @@ -2,9 +2,10 @@ import React, { useContext, useEffect, useCallback, useState, useMemo } from 're import metadataAPI from '../metadata/api'; import { Utils } from '../utils/utils'; import toaster from '../components/toast'; -import { MetadataAIOperationsProvider } from './metadata-ai-operation'; import Loading from '../components/loading'; +const { enableSeafileAI, enableSeafileOCR } = window.app.config; + // This hook provides content related to seahub interaction, such as whether to enable extended attributes const MetadataStatusContext = React.createContext(null); @@ -64,8 +65,8 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, sta setEnableTags(enableTags); setTagsLang(tagsLang || 'en'); setDetailsSettings(JSON.parse(detailsSettings)); - setEnableOCR(enableOCR); - setEnableFaceRecognition(enableFaceRecognition); + setEnableOCR(enableSeafileOCR && enableOCR); + setEnableFaceRecognition(enableSeafileAI && enableFaceRecognition); setEnableMetadata(enableMetadata); setLoading(false); }).catch(error => { @@ -154,16 +155,7 @@ export const MetadataStatusProvider = ({ repoID, repoInfo, hideMetadataView, sta }} > {!isLoading && ( - - {children} - + <>{children} )} ); diff --git a/frontend/src/index.js b/frontend/src/index.js index a8827052b8..bbe13e5967 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -5,8 +5,7 @@ import { I18nextProvider } from 'react-i18next'; import i18n from './_i18n/i18n-seafile-editor'; import MarkdownEditor from './pages/markdown-editor'; import Loading from './components/loading'; -import { MetadataStatusProvider } from './hooks'; -import { CollaboratorsProvider } from './metadata'; +import { MetadataMiddlewareProvider, MetadataStatusProvider } from './hooks'; import './index.css'; @@ -17,9 +16,9 @@ root.render( }> - + - + diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js index 863c3a4107..190f19e840 100644 --- a/frontend/src/metadata/api.js +++ b/frontend/src/metadata/api.js @@ -406,15 +406,6 @@ class MetadataManagerAPI { return this.req.post(url, params); }; - extractText = (repoID, filePath) => { - const url = this.server + '/api/v2.1/ai/extract-text/'; - const params = { - path: filePath, - repo_id: repoID, - }; - return this.req.post(url, params); - }; - } const metadataAPI = new MetadataManagerAPI(); diff --git a/frontend/src/metadata/components/dialog/ocr-result-dialog/index.css b/frontend/src/metadata/components/dialog/ocr-result-dialog/index.css new file mode 100644 index 0000000000..c7350d6683 --- /dev/null +++ b/frontend/src/metadata/components/dialog/ocr-result-dialog/index.css @@ -0,0 +1,8 @@ +.sf-metadata-ocr-file-dialog .sf-centered-loading { + border-top: 1px solid #e9ecef; +} + +.sf-metadata-ocr-file-dialog .seafile-multicolor-icon-save { + height: 15px; + width: 15px; +} diff --git a/frontend/src/metadata/components/dialog/ocr-result-dialog/index.js b/frontend/src/metadata/components/dialog/ocr-result-dialog/index.js new file mode 100644 index 0000000000..9f4ca037a7 --- /dev/null +++ b/frontend/src/metadata/components/dialog/ocr-result-dialog/index.js @@ -0,0 +1,139 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { I18nextProvider } from 'react-i18next'; +import { SimpleEditor, getBrowserInfo, LongTextModal, BrowserTip, slateToMdString, MarkdownPreview } from '@seafile/seafile-editor'; +import CenteredLoading from '../../../../components/centered-loading'; +import toaster from '../../../../components/toast'; +import { gettext, lang } from '../../../../utils/constants'; +import { getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell'; +import { Utils } from '../../../../utils/utils'; +import i18n from '../../../../_i18n/i18n-seafile-editor'; +import Icon from '../../../../components/icon'; + +import './index.css'; + +const OCRResultDialog = ({ record, onToggle, saveToDescription }) => { + const [isLoading, setLoading] = useState(true); + const [isFullScreen, setIsFullScreen] = useState(false); + const [dialogStyle, setDialogStyle] = useState({}); + + const ocrResult = useRef(null); + + const parentDir = useMemo(() => getParentDirFromRecord(record), [record]); + const fileName = useMemo(() => getFileNameFromRecord(record), [record]); + + const editorRef = useRef(null); + + const { isValidBrowser, isWindowsWechat } = useMemo(() => { + return getBrowserInfo(true); + }, []); + + const onFullScreenToggle = useCallback(() => { + let containerStyle = {}; + if (!isFullScreen) { + containerStyle = { + width: '100%', + height: '100%', + top: 0, + border: 'none' + }; + } + setIsFullScreen(!isFullScreen); + setDialogStyle(containerStyle); + }, [isFullScreen]); + + const onContainerKeyDown = useCallback((event) => { + event.stopPropagation(); + if (event.keyCode === 27) { + event.stopPropagation(); + event.preventDefault(); + onToggle(); + } + }, [onToggle]); + + const onSave = useCallback(() => { + const newContent = editorRef.current?.getSlateValue(); + const value = slateToMdString(newContent); + saveToDescription(value); + onToggle(); + }, [saveToDescription, onToggle]); + + useEffect(() => { + const path = window.sfMetadataContext.canModifyRow(record) ? Utils.joinPath(parentDir, fileName) : ''; + if (path === '') { + setLoading(false); + return; + } + window.sfMetadataContext.ocr(path).then(res => { + const result = res.data?.ocr_result || ''; + ocrResult.current = result.replaceAll('\n\n', '\n').replaceAll('\n', '\n\n'); + setLoading(false); + }).catch(error => { + const errorMessage = gettext('Failed to extract text'); + toaster.danger(errorMessage); + setLoading(false); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + +
+
+
+ {gettext('OCR result')} +
+ + + + + + + +
+
+ {!isValidBrowser && } +
+
+ {isLoading ? ( + + ) : ( + <> + {isWindowsWechat ? ( + + ) : ( + + )} + + )} +
+
+
+
+ ); +}; + +OCRResultDialog.propTypes = { + record: PropTypes.object, + onToggle: PropTypes.func, + saveToDescription: PropTypes.func, +}; + +export default OCRResultDialog; diff --git a/frontend/src/metadata/components/metadata-details/ai-icon.js b/frontend/src/metadata/components/metadata-details/ai-icon.js index 3462b3214d..0e2f5a79d4 100644 --- a/frontend/src/metadata/components/metadata-details/ai-icon.js +++ b/frontend/src/metadata/components/metadata-details/ai-icon.js @@ -9,7 +9,6 @@ import { getFileNameFromRecord, getFileObjIdFromRecord, getParentDirFromRecord, import { getColumnByKey } from '../../utils/column'; import { PRIVATE_COLUMN_KEY } from './constants'; import { useMetadataAIOperations } from '../../../hooks/metadata-ai-operation'; -import FileTagsDialog from '../dialog/file-tags-dialog'; import { checkIsDir } from '../../utils/row'; const OPERATION = { @@ -17,17 +16,15 @@ const OPERATION = { OCR: 'ocr', FILE_TAGS: 'file-tags', FILE_DETAIL: 'file-detail', - EXTRACT_TEXT: 'extract-text', }; const AIIcon = () => { const [isMenuShow, setMenuShow] = useState(false); - const [isFileTagsDialogShow, setFileTagsDialogShow] = useState(false); - const { enableMetadata, enableTags } = useMetadataStatus(); - const { canModifyRecord, columns, record, onChange, onLocalRecordChange, updateFileTags } = useMetadataDetails(); - const { onOCR, generateDescription, extractFileDetails, extractText } = useMetadataAIOperations(); + const { enableMetadata, enableTags, enableOCR } = useMetadataStatus(); + const { canModifyRecord, columns, record, onChange, onLocalRecordChange, updateFileTags, updateDescription } = useMetadataDetails(); + const { generateDescription, extractFileDetails, onOCR, generateFileTags } = useMetadataAIOperations(); const options = useMemo(() => { if (!canModifyRecord || !record || checkIsDir(record)) return []; @@ -47,6 +44,10 @@ const AIIcon = () => { }); } + if (enableOCR && (isImage || isPdf)) { + list.push({ value: OPERATION.OCR, label: gettext('Extract text'), record }); + } + if (isImage || isVideo) { list.push({ value: OPERATION.FILE_DETAIL, label: gettext('Extract file detail'), record }); } @@ -55,11 +56,8 @@ const AIIcon = () => { list.push({ value: OPERATION.FILE_TAGS, label: gettext('Generate file tags'), record }); } - if (isImage || isPdf) { - list.push({ value: OPERATION.EXTRACT_TEXT, label: gettext('Extract text'), record }); - } return list; - }, [enableTags, canModifyRecord, columns, record]); + }, [enableTags, enableOCR, canModifyRecord, columns, record]); const onToggle = useCallback((event) => { event && event.preventDefault(); @@ -67,10 +65,6 @@ const AIIcon = () => { setMenuShow(!isMenuShow); }, [isMenuShow]); - const toggleFileTagsDialog = useCallback(() => { - setFileTagsDialogShow(!isFileTagsDialogShow); - }, [isFileTagsDialogShow]); - const handelOperation = useCallback((op) => { const { value: opType, record } = op; const recordId = getRecordIdFromRecord(record); @@ -89,16 +83,15 @@ const AIIcon = () => { break; } case OPERATION.OCR: { - onOCR({ parentDir, fileName }, { - success_callback: ({ ocrResult }) => { - if (!ocrResult) return; - onChange && onChange(PRIVATE_COLUMN_KEY.OCR, JSON.stringify(ocrResult)); - }, + onOCR(record, { + success_callback: updateDescription }); break; } case OPERATION.FILE_TAGS: { - setFileTagsDialogShow(true); + generateFileTags(record, { + success_callback: updateFileTags + }); break; } case OPERATION.FILE_DETAIL: { @@ -128,56 +121,37 @@ const AIIcon = () => { }); break; } - case OPERATION.EXTRACT_TEXT: { - extractText({ parentDir, fileName }, { - success_callback: ({ extractedText }) => { - console.log(extractedText) - }, - }); - break; - } default: { setMenuShow(false); break; } } - }, [columns, generateDescription, onOCR, extractFileDetails, onChange, onLocalRecordChange]); - - const renderDropdown = useCallback(() => { - if (!enableMetadata || !canModifyRecord || !record || options.length === 0) return null; - return ( - - -
- -
-
- {isMenuShow && ( -
- - {options.map(op => ( handelOperation(op)}>{op.label}))} - -
- )} -
- ); - }, [isMenuShow, enableMetadata, canModifyRecord, record, options, onToggle, handelOperation]); + }, [columns, generateDescription, onOCR, generateFileTags, extractFileDetails, onChange, onLocalRecordChange, updateFileTags, updateDescription]); + if (!enableMetadata || !canModifyRecord || !record || options.length === 0) return null; return ( - <> - {renderDropdown()} - {isFileTagsDialogShow && ( - + + +
+ +
+
+ {isMenuShow && ( +
+ + {options.map(op => ( handelOperation(op)}>{op.label}))} + +
)} - +
); }; diff --git a/frontend/src/metadata/components/metadata-details/constants.js b/frontend/src/metadata/components/metadata-details/constants.js index 7ab69780e3..eb8ec71264 100644 --- a/frontend/src/metadata/components/metadata-details/constants.js +++ b/frontend/src/metadata/components/metadata-details/constants.js @@ -22,7 +22,6 @@ export const NOT_DISPLAY_COLUMN_KEYS = [ PRIVATE_COLUMN_KEY.EXCLUDED_FACE_LINKS, PRIVATE_COLUMN_KEY.INCLUDED_FACE_LINKS, PRIVATE_COLUMN_KEY.FACE_VECTORS, - PRIVATE_COLUMN_KEY.OCR, PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED, ]; diff --git a/frontend/src/metadata/constants/column/common.js b/frontend/src/metadata/constants/column/common.js index ff0cd7d36b..a3772ec711 100644 --- a/frontend/src/metadata/constants/column/common.js +++ b/frontend/src/metadata/constants/column/common.js @@ -13,7 +13,6 @@ export const NOT_DISPLAY_COLUMN_KEYS = [ PRIVATE_COLUMN_KEY.EXCLUDED_FACE_LINKS, PRIVATE_COLUMN_KEY.INCLUDED_FACE_LINKS, PRIVATE_COLUMN_KEY.FACE_VECTORS, - PRIVATE_COLUMN_KEY.OCR, PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED, ]; diff --git a/frontend/src/metadata/constants/column/private.js b/frontend/src/metadata/constants/column/private.js index 98be13afd9..ed357757a0 100644 --- a/frontend/src/metadata/constants/column/private.js +++ b/frontend/src/metadata/constants/column/private.js @@ -40,9 +40,6 @@ export const PRIVATE_COLUMN_KEY = { // tag TAGS: '_tags', - // ocr - OCR: '_ocr', - // location LOCATION_TRANSLATED: '_location_translated' }; @@ -81,7 +78,6 @@ export const PRIVATE_COLUMN_KEYS = [ PRIVATE_COLUMN_KEY.FACE_VECTORS, PRIVATE_COLUMN_KEY.FILE_RATE, PRIVATE_COLUMN_KEY.TAGS, - PRIVATE_COLUMN_KEY.OCR, PRIVATE_COLUMN_KEY.LOCATION_TRANSLATED, ]; diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js index 91b9dc8c80..59451c5b9d 100644 --- a/frontend/src/metadata/constants/event-bus-type.js +++ b/frontend/src/metadata/constants/event-bus-type.js @@ -46,7 +46,6 @@ export const EVENT_BUS_TYPE = { UPDATE_RECORD_DETAILS: 'update_record_details', UPDATE_FACE_RECOGNITION: 'update_face_recognition', GENERATE_DESCRIPTION: 'generate_description', - EXTRACT_TEXT: 'extract_text', OCR: 'ocr', // metadata diff --git a/frontend/src/metadata/hooks/metadata-details.js b/frontend/src/metadata/hooks/metadata-details.js index 9e9d10ecca..1bdb197279 100644 --- a/frontend/src/metadata/hooks/metadata-details.js +++ b/frontend/src/metadata/hooks/metadata-details.js @@ -1,4 +1,6 @@ import React, { useContext, useEffect, useCallback, useState, useMemo, useRef } from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import metadataAPI from '../api'; import { Utils } from '../../utils/utils'; import toaster from '../../components/toast'; @@ -6,13 +8,18 @@ import { useMetadataStatus } from '../../hooks/metadata-status'; import { SYSTEM_FOLDERS } from '../../constants'; import Column from '../model/column'; import { normalizeFields } from '../components/metadata-details/utils'; -import { CellType, EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../constants'; -import { getCellValueByColumn, getColumnOptionNamesByIds, getColumnOptionNameById, getRecordIdFromRecord, getServerOptions } from '../utils/cell'; +import { CellType, EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY, UTC_FORMAT_DEFAULT } from '../constants'; +import { + getCellValueByColumn, getColumnOptionNamesByIds, getColumnOptionNameById, getRecordIdFromRecord, + getServerOptions, getFileNameFromRecord, getParentDirFromRecord +} from '../utils/cell'; import tagsAPI from '../../tag/api'; import { getColumnByKey, getColumnOptions, getColumnOriginName } from '../utils/column'; import ObjectUtils from '../../utils/object'; import { NOT_DISPLAY_COLUMN_KEYS } from '../components/metadata-details/constants'; +dayjs.extend(utc); + const MetadataDetailsContext = React.createContext(null); export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, direntDetail, direntType, modifyLocalFileTags, onErrMessage, children }) => { @@ -50,10 +57,16 @@ export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, dirent return [...exitColumnsOrder, ...newColumns]; }, [originColumns, detailsSettings]); - const onLocalRecordChange = useCallback((recordId, updates) => { - if (getRecordIdFromRecord(record) !== recordId) return; - const newRecord = { ...record, ...updates }; - setRecord(newRecord); + const onLocalRecordChange = useCallback(({ recordId, parentDir, fileName }, updates) => { + if (getRecordIdFromRecord(record) === recordId || (getParentDirFromRecord(record) === parentDir && getFileNameFromRecord(record) === fileName)) { + const newRecord = { + ...record, + [PRIVATE_COLUMN_KEY.MTIME]: dayjs().utc().format(UTC_FORMAT_DEFAULT), + [PRIVATE_COLUMN_KEY.LAST_MODIFIER]: window.sfMetadataContext.getUsername(), + ...updates, + }; + setRecord(newRecord); + } }, [record]); const onChange = useCallback((fieldKey, newValue) => { @@ -70,7 +83,7 @@ export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, dirent setRecord({ ...record, ...update }); if (window?.sfMetadataContext?.eventBus) { window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, { recordId }, update); - window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, recordId, update); + window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, { recordId }, update); } }).catch(error => { const errorMsg = Utils.getErrorMsg(error); @@ -134,6 +147,10 @@ export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, dirent }); }, [repoID, record, modifyLocalFileTags]); + const updateDescription = useCallback((description) => { + onChange(PRIVATE_COLUMN_KEY.FILE_DESCRIPTION, description); + }, [onChange]); + const saveColumns = useCallback((columns) => { modifyDetailsSettings && modifyDetailsSettings({ columns: columns.map(c => ({ key: c.key, shown: c.shown })) }); }, [modifyDetailsSettings]); @@ -219,6 +236,7 @@ export const MetadataDetailsProvider = ({ repoID, repoInfo, path, dirent, dirent updateFileTags, modifyHiddenColumns, modifyColumnOrder, + updateDescription, }} > {children} diff --git a/frontend/src/metadata/hooks/metadata-view.js b/frontend/src/metadata/hooks/metadata-view.js index 6f4abd67d8..d1ac21e0ff 100644 --- a/frontend/src/metadata/hooks/metadata-view.js +++ b/frontend/src/metadata/hooks/metadata-view.js @@ -38,7 +38,7 @@ export const MetadataViewProvider = ({ const { collaborators } = useCollaborators(); const { isBeingBuilt, setIsBeingBuilt } = useMetadata(); - const { onOCR, generateDescription, extractFilesDetails, faceRecognition, extractText } = useMetadataAIOperations(); + const { onOCR: OCRAPI, generateDescription, extractFilesDetails, faceRecognition, generateFileTags: generateFileTagsAPI } = useMetadataAIOperations(); const tableChanged = useCallback(() => { setMetadata(storeRef.current.data); @@ -381,38 +381,35 @@ export const MetadataViewProvider = ({ }); }, [modifyRecords, generateDescription]); - const ocr = useCallback((record) => { + const onOCR = useCallback((record) => { const parentDir = getParentDirFromRecord(record); const fileName = getFileNameFromRecord(record); - if (!Utils.imageCheck(fileName)) return; - - const ocrResultColumnKey = PRIVATE_COLUMN_KEY.OCR; - let idOldRecordData = { [record[PRIVATE_COLUMN_KEY.ID]]: { [ocrResultColumnKey]: record[ocrResultColumnKey] } }; - let idOriginalOldRecordData = { [record[PRIVATE_COLUMN_KEY.ID]]: { [ocrResultColumnKey]: record[ocrResultColumnKey] } }; - onOCR({ parentDir, fileName }, { - success_callback: ({ ocrResult }) => { - if (!ocrResult) return; + if (!fileName || !parentDir) return; + const descriptionColumnKey = PRIVATE_COLUMN_KEY.FILE_DESCRIPTION; + let idOldRecordData = { [record[PRIVATE_COLUMN_KEY.ID]]: { [descriptionColumnKey]: record[descriptionColumnKey] } }; + let idOriginalOldRecordData = { [record[PRIVATE_COLUMN_KEY.ID]]: { [descriptionColumnKey]: record[descriptionColumnKey] } }; + OCRAPI(record, { + success_callback: (description) => { + if (!description) return; const updateRecordId = record[PRIVATE_COLUMN_KEY.ID]; const recordIds = [updateRecordId]; let idRecordUpdates = {}; let idOriginalRecordUpdates = {}; - idRecordUpdates[updateRecordId] = { [ocrResultColumnKey]: ocrResult ? JSON.stringify(ocrResult) : null }; - idOriginalRecordUpdates[updateRecordId] = { [ocrResultColumnKey]: ocrResult ? JSON.stringify(ocrResult) : null }; + idRecordUpdates[updateRecordId] = { [descriptionColumnKey]: description || null }; + idOriginalRecordUpdates[updateRecordId] = { [descriptionColumnKey]: description || null }; modifyRecords(recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData); - }, + } }); - }, [modifyRecords, onOCR]); + }, [modifyRecords, OCRAPI]); - const updateExtractText = useCallback((record) => { + const generateFileTags = useCallback((record) => { const parentDir = getParentDirFromRecord(record); const fileName = getFileNameFromRecord(record); if (!fileName || !parentDir) return; - extractText({ parentDir, fileName }, { - success_callback: ({ extractedText }) => { - console.log(extractedText) - } + generateFileTagsAPI(record, { + success_callback: updateFileTags }); - }, [extractText]); + }, [updateFileTags, generateFileTagsAPI]); // init useEffect(() => { @@ -452,8 +449,7 @@ export const MetadataViewProvider = ({ const unsubscribeUpdateDetails = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_RECORD_DETAILS, updateRecordDetails); const unsubscribeUpdateFaceRecognition = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_FACE_RECOGNITION, updateFaceRecognition); const unsubscribeUpdateDescription = eventBus.subscribe(EVENT_BUS_TYPE.GENERATE_DESCRIPTION, updateRecordDescription); - const unsubscribeOCR = eventBus.subscribe(EVENT_BUS_TYPE.OCR, ocr); - const unsubscribeUpdateExtract = eventBus.subscribe(EVENT_BUS_TYPE.EXTRACT_TEXT, updateExtractText); + const unsubscribeOCR = eventBus.subscribe(EVENT_BUS_TYPE.OCR, onOCR); return () => { if (window.sfMetadataContext) { @@ -480,7 +476,6 @@ export const MetadataViewProvider = ({ unsubscribeUpdateFaceRecognition(); unsubscribeUpdateDescription(); unsubscribeOCR(); - unsubscribeUpdateExtract(); delayReloadDataTimer.current && clearTimeout(delayReloadDataTimer.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -520,8 +515,8 @@ export const MetadataViewProvider = ({ updateRecordDetails, updateFaceRecognition, updateRecordDescription, - updateExtractText, - ocr, + onOCR, + generateFileTags, }} > {children} diff --git a/frontend/src/metadata/utils/column/index.js b/frontend/src/metadata/utils/column/index.js index 25bd4b7c46..1911fbfe2e 100644 --- a/frontend/src/metadata/utils/column/index.js +++ b/frontend/src/metadata/utils/column/index.js @@ -193,8 +193,6 @@ export const getColumnDisplayName = (key, name) => { return gettext('Document keywords'); case PRIVATE_COLUMN_KEY.FILE_DESCRIPTION: return gettext('Description'); - case PRIVATE_COLUMN_KEY.OCR: - return gettext('OCR result'); case PRIVATE_COLUMN_KEY.FILE_EXPIRED: return gettext('Is expired'); case PRIVATE_COLUMN_KEY.FILE_STATUS: @@ -264,8 +262,6 @@ export const getNormalizedColumnType = (key, type) => { return CellType.TEXT; case PRIVATE_COLUMN_KEY.FILE_DESCRIPTION: return CellType.LONG_TEXT; - case PRIVATE_COLUMN_KEY.OCR: - return CellType.TEXT; case PRIVATE_COLUMN_KEY.FILE_EXPIRED: return CellType.CHECKBOX; case PRIVATE_COLUMN_KEY.FILE_STATUS: diff --git a/frontend/src/metadata/views/face-recognition/peoples/index.js b/frontend/src/metadata/views/face-recognition/peoples/index.js index dc60efdf29..3b2855715f 100644 --- a/frontend/src/metadata/views/face-recognition/peoples/index.js +++ b/frontend/src/metadata/views/face-recognition/peoples/index.js @@ -63,7 +63,7 @@ const Peoples = ({ peoples, onOpenPeople, onRename }) => { return () => {}; }, []); - if (!Array.isArray(peoples) || peoples.length === 0) return (); + if (!Array.isArray(peoples) || peoples.length === 0) return (); return (
diff --git a/frontend/src/metadata/views/kanban/index.js b/frontend/src/metadata/views/kanban/index.js index 042b5a3305..eb74f47f78 100644 --- a/frontend/src/metadata/views/kanban/index.js +++ b/frontend/src/metadata/views/kanban/index.js @@ -23,7 +23,7 @@ const Kanban = () => { modifyRecordAPI(rowId, updates, oldRowData, originalUpdates, originalOldRowData, false, { success_callback: () => { success_callback && success_callback(); const eventBus = window.sfMetadataContext.eventBus; - eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, rowId, updates); + eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_DETAIL_CHANGED, { recordId: rowId }, updates); } }); }, [modifyRecordAPI]); diff --git a/frontend/src/metadata/views/table/context-menu.js b/frontend/src/metadata/views/table/context-menu.js index 59a400ebf3..0d2037fffb 100644 --- a/frontend/src/metadata/views/table/context-menu.js +++ b/frontend/src/metadata/views/table/context-menu.js @@ -4,7 +4,7 @@ import { useMetadataStatus } from '@/hooks'; import { gettext } from '@/utils/constants'; import { Utils } from '@/utils/utils'; import DeleteFolderDialog from '@/components/dialog/delete-folder-dialog'; -import MoveDirent from '@/components/dialog/move-dirent-dialog'; +import MoveDirentDialog from '@/components/dialog/move-dirent-dialog'; import { Dirent } from '@/models'; import { useMetadataView } from '../../hooks/metadata-view'; import RowUtils from './utils/row-utils'; @@ -12,7 +12,6 @@ import { checkIsDir } from '../../utils/row'; import { getColumnByKey, isNameColumn } from '../../utils/column'; import { EVENT_BUS_TYPE, EVENT_BUS_TYPE as METADATA_EVENT_BUS_TYPE, PRIVATE_COLUMN_KEY } from '../../constants'; import { getFileNameFromRecord, getParentDirFromRecord, getRecordIdFromRecord } from '../../utils/cell'; -import FileTagsDialog from '../../components/dialog/file-tags-dialog'; import ContextMenuComponent from '../../components/context-menu'; import { openInNewTab, openParentFolder } from '../../utils/file'; @@ -33,19 +32,17 @@ const OPERATION = { FILE_DETAILS: 'file-details', DETECT_FACES: 'detect-faces', MOVE: 'move', - EXTRACT_TEXT: 'extract_text', }; const { enableSeafileAI } = window.app.config; const ContextMenu = ({ isGroupView, selectedRange, selectedPosition, recordMetrics, recordGetterByIndex, onClearSelected, onCopySelected, - getTableContentRect, getTableCanvasContainerRect, deleteRecords, selectNone, updateFileTags, moveRecord, addFolder, updateRecordDetails, - updateFaceRecognition, updateRecordDescription, ocr, updateExtractText + getTableContentRect, getTableCanvasContainerRect, deleteRecords, selectNone, moveRecord, addFolder, updateRecordDetails, + updateFaceRecognition, updateRecordDescription, onOCR, generateFileTags }) => { const currentRecord = useRef(null); - const [fileTagsRecord, setFileTagsRecord] = useState(null); const [deletedFolderPath, setDeletedFolderPath] = useState(''); const [isMoveDialogShow, setMoveDialogShow] = useState(false); @@ -230,16 +227,12 @@ const ContextMenu = ({ }); } - if (tagsColumn && isDescribableFile && !isVideo) { + if (enableSeafileAI && tagsColumn && isDescribableFile && !isVideo) { aiOptions.push({ value: OPERATION.FILE_TAGS, label: gettext('Generate file tags'), record: record }); } - if (enableOCR && isImage) { - aiOptions.push({ value: OPERATION.OCR, label: gettext('OCR'), record }); - } - - if (isImage || isPdf) { - aiOptions.push({ value: OPERATION.EXTRACT_TEXT, label: gettext('Extract text'), record }); + if (enableSeafileAI && enableOCR && (isImage || isPdf)) { + aiOptions.push({ value: OPERATION.OCR, label: gettext('Extract text'), record }); } if (aiOptions.length > 0) { @@ -251,10 +244,6 @@ const ContextMenu = ({ return list; }, [isGroupView, selectedPosition, recordMetrics, selectedRange, metadata, recordGetterByIndex, checkIsDescribableFile, enableOCR, getAbleDeleteRecords]); - const toggleFileTagsRecord = useCallback((record = null) => { - setFileTagsRecord(record); - }, []); - const handleMoveRecord = useCallback((...params) => { selectNone(); moveRecord && moveRecord(...params); @@ -290,18 +279,13 @@ const ContextMenu = ({ case OPERATION.FILE_TAGS: { const { record } = option; if (!record) break; - toggleFileTagsRecord(record); + generateFileTags(record); break; } case OPERATION.OCR: { const { record } = option; if (!record) break; - ocr(record); - break; - } - case OPERATION.EXTRACT_TEXT: { - const { record } = option; - updateExtractText(record) + onOCR(record); break; } case OPERATION.DELETE_RECORD: { @@ -357,7 +341,7 @@ const ContextMenu = ({ break; } } - }, [repoID, onCopySelected, onClearSelected, updateRecordDescription, toggleFileTagsRecord, ocr, deleteRecords, toggleDeleteFolderDialog, selectNone, updateRecordDetails, updateFaceRecognition, toggleMoveDialog, updateExtractText]); + }, [repoID, onCopySelected, onClearSelected, updateRecordDescription, generateFileTags, onOCR, deleteRecords, toggleDeleteFolderDialog, selectNone, updateRecordDetails, updateFaceRecognition, toggleMoveDialog]); useEffect(() => { const unsubscribeToggleMoveDialog = window.sfMetadataContext.eventBus.subscribe(EVENT_BUS_TYPE.TOGGLE_MOVE_DIALOG, toggleMoveDialog); @@ -380,9 +364,6 @@ const ContextMenu = ({ boundaryCoordinates={{ top, left, right, bottom }} onOptionClick={handleOptionClick} /> - {fileTagsRecord && ( - - )} {deletedFolderPath && ( )} {isMoveDialogShow && ( - { updateRecordDetails, updateFaceRecognition, updateRecordDescription, - updateExtractText, - ocr, + onOCR, + generateFileTags, } = useMetadataView(); const containerRef = useRef(null); @@ -178,6 +178,7 @@ const Table = () => { modifyColumnWidth={modifyColumnWidth} modifyColumnOrder={modifyColumnOrder} updateFileTags={updateFileTags} + generateFileTags={generateFileTags} onGridKeyDown={onHotKey} onGridKeyUp={onHotKeyUp} moveRecord={moveRecord} @@ -186,8 +187,7 @@ const Table = () => { updateRecordDetails={updateRecordDetails} updateFaceRecognition={updateFaceRecognition} updateRecordDescription={updateRecordDescription} - updateExtractText={updateExtractText} - ocr={ocr} + onOCR={onOCR} />
); diff --git a/frontend/src/metadata/views/table/masks/interaction-masks/index.js b/frontend/src/metadata/views/table/masks/interaction-masks/index.js index b6a6714cad..0ff2b8e965 100644 --- a/frontend/src/metadata/views/table/masks/interaction-masks/index.js +++ b/frontend/src/metadata/views/table/masks/interaction-masks/index.js @@ -1224,7 +1224,7 @@ class InteractionMasks extends React.Component { onCopySelected: this.onCopySelected, getTableContentRect: this.props.getTableContentRect, getTableCanvasContainerRect: this.props.getTableCanvasContainerRect, - updateFileTags: this.props.updateFileTags, + generateFileTags: this.props.generateFileTags, })} ); diff --git a/frontend/src/metadata/views/table/table-main/records/body.js b/frontend/src/metadata/views/table/table-main/records/body.js index e3ded436d6..7f84ef6b5a 100644 --- a/frontend/src/metadata/views/table/table-main/records/body.js +++ b/frontend/src/metadata/views/table/table-main/records/body.js @@ -561,6 +561,7 @@ class RecordsBody extends Component { updateFileTags={this.props.updateFileTags} deleteRecords={this.props.deleteRecords} moveRecord={this.props.moveRecord} + generateFileTags={this.props.generateFileTags} />
{this.renderRecords()} diff --git a/frontend/src/metadata/views/table/table-main/records/group-body/index.js b/frontend/src/metadata/views/table/table-main/records/group-body/index.js index 60e17304b6..c8819ff890 100644 --- a/frontend/src/metadata/views/table/table-main/records/group-body/index.js +++ b/frontend/src/metadata/views/table/table-main/records/group-body/index.js @@ -915,6 +915,7 @@ class GroupBody extends Component { modifyColumnData={this.props.modifyColumnData} getTableCanvasContainerRect={this.props.getTableCanvasContainerRect} updateFileTags={this.props.updateFileTags} + generateFileTags={this.props.generateFileTags} />
{this.renderGroups()} diff --git a/frontend/src/metadata/views/table/table-main/records/index.js b/frontend/src/metadata/views/table/table-main/records/index.js index 5b7834c96e..a9d8767f6a 100644 --- a/frontend/src/metadata/views/table/table-main/records/index.js +++ b/frontend/src/metadata/views/table/table-main/records/index.js @@ -648,8 +648,7 @@ class Records extends Component { updateRecordDetails={this.props.updateRecordDetails} updateFaceRecognition={this.props.updateFaceRecognition} updateRecordDescription={this.props.updateRecordDescription} - ocr={this.props.ocr} - updateExtractText={this.props.updateExtractText} + onOCR={this.props.onOCR} /> ), hasSelectedRecord: this.hasSelectedRecord(), diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 5a8674259a..44fe54c2e7 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -23,9 +23,8 @@ import CopyMoveDirentProgressDialog from '../../components/dialog/copy-move-dire import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog'; import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type'; import { PRIVATE_FILE_TYPE, DIRENT_DETAIL_SHOW_KEY, TREE_PANEL_STATE_KEY, RECENTLY_USED_LIST_KEY } from '../../constants'; -import { MetadataStatusProvider, DownloadFileProvider } from '../../hooks'; -import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks'; -import { TagsProvider } from '../../tag/hooks'; +import { MetadataStatusProvider, DownloadFileProvider, MetadataMiddlewareProvider } from '../../hooks'; +import { MetadataProvider } from '../../metadata/hooks'; import { LIST_MODE, METADATA_MODE, TAGS_MODE } from '../../components/dir-view-mode/constants'; import CurDirPath from '../../components/cur-dir-path'; import DirTool from '../../components/cur-dir-path/dir-tool'; @@ -2360,236 +2359,234 @@ class LibContentView extends React.Component { - + - -
-
- {this.state.currentRepoInfo.status === 'read-only' && -
- {gettext('This library has been set to read-only by admin and cannot be updated.')} -
- } -
-
- {isDirentSelected ? ( - currentMode === TAGS_MODE || currentMode === METADATA_MODE ? ( - - ) : ( - - ) +
+
+ {this.state.currentRepoInfo.status === 'read-only' && +
+ {gettext('This library has been set to read-only by admin and cannot be updated.')} +
+ } +
+
+ {isDirentSelected ? ( + currentMode === TAGS_MODE || currentMode === METADATA_MODE ? ( + ) : ( - - )} -
- {isDesktop && -
- -
- } -
-
- {this.state.pathExist ? - - : -
{gettext('Folder does not exist.')}
- } - {!isCustomPermission && this.state.isDirentDetailShow && ( - )}
+ {isDesktop && +
+ +
+ } +
+
+ {this.state.pathExist ? + + : +
{gettext('Folder does not exist.')}
+ } + {!isCustomPermission && this.state.isDirentDetailShow && ( + + )}
- {canUpload && this.state.pathExist && !this.state.isViewFile && ![METADATA_MODE, TAGS_MODE].includes(this.state.currentMode) && ( - this.uploader = uploader} - dragAndDrop={true} - path={this.state.path} - repoID={this.props.repoID} - direntList={this.state.direntList} - onFileUploadSuccess={this.onFileUploadSuccess} - isCustomPermission={isCustomPermission} - /> - )}
- {isCopyMoveProgressDialogShow && ( - - )} - {isDeleteFolderDialogOpen && ( - this.uploader = uploader} + dragAndDrop={true} + path={this.state.path} repoID={this.props.repoID} - path={this.state.folderToDelete} - deleteFolder={this.deleteFolder} - toggleDialog={this.toggleDeleteFolderDialog} + direntList={this.state.direntList} + onFileUploadSuccess={this.onFileUploadSuccess} + isCustomPermission={isCustomPermission} /> )} - - - - +
+ {isCopyMoveProgressDialogShow && ( + + )} + {isDeleteFolderDialogOpen && ( + + )} + + + - + diff --git a/frontend/src/tag/views/tag-files/index.js b/frontend/src/tag/views/tag-files/index.js index 8eaa14d4b3..75cfbdba32 100644 --- a/frontend/src/tag/views/tag-files/index.js +++ b/frontend/src/tag/views/tag-files/index.js @@ -5,7 +5,7 @@ import { gettext, username } from '../../../utils/constants'; import EmptyTip from '../../../components/empty-tip'; import toaster from '../../../components/toast'; import ContextMenu from '../../../components/context-menu/context-menu'; -import MoveDirent from '../../../components/dialog/move-dirent-dialog'; +import MoveDirentDialog from '../../../components/dialog/move-dirent-dialog'; import CopyDirent from '../../../components/dialog/copy-dirent-dialog'; import ZipDownloadDialog from '../../../components/dialog/zip-download-dialog'; import ShareDialog from '../../../components/dialog/share-dialog'; @@ -464,7 +464,7 @@ const TagFiles = () => { getMenuContainerSize={getMenuContainerSize} /> {isMoveDialogOpen && ( - }> - - - {filePerm === 'rw' ? : } - - + + {filePerm === 'rw' ? : } + diff --git a/seahub/ai/apis.py b/seahub/ai/apis.py index 90778e898e..52f8f0fc48 100644 --- a/seahub/ai/apis.py +++ b/seahub/ai/apis.py @@ -16,7 +16,7 @@ from seahub.api2.authentication import TokenAuthentication, SdocJWTTokenAuthenti from seahub.utils import get_file_type_and_ext, IMAGE from seahub.views import check_folder_permission from seahub.ai.utils import image_caption, translate, writing_assistant, verify_ai_config, generate_summary, \ - generate_file_tags, ocr, extract_text + generate_file_tags, ocr logger = logging.getLogger(__name__) @@ -226,7 +226,7 @@ class OCR(APIView): def post(self, request): if not verify_ai_config(): - return api_error(status.HTTP_400_BAD_REQUEST, 'OCR server not configured') + return api_error(status.HTTP_400_BAD_REQUEST, 'AI server not configured') repo_id = request.data.get('repo_id') path = request.data.get('path') @@ -236,18 +236,15 @@ class OCR(APIView): if not path: return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid') + file_type, _ = get_file_type_and_ext(os.path.basename(path)) + if file_type != IMAGE and not path.lower().endswith('.pdf'): + return api_error(status.HTTP_400_BAD_REQUEST, 'file type not image or pdf') + repo = seafile_api.get_repo(repo_id) if not repo: error_msg = 'Library %s not found.' % repo_id return api_error(status.HTTP_404_NOT_FOUND, error_msg) - try: - record = RepoMetadata.objects.filter(repo_id=repo_id).first() - except Exception as e: - logger.error(e) - error_msg = 'Internal Server Error' - return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - permission = check_folder_permission(request, repo_id, os.path.dirname(path)) if not permission: error_msg = 'Permission denied.' @@ -262,20 +259,26 @@ class OCR(APIView): if not file_id: return api_error(status.HTTP_404_NOT_FOUND, f"File {path} not found") + file_size = get_file_size(repo.store_id, repo.version, file_id) + if file_size >> 20 > 5: + error_msg = 'File size exceed the limit.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + token = seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', request.user.username, use_onetime=True) if not token: error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) params = { - 'path': path, - 'download_token': token + 'file_name': os.path.basename(path), + 'download_token': token, } try: resp = ocr(params) resp_json = resp.json() except Exception as e: + logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) @@ -348,68 +351,3 @@ class WritingAssistant(APIView): return Response(resp_json, resp.status_code) - -class ExtractText(APIView): - authentication_classes = (TokenAuthentication, SessionAuthentication) - permission_classes = (IsAuthenticated,) - throttle_classes = (UserRateThrottle,) - - def post(self, request): - if not verify_ai_config(): - return api_error(status.HTTP_400_BAD_REQUEST, 'AI server not configured') - - repo_id = request.data.get('repo_id') - path = request.data.get('path') - - if not repo_id: - return api_error(status.HTTP_400_BAD_REQUEST, 'repo_id invalid') - if not path: - return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid') - - file_type, _ = get_file_type_and_ext(os.path.basename(path)) - if file_type != IMAGE and not path.lower().endswith('.pdf'): - return api_error(status.HTTP_400_BAD_REQUEST, 'file type not image or pdf') - - repo = seafile_api.get_repo(repo_id) - if not repo: - error_msg = 'Library %s not found.' % repo_id - return api_error(status.HTTP_404_NOT_FOUND, error_msg) - - permission = check_folder_permission(request, repo_id, os.path.dirname(path)) - if not permission: - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) - - try: - file_id = seafile_api.get_file_id_by_path(repo_id, path) - except SearpcError as e: - logger.error(e) - return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') - - if not file_id: - return api_error(status.HTTP_404_NOT_FOUND, f"File {path} not found") - - file_size = get_file_size(repo.store_id, repo.version, file_id) - if file_size >> 20 > 5: - error_msg = 'File size exceed the limit.' - return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - - token = seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', request.user.username, use_onetime=True) - if not token: - error_msg = 'Internal Server Error' - return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - - params = { - 'file_name': os.path.basename(path), - 'download_token': token, - } - - try: - resp = extract_text(params) - resp_json = resp.json() - except Exception as e: - logger.error(e) - error_msg = 'Internal Server Error' - return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - - return Response(resp_json, resp.status_code) diff --git a/seahub/ai/urls.py b/seahub/ai/urls.py new file mode 100644 index 0000000000..ad1b1a85fb --- /dev/null +++ b/seahub/ai/urls.py @@ -0,0 +1,12 @@ +from django.urls import re_path +from .apis import ImageCaption, GenerateSummary, GenerateFileTags, OCR, Translate, WritingAssistant + +urlpatterns = [ + re_path(r'^image-caption/$', ImageCaption.as_view(), name='api-v2.1-image-caption'), + re_path(r'^generate-file-tags/$', GenerateFileTags.as_view(), name='api-v2.1-generate-file-tags'), + re_path(r'^generate-summary/$', GenerateSummary.as_view(), name='api-v2.1-generate-summary'), + re_path(r'^ocr/$', OCR.as_view(), name='api-v2.1-ocr'), + re_path(r'^translate/$', Translate.as_view(), name='api-v2.1-translate'), + re_path(r'^writing-assistant/$', WritingAssistant.as_view(), name='api-v2.1-writing-assistant'), +] + diff --git a/seahub/repo_metadata/apis.py b/seahub/repo_metadata/apis.py index 6645586245..1b73641ab9 100644 --- a/seahub/repo_metadata/apis.py +++ b/seahub/repo_metadata/apis.py @@ -17,8 +17,8 @@ from seahub.views import check_folder_permission from seahub.repo_metadata.utils import add_init_metadata_task, recognize_faces, gen_unique_id, init_metadata, \ get_unmodifiable_columns, can_read_metadata, init_faces, \ extract_file_details, get_table_by_name, remove_faces_table, FACES_SAVE_PATH, \ - init_tags, init_tag_self_link_columns, remove_tags_table, add_init_face_recognition_task, init_ocr, \ - remove_ocr_column, get_update_record, update_people_cover_photo + init_tags, init_tag_self_link_columns, remove_tags_table, add_init_face_recognition_task, \ + get_update_record, update_people_cover_photo from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records from seahub.utils.repo import is_repo_admin from seaserv import seafile_api @@ -249,9 +249,6 @@ class MetadataOCRManageView(APIView): logger.exception(e) return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') - metadata_server_api = MetadataServerAPI(repo_id, request.user.username) - init_ocr(metadata_server_api) - return Response({'success': True}) def delete(self, request, repo_id): @@ -272,14 +269,6 @@ class MetadataOCRManageView(APIView): error_msg = f'The repo {repo_id} has disabled the OCR.' return api_error(status.HTTP_409_CONFLICT, error_msg) - metadata_server_api = MetadataServerAPI(repo_id, request.user.username) - try: - remove_ocr_column(metadata_server_api) - except Exception as err: - logger.error(err) - error_msg = 'Internal Server Error' - return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - try: record.ocr_enabled = False record.save() diff --git a/seahub/repo_metadata/metadata_server_api.py b/seahub/repo_metadata/metadata_server_api.py index b5b6f78137..5e809b8820 100644 --- a/seahub/repo_metadata/metadata_server_api.py +++ b/seahub/repo_metadata/metadata_server_api.py @@ -67,8 +67,6 @@ def list_metadata_view_records(repo_id, user, view, tags_enabled, start=0, limit column_name = column.get('name') if column_name == METADATA_TABLE.columns.face_vectors.name: continue - elif column_name == METADATA_TABLE.columns.ocr.name: - continue column_name_str = '`%s`, ' % column_name query_fields_str += column_name_str query_fields_str = query_fields_str.strip(', ') diff --git a/seahub/repo_metadata/utils.py b/seahub/repo_metadata/utils.py index aed44b8d66..1a1f258b50 100644 --- a/seahub/repo_metadata/utils.py +++ b/seahub/repo_metadata/utils.py @@ -273,32 +273,6 @@ def remove_tags_table(metadata_server_api): metadata_server_api.delete_column(table['id'], column['key'], True) -# ocr -def init_ocr(metadata_server_api): - from seafevents.repo_metadata.constants import METADATA_TABLE - - remove_ocr_column(metadata_server_api) - - # init ocr column - columns = [ - METADATA_TABLE.columns.ocr.to_dict(), - ] - metadata_server_api.add_columns(METADATA_TABLE.id, columns) - - -def remove_ocr_column(metadata_server_api): - from seafevents.repo_metadata.constants import METADATA_TABLE - metadata = metadata_server_api.get_metadata() - - tables = metadata.get('tables', []) - for table in tables: - if table['name'] == METADATA_TABLE.name: - columns = table.get('columns', []) - for column in columns: - if column['key'] == METADATA_TABLE.columns.ocr.key: - metadata_server_api.delete_column(table['id'], METADATA_TABLE.columns.ocr.key, True) - - def get_file_download_token(repo_id, file_id, username): return seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', username, use_onetime=True) diff --git a/seahub/urls.py b/seahub/urls.py index caf88f08f2..b70fe6eebb 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -2,8 +2,6 @@ from django.urls import include, path, re_path from django.views.generic import TemplateView -from seahub.ai.apis import ImageCaption, GenerateSummary, GenerateFileTags, OCR, Translate, WritingAssistant, \ - ExtractText from seahub.api2.endpoints.file_comments import FileCommentsView, FileCommentView, FileCommentRepliesView, \ FileCommentReplyView from seahub.api2.endpoints.share_link_auth import ShareLinkUserAuthView, ShareLinkEmailAuthView @@ -1080,13 +1078,8 @@ if getattr(settings, 'ENABLE_METADATA_MANAGEMENT', False): re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/metadata/', include('seahub.repo_metadata.urls')), ] -# ai API -urlpatterns += [ - re_path(r'^api/v2.1/ai/image-caption/$', ImageCaption.as_view(), name='api-v2.1-image-caption'), - re_path(r'^api/v2.1/ai/generate-file-tags/$', GenerateFileTags.as_view(), name='api-v2.1-generate-file-tags'), - re_path(r'^api/v2.1/ai/generate-summary/$', GenerateSummary.as_view(), name='api-v2.1-generate-summary'), - re_path(r'^api/v2.1/ai/ocr/$', OCR.as_view(), name='api-v2.1-ocr'), - re_path(r'^api/v2.1/ai/translate/$', Translate.as_view(), name='api-v2.1-translate'), - re_path(r'^api/v2.1/ai/writing-assistant/$', WritingAssistant.as_view(), name='api-v2.1-writing-assistant'), - re_path(r'^api/v2.1/ai/extract-text/$', ExtractText.as_view(), name='api-v2.1-extract-text'), -] + if getattr(settings, 'ENABLE_SEAFILE_AI', False): + urlpatterns += [ + re_path(r'^api/v2.1/ai/', include('seahub.ai.urls')), + ] +