diff --git a/frontend/src/components/search/search.js b/frontend/src/components/search/search.js index 31c8a7c60b5..595c4bf1b5d 100644 --- a/frontend/src/components/search/search.js +++ b/frontend/src/components/search/search.js @@ -3,17 +3,30 @@ import PropTypes from 'prop-types'; import isHotkey from 'is-hotkey'; import MediaQuery from 'react-responsive'; import { seafileAPI } from '../../utils/seafile-api'; -import { gettext, siteRoot, username } from '../../utils/constants'; +import { gettext, siteRoot, username, enableSeafileAI } from '../../utils/constants'; import SearchResultItem from './search-result-item'; import { Utils } from '../../utils/utils'; import { isMac } from '../../utils/extra-attributes'; import toaster from '../toast'; +const INDEX_STATE = { + RUNNING: 'running', + UNCREATED: 'uncreated', + FINISHED: 'finished' +}; + +const SEARCH_MODE = { + SIMILARITY: 'similarity', + NORMAL: 'normal', +}; + const propTypes = { repoID: PropTypes.string, placeholder: PropTypes.string, onSearchedClick: PropTypes.func.isRequired, isPublic: PropTypes.bool, + isLibView: PropTypes.bool, + repoName: PropTypes.string, }; const PER_PAGE = 10; @@ -37,7 +50,9 @@ class Search extends Component { isResultGetted: false, isCloseShow: false, isSearchInputShow: false, // for mobile - searchPageUrl: this.baseSearchPageURL + searchPageUrl: this.baseSearchPageURL, + searchMode: SEARCH_MODE.NORMAL, + indexState: INDEX_STATE.UNCREATED, }; this.inputValue = ''; this.highlightRef = null; @@ -215,32 +230,71 @@ class Search extends Component { this.updateSearchPageURL(queryData); queryData['per_page'] = PER_PAGE; queryData['page'] = page; - seafileAPI.searchFiles(queryData, cancelToken).then(res => { - this.source = null; - if (res.data.total > 0) { - this.setState({ - resultItems: [...this.state.resultItems, ...this.formatResultItems(res.data.results)], - isResultGetted: true, - isLoading: false, - page: page + 1, - hasMore: res.data.has_more, - }); - } else { - this.setState({ - highlightIndex: 0, - resultItems: [], - isLoading: false, - isResultGetted: true, - hasMore: res.data.has_more, - }); - } - }).catch(error => { - /* eslint-disable */ - console.log(error); - /* eslint-enable */ - this.setState({ isLoading: false }); - }); + if (this.state.searchMode === SEARCH_MODE.NORMAL) { + this.onNarmalSearch(queryData, cancelToken, page); + } else { + this.onSimilaritySearch(queryData, cancelToken, page); + } + } + }; + + onNarmalSearch = (queryData, cancelToken, page) => { + seafileAPI.searchFiles(queryData, cancelToken).then(res => { + this.source = null; + if (res.data.total > 0) { + this.setState({ + resultItems: [...this.state.resultItems, ...this.formatResultItems(res.data.results)], + isResultGetted: true, + isLoading: false, + page: page + 1, + hasMore: res.data.has_more, + }); + } else { + this.setState({ + highlightIndex: 0, + resultItems: [], + isLoading: false, + isResultGetted: true, + hasMore: res.data.has_more, + }); + } + }).catch(error => { + /* eslint-disable */ + console.log(error); + /* eslint-enable */ + this.setState({ isLoading: false }); + }); + }; + + onSimilaritySearch = (queryData, cancelToken, page) => { + if (this.state.indexState !== INDEX_STATE.FINISHED) { + toaster.danger(gettext('Please create index first.')); } + seafileAPI.similaritySearchFiles(queryData, cancelToken).then(res => { + this.source = null; + if (res.data && res.data.children_list.length > 0) { + this.setState({ + resultItems: [...this.state.resultItems, ...this.formatSimilarityItems(res.data.children_list)], + isResultGetted: true, + isLoading: false, + page: page + 1, + hasMore: res.data.has_more, + }); + } else { + this.setState({ + highlightIndex: 0, + resultItems: [], + isLoading: false, + isResultGetted: true, + hasMore: res.data.has_more, + }); + } + }).catch(error => { + /* eslint-disable */ + console.log(error); + /* eslint-enable */ + this.setState({ isLoading: false }); + }); }; onResultListScroll = (e) => { @@ -299,6 +353,24 @@ class Search extends Component { return items; } + formatSimilarityItems(data) { + let items = []; + let repo_id = this.props.repoID; + for (let i = 0; i < data.length; i++) { + items[i] = {}; + items[i]['index'] = [i]; + items[i]['name'] = data[i].path.substring(data[i].path.lastIndexOf('/')+1); + items[i]['path'] = data[i].path; + items[i]['repo_id'] = repo_id; + items[i]['repo_name'] = this.props.repoName; + items[i]['is_dir'] = false; + items[i]['link_content'] = decodeURI(data[i].path).substring(1); + items[i]['content'] = data[i].sentence; + items[i]['thumbnail_url'] = ''; + } + return items; + } + resetToDefault() { this.inputValue = null; this.setState({ @@ -354,10 +426,55 @@ class Search extends Component { }); }; + onChangeSearchMode = (searchMode) => { + this.setState({ + searchMode: searchMode + }); + + if (searchMode === SEARCH_MODE.SIMILARITY && this.state.indexState === INDEX_STATE.UNCREATED) { + this.libraryIndexState(); + } + }; + + libraryIndexState = () => { + seafileAPI.queryLibraryIndexState(this.props.repoID).then(res => { + this.setState({indexState: res.data.state}); + }).catch(error => { + /* eslint-disable */ + console.log(error); + /* eslint-enable */ + }); + }; + + onCreateIndex = () => { + this.setState({ indexState: INDEX_STATE.RUNNING }); + seafileAPI.createLibraryIndex(this.props.repoID).then(res => { + const taskId = res.data.task_id; + this.timer = setInterval(() => { + seafileAPI.queryIndexTaskStatus(taskId).then(res => { + const is_finished = res.data.is_finished; + if (is_finished) { + this.setState({ indexState: INDEX_STATE.FINISHED }); + this.timer && clearInterval(this.timer); + this.timer = null; + } + }).catch(error => { + this.timer && clearInterval(this.timer); + this.timer = null; + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + }, 3000); + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + }; + render() { let width = this.state.width !== 'default' ? this.state.width : ''; let style = {'width': width}; - const { searchPageUrl, isMaskShow } = this.state; + const { searchPageUrl, isMaskShow, searchMode, indexState } = this.state; const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`; return ( @@ -391,6 +508,15 @@ class Search extends Component { onScroll={this.onResultListScroll} ref={this.searchContainer} > + {this.state.isCloseShow && enableSeafileAI && this.props.isLibView && +
+ + + {searchMode === SEARCH_MODE.SIMILARITY && + + } +
+ } {this.renderSearchResult()} diff --git a/frontend/src/components/toolbar/common-toolbar.js b/frontend/src/components/toolbar/common-toolbar.js index 9d91c56cc3c..e2caf54d052 100644 --- a/frontend/src/components/toolbar/common-toolbar.js +++ b/frontend/src/components/toolbar/common-toolbar.js @@ -26,6 +26,8 @@ class CommonToolbar extends React.Component { repoID={repoID} placeholder={this.props.searchPlaceholder || gettext('Search files')} onSearchedClick={this.props.onSearchedClick} + isLibView={this.props.isLibView} + repoName={repoName} /> )} {this.props.isLibView && !isPro && diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index e348c31e237..c80fba907db 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -94,6 +94,9 @@ export const enableVideoThumbnail = window.app.pageOptions.enableVideoThumbnail; export const enableOnlyoffice = window.app.pageOptions.enableOnlyoffice || false; export const onlyofficeConverterExtensions = window.app.pageOptions.onlyofficeConverterExtensions || []; +// seafile_ai +export const enableSeafileAI = window.app.pageOptions.enableSeafileAI || false; + // dtable export const workspaceID = window.app.pageOptions.workspaceID; export const showLogoutIcon = window.app.pageOptions.showLogoutIcon; diff --git a/seahub/ai/apis.py b/seahub/ai/apis.py index 0d3b802e718..4ff2f7ef5c8 100644 --- a/seahub/ai/apis.py +++ b/seahub/ai/apis.py @@ -15,7 +15,8 @@ from seahub.views import check_folder_permission from seahub.utils.repo import parse_repo_perm from seahub.ai.utils import create_library_sdoc_index, get_dir_file_recursively, similarity_search_in_library, \ - update_library_sdoc_index, delete_library_index, query_task_status, get_dir_sdoc_info_list + update_library_sdoc_index, delete_library_index, query_task_status, get_dir_sdoc_info_list, \ + query_library_index_state from seaserv import seafile_api @@ -218,6 +219,29 @@ def get(self, request): return Response(resp_json, resp.status_code) +class LibraryIndexState(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def get(self, request): + repo_id = request.GET.get('repo_id') + + if not repo_id: + return api_error(status.HTTP_400_BAD_REQUEST, 'repo_id invalid') + try: + resp = query_library_index_state(repo_id) + if resp.status_code == 500: + logger.error('query library index state error status: %s body: %s', resp.status_code, resp.text) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') + resp_json = resp.json() + except Exception as e: + logger.error(e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') + + return Response(resp_json, resp.status_code) + + class RepoFiles(APIView): authentication_classes = (SeafileAiAuthentication, ) throttle_classes = (UserRateThrottle, ) diff --git a/seahub/ai/utils.py b/seahub/ai/utils.py index dc478239fc1..dc5e8f70de7 100644 --- a/seahub/ai/utils.py +++ b/seahub/ai/utils.py @@ -100,3 +100,10 @@ def query_task_status(task_id): url = urljoin(SEAFILE_AI_SERVER_URL, '/api/v1/task-status/') resp = requests.get(url, headers=headers, params={'task_id': task_id}) return resp + + +def query_library_index_state(associate_id): + headers = gen_headers() + url = urljoin(SEAFILE_AI_SERVER_URL, '/api/v1/library-index-state/') + resp = requests.get(url, headers=headers, params={'associate_id': associate_id}) + return resp diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 22dfc4c4e4f..85258196847 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -24,7 +24,7 @@ MEDIA_ROOT, SHOW_LOGOUT_ICON, CUSTOM_LOGO_PATH, CUSTOM_FAVICON_PATH, \ ENABLE_SEAFILE_DOCS, LOGIN_BG_IMAGE_PATH, \ CUSTOM_LOGIN_BG_PATH, ENABLE_SHARE_LINK_REPORT_ABUSE, \ - PRIVACY_POLICY_LINK, TERMS_OF_SERVICE_LINK, ENABLE_SEADOC + PRIVACY_POLICY_LINK, TERMS_OF_SERVICE_LINK, ENABLE_SEADOC, ENABLE_SEAFILE_AI from seahub.organizations.models import OrgAdminSettings from seahub.organizations.settings import ORG_ENABLE_ADMIN_CUSTOM_LOGO @@ -165,7 +165,8 @@ def base(request): 'side_nav_footer_custom_html': SIDE_NAV_FOOTER_CUSTOM_HTML, 'about_dialog_custom_html': ABOUT_DIALOG_CUSTOM_HTML, 'enable_repo_auto_del': ENABLE_REPO_AUTO_DEL, - 'enable_seadoc': ENABLE_SEADOC + 'enable_seadoc': ENABLE_SEADOC, + 'enable_seafile_ai': ENABLE_SEAFILE_AI, } if request.user.is_staff: diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index d200ee81bfb..166e4d17ced 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -145,6 +145,7 @@ enableOnlyoffice: {% if enableOnlyoffice %} true {% else %} false {% endif %}, onlyofficeConverterExtensions: {% if onlyofficeConverterExtensions %} {{onlyofficeConverterExtensions|safe}} {% else %} null {% endif %}, enableSeadoc: {% if enable_seadoc %} true {% else %} false {% endif %}, + enableSeafileAI: {% if enable_seafile_ai %} true {% else %} false {% endif %}, } }; diff --git a/seahub/urls.py b/seahub/urls.py index 120c72aad03..db683eaeb96 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -201,7 +201,8 @@ from seahub.ocm.settings import OCM_ENDPOINT -from seahub.ai.apis import LibrarySdocIndexes, SimilaritySearchInLibrary, LibrarySdocIndex, RepoFiles, TaskStatus +from seahub.ai.apis import LibrarySdocIndexes, SimilaritySearchInLibrary, LibrarySdocIndex, RepoFiles, TaskStatus, \ + LibraryIndexState urlpatterns = [ path('accounts/', include('seahub.base.registration_urls')), @@ -535,13 +536,6 @@ re_path(r'api/v2.1/ocm/providers/(?P[-0-9a-f]{36})/repos/(?P[-0-9a-f]{36})/download-link/$', OCMReposDownloadLinkView.as_view(), name='api-v2.1-ocm-repos-download-link'), re_path(r'api/v2.1/ocm/providers/(?P[-0-9a-f]{36})/repos/(?P[-0-9a-f]{36})/upload-link/$', OCMReposUploadLinkView.as_view(), name='api-v2.1-ocm-repos-upload-link'), - # seafile-ai - re_path(r'^api/v2.1/ai/library-sdoc-indexes/$', LibrarySdocIndexes.as_view(), name='api-v2.1-ai-library-sdoc-indexes'), - re_path(r'^api/v2.1/ai/similarity-search-in-library/$', SimilaritySearchInLibrary.as_view(), name='api-v2.1-ai-similarity-search-in-library'), - re_path(r'^api/v2.1/ai/library-sdoc-index/$', LibrarySdocIndex.as_view(), name='api-v2.1-ai-library-sdoc-index'), - re_path(r'^api/v2.1/ai/repo/files/$', RepoFiles.as_view(), name='api-v2.1-ai-repo-files'), - re_path(r'^api/v2.1/ai/task-status/$', TaskStatus.as_view(), name='api-v2.1-ai-task-status'), - # admin: activities re_path(r'^api/v2.1/admin/user-activities/$', UserActivitiesView.as_view(), name='api-v2.1-admin-user-activity'), @@ -964,3 +958,13 @@ urlpatterns += [ re_path(r'^api/v2.1/seadoc/', include('seahub.seadoc.urls')), ] + +if settings.ENABLE_SEAFILE_AI: + urlpatterns += [ + re_path(r'^api/v2.1/ai/library-sdoc-indexes/$', LibrarySdocIndexes.as_view(), name='api-v2.1-ai-library-sdoc-indexes'), + re_path(r'^api/v2.1/ai/similarity-search-in-library/$', SimilaritySearchInLibrary.as_view(), name='api-v2.1-ai-similarity-search-in-library'), + re_path(r'^api/v2.1/ai/library-sdoc-index/$', LibrarySdocIndex.as_view(), name='api-v2.1-ai-library-sdoc-index'), + re_path(r'^api/v2.1/ai/repo/files/$', RepoFiles.as_view(), name='api-v2.1-ai-repo-files'), + re_path(r'^api/v2.1/ai/task-status/$', TaskStatus.as_view(), name='api-v2.1-ai-task-status'), + re_path(r'^api/v2.1/ai/library-index-state/$', LibraryIndexState.as_view(), name='api-v2.1-ai-library-index-state'), + ]