diff --git a/frontend/src/components/search/ai-search.js b/frontend/src/components/search/ai-search.js
index c4647a4f8ab..a59e5efa3ee 100644
--- a/frontend/src/components/search/ai-search.js
+++ b/frontend/src/components/search/ai-search.js
@@ -26,6 +26,11 @@ const SEARCH_MODE = {
COMBINED: 'combined-search',
};
+const SEARCH_TAB = {
+ GLOBAL: 'global',
+ LIBRARY: 'library',
+};
+
const PER_PAGE = 10;
const controlKey = isMac() ? '⌘' : 'Ctrl';
@@ -59,6 +64,8 @@ export default class AISearch extends Component {
searchPageUrl: this.baseSearchPageURL,
indexState: '',
searchMode: SEARCH_MODE.COMBINED,
+ selectedSearchTab: SEARCH_TAB.GLOBAL,
+ isFocused: false,
};
this.inputValue = '';
this.highlightRef = null;
@@ -151,7 +158,7 @@ export default class AISearch extends Component {
};
onFocusHandler = () => {
- this.setState({ width: '570px', isMaskShow: true, isCloseShow: true });
+ this.setState({ width: '570px', isMaskShow: true, isCloseShow: true, isFocused: true });
};
onCloseHandler = () => {
@@ -210,8 +217,9 @@ export default class AISearch extends Component {
keepVisitedItem = (targetItem) => {
const { repoID } = this.props;
+ const { selectedSearchTab } = this.state;
let targetIndex;
- let storeKey = 'sfVisitedAISearchItems';
+ let storeKey = selectedSearchTab === 'global' ? 'sfVisitedSearchItems' : 'sfVisitedLibraryItems';
if (repoID) {
storeKey += repoID;
}
@@ -258,7 +266,7 @@ export default class AISearch extends Component {
};
onSearch = () => {
- const { value } = this.state;
+ const { value, selectedSearchTab } = this.state;
const { repoID } = this.props;
if (this.inputValue === '' || getValueLength(this.inputValue) < 3) {
this.setState({
@@ -273,8 +281,11 @@ export default class AISearch extends Component {
search_repo: repoID ? repoID : 'all',
search_ftypes: 'all',
};
- this.getSearchResult(queryData);
- };
+ if (selectedSearchTab === SEARCH_TAB.GLOBAL) {
+ this.getSearchResult(queryData);
+ } else if (selectedSearchTab === SEARCH_TAB.LIBRARY) {
+ this.getRepoSearchResult(value);
+ } };
getSearchResult = (queryData) => {
if (this.source) {
@@ -315,6 +326,34 @@ export default class AISearch extends Component {
});
};
+ getRepoSearchResult = (query_str) => {
+ if (this.source) {
+ this.source.cancel('prev request is cancelled');
+ }
+ this.setState({
+ isResultGetted: false,
+ resultItems: [],
+ highlightIndex: 0,
+ });
+ this.source = seafileAPI.getSource();
+
+ const query_type = SEARCH_TAB.LIBRARY
+ let results = [];
+ seafileAPI.searchItems(query_str, query_type, this.source.token).then(res => {
+ results = [...results, ...this.formatResultItems(res.data.results)];
+ this.setState({
+ resultItems: results,
+ isResultGetted: true,
+ isLoading: false,
+ hasMore: false,
+ });
+ }).catch(error => {
+ /* eslint-disable */
+ console.log(error);
+ this.setState({ isLoading: false });
+ });
+ };
+
onResultListScroll = (e) => {
// Load less than 100 results
if (!this.state.hasMore || this.state.isLoading || this.state.resultItems.length > 100) {
@@ -367,7 +406,36 @@ export default class AISearch extends Component {
highlightIndex: 0,
isSearchInputShow: false,
showRecent: true,
+ isFocused: false
+ });
+ }
+
+ onTabChange = (selectedSearchTab) => {
+ this.setState({ selectedSearchTab, resultItems: [], isResultGetted: false }, () => {
+ this.onSearch();
});
+ };
+
+ renderTabs() {
+ const { selectedSearchTab } = this.state;
+ return (
+
+
this.onTabChange('global')}
+ >
+ Global
+
+
this.onTabChange('library')}
+ >
+ Library
+
+
+ );
}
openAsk = () => {
@@ -415,12 +483,12 @@ export default class AISearch extends Component {
}
renderSearchResult() {
- const { resultItems, highlightIndex, width, isResultGetted } = this.state;
+ const { resultItems, highlightIndex, width, isResultGetted, selectedSearchTab } = this.state;
if (!width || width === 'default') return null;
- if (this.state.showRecent) {
+ if ((selectedSearchTab === SEARCH_TAB.GLOBAL || selectedSearchTab === SEARCH_TAB.LIBRARY) && this.state.showRecent) {
const { repoID } = this.props;
- let storeKey = 'sfVisitedAISearchItems';
+ let storeKey = selectedSearchTab === 'global' ? 'sfVisitedSearchItems' : 'sfVisitedLibraryItems';
if (repoID) {
storeKey += repoID;
}
@@ -444,12 +512,13 @@ export default class AISearch extends Component {
else if (!resultItems.length) {
return (
<>
-
-
-
-
{gettext('Ask AI')}{': '}{this.state.value.trim()}
-
-
+ {selectedSearchTab === SEARCH_TAB.GLOBAL && (
+
+
+
+
{gettext('Ask AI')}{': '}{this.state.value.trim()}
+
+ )}
{gettext('No results matching')}
>
);
@@ -457,6 +526,7 @@ export default class AISearch extends Component {
const results = (
+ {selectedSearchTab === SEARCH_TAB.GLOBAL && (
- {gettext('Ask AI')}{': '}{this.state.value.trim()}
+ {gettext('Ask AI')}{': '}{this.state.value.trim()}
+ )}
{resultItems.map((item, index) => {
const isHighlight = (index + 1) === highlightIndex;
return (
@@ -618,7 +689,7 @@ export default class AISearch extends Component {
render() {
let width = this.state.width !== 'default' ? this.state.width : '';
let style = {'width': width};
- const { isMaskShow, searchMode } = this.state;
+ const { isMaskShow, searchMode, isFocused } = this.state;
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`;
if (searchMode === SEARCH_MODE.QA) {
@@ -665,6 +736,7 @@ export default class AISearch extends Component {
}
{this.state.isSettingsShown && this.renderSwitch()}
+ {isFocused && this.renderTabs()}
}
+ {isFocused && this.renderTabs()}
{this.renderSearchResult()}
diff --git a/frontend/src/components/search/search.js b/frontend/src/components/search/search.js
index 2b31c8fdfdb..20a32b6f1d0 100644
--- a/frontend/src/components/search/search.js
+++ b/frontend/src/components/search/search.js
@@ -10,6 +10,11 @@ import { isMac } from '../../utils/extra-attributes';
import toaster from '../toast';
import { SEARCH_DELAY_TIME, getValueLength } from './constant';
+const SEARCH_TAB = {
+ GLOBAL: 'global',
+ LIBRARY: 'library',
+};
+
const propTypes = {
repoID: PropTypes.string,
placeholder: PropTypes.string,
@@ -39,6 +44,8 @@ class Search extends Component {
isCloseShow: false,
isSearchInputShow: false, // for mobile
searchPageUrl: this.baseSearchPageURL,
+ selectedSearchTab: SEARCH_TAB.GLOBAL,
+ isFocused: false,
};
this.inputValue = '';
this.highlightRef = null;
@@ -110,7 +117,7 @@ class Search extends Component {
};
onFocusHandler = () => {
- this.setState({ width: '570px', isMaskShow: true, isCloseShow: true });
+ this.setState({ width: '570px', isMaskShow: true, isCloseShow: true, isFocused: true });
};
onCloseHandler = () => {
@@ -169,8 +176,9 @@ class Search extends Component {
keepVisitedItem = (targetItem) => {
const { repoID } = this.props;
+ const { selectedSearchTab } = this.state;
let targetIndex;
- let storeKey = 'sfVisitedSearchItems';
+ let storeKey = selectedSearchTab === 'global' ? 'sfVisitedSearchItems' : 'sfVisitedLibraryItems';
if (repoID) {
storeKey += repoID;
}
@@ -220,7 +228,7 @@ class Search extends Component {
};
onSearch = () => {
- const { value } = this.state;
+ const { value, selectedSearchTab } = this.state;
const { repoID } = this.props;
if (this.inputValue === '' || getValueLength(this.inputValue) < 3) {
this.setState({
@@ -235,7 +243,11 @@ class Search extends Component {
search_repo: repoID ? repoID : 'all',
search_ftypes: 'all',
};
- this.getSearchResult(queryData);
+ if (selectedSearchTab === SEARCH_TAB.GLOBAL) {
+ this.getSearchResult(queryData);
+ } else if (selectedSearchTab === SEARCH_TAB.LIBRARY) {
+ this.getRepoSearchResult(value);
+ }
};
getSearchResult = (queryData) => {
@@ -319,16 +331,44 @@ class Search extends Component {
});
}
+ getRepoSearchResult = (query_str) => {
+ if (this.source) {
+ this.source.cancel('prev request is cancelled');
+ }
+ this.setState({
+ isResultGetted: false,
+ resultItems: [],
+ highlightIndex: 0,
+ });
+ this.source = seafileAPI.getSource();
+
+ const query_type = SEARCH_TAB.LIBRARY
+ let results = [];
+ seafileAPI.searchItems(query_str, query_type, this.source.token).then(res => {
+ results = [...results, ...this.formatResultItems(res.data.results)];
+ this.setState({
+ resultItems: results,
+ isResultGetted: true,
+ isLoading: false,
+ hasMore: false,
+ });
+ }).catch(error => {
+ /* eslint-disable */
+ console.log(error);
+ this.setState({ isLoading: false });
+ });
+ };
+
onAiSearch = (params, cancelToken) => {
let results = [];
seafileAPI.aiSearchFiles(params, cancelToken).then(res => {
results = [...results, ...this.formatResultItems(res.data.results)];
this.setState({
- resultItems: results,
- isResultGetted: true,
- isLoading: false,
- hasMore: false,
- });
+ resultItems: results,
+ isResultGetted: true,
+ isLoading: false,
+ hasMore: false,
+ });
}).catch(error => {
/* eslint-disable */
console.log(error);
@@ -388,16 +428,46 @@ class Search extends Component {
highlightIndex: 0,
isSearchInputShow: false,
showRecent: true,
+ isFocused: false
+ });
+ }
+
+ onTabChange = (selectedSearchTab) => {
+ this.setState({ selectedSearchTab, resultItems: [], isResultGetted: false }, () => {
+ this.onSearch();
});
+ };
+
+ renderTabs() {
+ const { selectedSearchTab } = this.state;
+ return (
+
+
this.onTabChange('global')}
+ >
+ Global
+
+
this.onTabChange('library')}
+ >
+ Library
+
+
+ );
}
+
renderSearchResult() {
- const { resultItems, width, showRecent, isResultGetted } = this.state;
+ const { resultItems, width, showRecent, isResultGetted, selectedSearchTab } = this.state;
if (!width || width === 'default') return null;
- if (showRecent) {
+ if ((selectedSearchTab === SEARCH_TAB.GLOBAL || selectedSearchTab === SEARCH_TAB.LIBRARY) && showRecent) {
const { repoID } = this.props;
- let storeKey = 'sfVisitedSearchItems';
+ let storeKey = selectedSearchTab === 'global' ? 'sfVisitedSearchItems' : 'sfVisitedLibraryItems';
if (repoID) {
storeKey += repoID;
}
@@ -470,7 +540,7 @@ class Search extends Component {
render() {
let width = this.state.width !== 'default' ? this.state.width : '';
let style = {'width': width};
- const { isMaskShow } = this.state;
+ const { isMaskShow, isFocused } = this.state;
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`;
return (
@@ -497,6 +567,7 @@ class Search extends Component {
}
+ {!this.props.isPublic && isFocused && enableSeafileAI && this.renderTabs()}
}
+ {!this.props.isPublic && isFocused && enableSeafileAI && this.renderTabs()}
{this.renderSearchResult()}
diff --git a/frontend/src/css/search.css b/frontend/src/css/search.css
index 6c45eac2d71..f0c35c1ec7c 100644
--- a/frontend/src/css/search.css
+++ b/frontend/src/css/search.css
@@ -139,6 +139,23 @@
background-color: #f0f0f0;
}
+.search-tabs {
+ display: flex;
+ border-bottom: 1px solid #eaeaea;
+ margin-bottom: 10px;
+}
+
+.search-tab {
+ padding: 10px 20px;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+}
+
+.search-tab.active {
+ color: orange;
+ border-bottom-color: orange;
+}
+
.search-result-item .item-img {
width: 36px;
height: 36px;
diff --git a/seahub/ai/apis.py b/seahub/ai/apis.py
index 240ab2c61bb..d15db54a6cf 100644
--- a/seahub/ai/apis.py
+++ b/seahub/ai/apis.py
@@ -18,7 +18,7 @@
from seahub.ai.utils import create_library_sdoc_index, search, update_library_sdoc_index, \
delete_library_index, query_task_status, query_library_index_state, question_answering_search_in_library,\
get_file_download_token, get_search_repos, RELATED_REPOS_PREFIX, RELATED_REPOS_CACHE_TIMEOUT, SEARCH_REPOS_LIMIT, \
- format_repos
+ format_repos, USER_REPOS_CACHE_PREFIX, USER_REPOS_CACHE_TIMEOUT
from seahub.utils import is_org_context, normalize_cache_key
from seahub.views import check_folder_permission
@@ -337,3 +337,51 @@ def get(self, request):
}
return Response(library_files_info, status.HTTP_200_OK)
+
+
+class ItemsSearch(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated, )
+ throttle_classes = (UserRateThrottle, )
+
+ def get(self, request):
+ """search items"""
+ QUERY_TYPES = [
+ 'library',
+ ]
+
+ query_str = request.GET.get('query_str', '')
+ query_type = request.GET.get('query_type', '')
+
+ if not query_str:
+ error_msg = 'query invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if query_type not in QUERY_TYPES:
+ error_msg = 'query type invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ username = request.user.username
+ org_id = request.user.org.org_id if is_org_context(request) else None
+
+ if query_type == 'library':
+ cache_key = normalize_cache_key(username, USER_REPOS_CACHE_PREFIX)
+ all_repos = cache.get(cache_key)
+ if not all_repos:
+ all_repos = get_search_repos(username, org_id)
+ cache.set(cache_key, all_repos, USER_REPOS_CACHE_TIMEOUT)
+
+ query_result = []
+ # Iterator avoids loading all memory at once
+ query_result = [
+ {
+ "fullpath": "/",
+ "is_dir": True,
+ "repo_name": repo_info[3],
+ "repo_id": repo_info[0],
+ "name": repo_info[3]
+ }
+ for repo_info in all_repos
+ if query_str in repo_info[3]
+ ]
+ return Response({'results': query_result})
diff --git a/seahub/ai/utils.py b/seahub/ai/utils.py
index 5aeb83729ac..b4f3a5f3162 100644
--- a/seahub/ai/utils.py
+++ b/seahub/ai/utils.py
@@ -15,7 +15,9 @@
SEARCH_REPOS_LIMIT = 200
RELATED_REPOS_PREFIX = 'RELATED_REPOS_'
+USER_REPOS_CACHE_PREFIX = 'user_repos_'
RELATED_REPOS_CACHE_TIMEOUT = 2 * 60 * 60
+USER_REPOS_CACHE_TIMEOUT = 2 * 60 * 60
def gen_headers():
diff --git a/seahub/urls.py b/seahub/urls.py
index 065416746bb..9ccf4f692db 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -202,7 +202,7 @@
from seahub.ocm.settings import OCM_ENDPOINT
from seahub.ai.apis import LibrarySdocIndexes, Search, LibrarySdocIndex, TaskStatus, \
- LibraryIndexState, QuestionAnsweringSearchInLibrary, FileDownloadToken
+ LibraryIndexState, QuestionAnsweringSearchInLibrary, FileDownloadToken, ItemsSearch
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
urlpatterns = [
@@ -988,6 +988,7 @@
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/search/$', Search.as_view(), name='api-v2.1-ai-search'),
+ re_path(r'^api/v2.1/ai/items-search/$', ItemsSearch.as_view(), name='api-v2.1-items-search'),
re_path(r'^api/v2.1/ai/question-answering-search-in-library/$', QuestionAnsweringSearchInLibrary.as_view(), name='api-v2.1-ai-question-answering-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/task-status/$', TaskStatus.as_view(), name='api-v2.1-ai-task-status'),