diff --git a/frontend/src/components/search/ai-search.js b/frontend/src/components/search/ai-search.js new file mode 100644 index 0000000000..4406210758 --- /dev/null +++ b/frontend/src/components/search/ai-search.js @@ -0,0 +1,621 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import isHotkey from 'is-hotkey'; +import MediaQuery from 'react-responsive'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, siteRoot } from '../../utils/constants'; +import SearchResultItem from './search-result-item'; +import { Utils } from '../../utils/utils'; +import { isMac } from '../../utils/extra-attributes'; +import toaster from '../toast'; +import Switch from '../common/switch'; +import { SEARCH_DELAY_TIME } from './constant'; + +const INDEX_STATE = { + RUNNING: 'running', + UNCREATED: 'uncreated', + FINISHED: 'finished' +}; + +const PER_PAGE = 10; +const controlKey = isMac() ? '⌘' : 'Ctrl'; + +export default class AISearch extends Component { + + static propTypes = { + repoID: PropTypes.string, + placeholder: PropTypes.string, + onSearchedClick: PropTypes.func.isRequired, + repoName: PropTypes.string, + }; + + constructor(props) { + super(props); + this.baseSearchPageURL = `${siteRoot}search/`; + this.state = { + width: 'default', + value: '', + resultItems: [], + highlightIndex: 0, + page: 0, + isLoading: false, + hasMore: true, + isMaskShow: false, + isResultShow: false, + isResultGetted: false, + isCloseShow: false, + isSearchInputShow: false, // for mobile + searchPageUrl: this.baseSearchPageURL, + indexState: '', + }; + this.inputValue = ''; + this.highlightRef = null; + this.source = null; // used to cancel request; + this.inputRef = React.createRef(); + this.searchContainer = React.createRef(); + this.searchResultListRef = React.createRef(); + this.indexStateTimer = null; + this.timer = null; + this.isChineseInput = false; + } + + componentDidMount() { + document.addEventListener('keydown', this.onDocumentKeydown); + document.addEventListener('compositionstart', this.onCompositionStart); + document.addEventListener('compositionend', this.onCompositionEnd); + this.queryLibraryIndexState(); + } + + queryLibraryIndexState() { + seafileAPI.queryLibraryIndexState(this.props.repoID).then(res => { + const { state: indexState, task_id: taskId } = res.data; + this.setState({ indexState }, () => { + if (indexState === INDEX_STATE.RUNNING) { + this.queryIndexTaskStatus(taskId); + } + }); + }).catch(error => { + this.setState({ indexState: INDEX_STATE.UNCREATED }); + }); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.onDocumentKeydown); + document.removeEventListener('compositionstart', this.onCompositionStart); + document.removeEventListener('compositionend', this.onCompositionEnd); + this.isChineseInput = false; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + if (this.indexStateTimer) { + clearInterval(this.indexStateTimer); + this.indexStateTimer = null; + } + } + + onCompositionStart = () => { + this.isChineseInput = true; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + }; + + onCompositionEnd = () => { + this.isChineseInput = false; + // chrome:compositionstart -> onChange -> compositionend + // not chrome:compositionstart -> compositionend -> onChange + // The onChange event will setState and change input value, then setTimeout to initiate the search + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.timer = setTimeout(() => { + this.onSearch(); + }, SEARCH_DELAY_TIME); + }; + + onDocumentKeydown = (e) => { + if (isHotkey('mod+f')(e)) { + e.preventDefault(); + this.onFocusHandler(); + if (this.inputRef && this.inputRef.current) { + this.inputRef.current.focus(); + } + } else if (isHotkey('esc', e)) { + e.preventDefault(); + this.inputRef && this.inputRef.current && this.inputRef.current.blur(); + this.resetToDefault(); + } else if (isHotkey('enter', e)) { + this.onEnter(e); + } else if (isHotkey('up', e)) { + this.onUp(e); + } else if (isHotkey('down', e)) { + this.onDown(e); + } + }; + + onFocusHandler = () => { + this.setState({ width: '570px', isMaskShow: true, isCloseShow: true }); + }; + + onCloseHandler = () => { + this.resetToDefault(); + }; + + onUp = (e) => { + e.preventDefault(); + e.stopPropagation(); + const { highlightIndex } = this.state; + if (highlightIndex > 0) { + this.setState({ highlightIndex: highlightIndex - 1 }, () => { + if (this.highlightRef) { + const { top, height } = this.highlightRef.getBoundingClientRect(); + if (top - height < 0) { + this.searchContainer.current.scrollTop -= height; + } + } + }); + } + }; + + onDown = (e) => { + e.preventDefault(); + e.stopPropagation(); + const { highlightIndex, resultItems } = this.state; + if (highlightIndex < resultItems.length - 1) { + this.setState({ highlightIndex: highlightIndex + 1 }, () => { + if (this.highlightRef) { + const { top, height } = this.highlightRef.getBoundingClientRect(); + const outerHeight = 300; + if (top > outerHeight) { + this.searchContainer.current.scrollTop += height; + } + } + }); + } + }; + + onEnter = (e) => { + e.preventDefault(); + let item = this.state.resultItems[this.state.highlightIndex]; + if (item) { + if (document.activeElement) { + document.activeElement.blur(); + } + this.onItemClickHandler(item); + } + }; + + onItemClickHandler = (item) => { + this.resetToDefault(); + this.props.onSearchedClick(item); + }; + + onChangeHandler = (event) => { + const newValue = event.target.value; + this.setState({ value: newValue }, () => { + if (this.inputValue === newValue.trim()) return; + this.inputValue = newValue.trim(); + if (!this.isChineseInput) { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.timer = setTimeout(() => { + this.onSearch(); + }, SEARCH_DELAY_TIME); + } + }); + }; + + onKeydownHandler = (event) => { + if (isHotkey('enter', event)) { + this.onSearch(); + } + }; + + onSearch = () => { + const { value } = this.state; + const { repoID } = this.props; + if (this.inputValue === '' || this.getValueLength(this.inputValue) < 3) { + this.setState({ + highlightIndex: 0, + resultItems: [], + isResultShow: false, + isResultGetted: false + }); + return; + } + const queryData = { + q: value, + search_repo: repoID ? repoID : 'all', + search_ftypes: 'all', + }; + this.getSearchResult(queryData); + }; + + getSearchResult = (queryData) => { + if (this.source) { + this.source.cancel('prev request is cancelled'); + } + this.setState({ + isResultShow: true, + isResultGetted: false, + resultItems: [], + highlightIndex: 0, + }); + this.source = seafileAPI.getSource(); + this.sendRequest(queryData, this.source.token, 1); + }; + + sendRequest = (queryData, cancelToken, page) => { + this.queryData = queryData; + this.updateSearchPageURL(queryData); + queryData['per_page'] = PER_PAGE; + queryData['page'] = page; + queryData['search_filename_only'] = true; + if (this.state.indexState === INDEX_STATE.FINISHED) { + this.onCombinedSearch(queryData, cancelToken, page); + } else { + this.onNormalSearch(queryData, cancelToken, page); + } + }; + + onNormalSearch = (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, + }); + return; + } + this.setState({ + highlightIndex: 0, + resultItems: [], + isLoading: false, + isResultGetted: true, + hasMore: res.data.has_more, + }); + }).catch(error => { + /* eslint-disable */ + console.log(error); + this.setState({ isLoading: false }); + }); + }; + + onCombinedSearch = (queryData, cancelToken, page) => { + const { indexState } = this.state; + if (indexState === INDEX_STATE.UNCREATED) { + toaster.warning(gettext('Please create index first.')); + return; + } + if (indexState === INDEX_STATE.RUNNING) { + toaster.warning(gettext('Indexing, please try again later.')); + return; + } + + let results = []; + let normalSearchQueryData = Object.assign({}, queryData, {'search_filename_only': true}); + seafileAPI.searchFiles(normalSearchQueryData, cancelToken).then(res => { + if (res.data.total > 0) { + results = [...results, ...this.formatResultItems(res.data.results)]; + } + seafileAPI.similaritySearchFiles(queryData, cancelToken).then(res => { + this.source = null; + if (res.data && res.data.children_list) { + results = [...results, ...this.formatSimilarityItems(res.data.children_list)]; + } + + let tempPathObj = {}; + let searchResults = []; + results.forEach(item => { + if (!tempPathObj[item.path]) { + tempPathObj[item.path] = true; + searchResults.push(item); + } + }); + this.setState({ + resultItems: searchResults, + isResultGetted: true, + isLoading: false, + page: page + 1, + hasMore: false, + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + this.setState({ isLoading: 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) { + return; + } + const listPadding = 20; + if (e.target.scrollTop + e.target.clientHeight + listPadding > this.searchResultListRef.current.clientHeight - 10) { + this.setState({isLoading: true}, () => { + this.source = seafileAPI.getSource(); + this.sendRequest(this.queryData, this.source.token, this.state.page); + }); + } + }; + + updateSearchPageURL(queryData) { + let params = ''; + for (let key in queryData) { + params += key + '=' + encodeURIComponent(queryData[key]) + '&'; + } + this.setState({searchPageUrl: `${this.baseSearchPageURL}?${params.substring(0, params.length - 1)}`}); + } + + getValueLength(str) { + var i = 0, code, len = 0; + for (; i < str.length; i++) { + code = str.charCodeAt(i); + if (code == 10) { //solve enter problem + len += 2; + } else if (code < 0x007f) { + len += 1; + } else if (code >= 0x0080 && code <= 0x07ff) { + len += 2; + } else if (code >= 0x0800 && code <= 0xffff) { + len += 3; + } + } + return len; + } + + formatResultItems(data) { + let items = []; + for (let i = 0; i < data.length; i++) { + items[i] = {}; + items[i]['index'] = [i]; + items[i]['name'] = data[i].name; + items[i]['path'] = data[i].fullpath; + items[i]['repo_id'] = data[i].repo_id; + items[i]['repo_name'] = data[i].repo_name; + items[i]['is_dir'] = data[i].is_dir; + items[i]['link_content'] = decodeURI(data[i].fullpath).substring(1); + items[i]['content'] = data[i].content_highlight; + items[i]['thumbnail_url'] = data[i].thumbnail_url; + } + 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 = ''; + this.setState({ + width: '', + value: '', + isMaskShow: false, + isCloseShow: false, + isResultShow: false, + isResultGetted: false, + resultItems: [], + highlightIndex: 0, + isSearchInputShow: false, + }); + } + + renderSearchResult() { + const { resultItems, highlightIndex, width } = this.state; + if (!width || width === 'default') return null; + + if (!this.state.isResultShow) return null; + if (!this.state.isResultGetted || this.getValueLength(this.inputValue) < 3) { + return ( + + ); + } + if (!resultItems.length) { + return ( +
{gettext('No results matching.')}
+ ); + } + + const results = ( + + ); + + return ( + <> + +
{results}
+
+ + {results} + + + ); + } + + onSearchToggle = () => { + this.setState({ + isSearchInputShow: !this.state.isSearchInputShow, + isMaskShow: !this.state.isMaskShow, + }); + }; + + queryIndexTaskStatus = (taskId) => { + if (!taskId) return; + this.indexStateTimer = setInterval(() => { + seafileAPI.queryIndexTaskStatus(taskId).then(res => { + const isFinished = res.data.is_finished; + if (isFinished) { + this.setState({ indexState: INDEX_STATE.FINISHED }); + this.indexStateTimer && clearInterval(this.indexStateTimer); + this.indexStateTimer = null; + } + }).catch(error => { + this.indexStateTimer && clearInterval(this.indexStateTimer); + this.indexStateTimer = null; + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + this.setState({ indexState: INDEX_STATE.UNCREATED }); + }); + }, 3000); + }; + + onCreateIndex = () => { + this.setState({ indexState: INDEX_STATE.RUNNING }); + seafileAPI.createLibraryIndex(this.props.repoID).then(res => { + const taskId = res.data.task_id; + toaster.notify(gettext('Indexing the library. Semantic search will be available within a few minutes.')) + this.queryIndexTaskStatus(taskId); + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + this.setState({ indexState: INDEX_STATE.UNCREATED }); + }); + }; + + renderSwitch = () => { + const { indexState } = this.state; + if (indexState === INDEX_STATE.FINISHED || indexState === INDEX_STATE.RUNNING) { + return ( + + ); + } else if (indexState === '' || indexState === INDEX_STATE.UNCREATED) { + return ( + + ); + } + return null; + } + + render() { + let width = this.state.width !== 'default' ? this.state.width : ''; + let style = {'width': width}; + const { isMaskShow, isCloseShow } = this.state; + const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`; + return ( + + +
+
+
+
+ + + {this.state.isCloseShow && + + } +
+
+ {isCloseShow && this.renderSwitch()} + {this.renderSearchResult()} +
+
+
+
+ +
+ +
+ {this.state.isSearchInputShow && +
+
+
+
+ + + {this.state.isCloseShow && + + } +
+
+ {this.renderSearchResult()} +
+
+
+ } +
+
+ ); + } +} diff --git a/frontend/src/components/search/constant.js b/frontend/src/components/search/constant.js new file mode 100644 index 0000000000..259f60cdb1 --- /dev/null +++ b/frontend/src/components/search/constant.js @@ -0,0 +1,3 @@ +const SEARCH_DELAY_TIME = 1000; + +export { SEARCH_DELAY_TIME }; diff --git a/frontend/src/components/search/search.js b/frontend/src/components/search/search.js index f2f829e0c9..3937635813 100644 --- a/frontend/src/components/search/search.js +++ b/frontend/src/components/search/search.js @@ -3,26 +3,18 @@ import PropTypes from 'prop-types'; import isHotkey from 'is-hotkey'; import MediaQuery from 'react-responsive'; import { seafileAPI } from '../../utils/seafile-api'; -import { gettext, siteRoot, enableSeafileAI } from '../../utils/constants'; +import { gettext, siteRoot } from '../../utils/constants'; import SearchResultItem from './search-result-item'; import { Utils } from '../../utils/utils'; import { isMac } from '../../utils/extra-attributes'; import toaster from '../toast'; -import Switch from '../common/switch'; - -const INDEX_STATE = { - RUNNING: 'running', - UNCREATED: 'uncreated', - FINISHED: 'finished' -}; +import { SEARCH_DELAY_TIME } from './constant'; 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; @@ -47,7 +39,6 @@ class Search extends Component { isCloseShow: false, isSearchInputShow: false, // for mobile searchPageUrl: this.baseSearchPageURL, - indexState: '', }; this.inputValue = ''; this.highlightRef = null; @@ -56,7 +47,6 @@ class Search extends Component { this.searchContainer = React.createRef(); this.searchResultListRef = React.createRef(); this.timer = null; - this.indexStateTimer = null; this.isChineseInput = false; } @@ -64,35 +54,25 @@ class Search extends Component { document.addEventListener('keydown', this.onDocumentKeydown); document.addEventListener('compositionstart', this.onCompositionStart); document.addEventListener('compositionend', this.onCompositionEnd); - if (enableSeafileAI && this.props.isLibView) { - this.queryLibraryIndexState(); - } - } - - queryLibraryIndexState() { - seafileAPI.queryLibraryIndexState(this.props.repoID).then(res => { - const { state: indexState, task_id: taskId } = res.data; - this.setState({ indexState }, () => { - if (indexState === INDEX_STATE.RUNNING) { - this.queryIndexTaskStatus(taskId); - } - }); - }).catch(error => { - this.setState({ indexState: INDEX_STATE.UNCREATED }); - }); } componentWillUnmount() { document.removeEventListener('keydown', this.onDocumentKeydown); document.removeEventListener('compositionstart', this.onCompositionStart); document.removeEventListener('compositionend', this.onCompositionEnd); - this.indexStateTimer && clearInterval(this.indexStateTimer); - this.timer && clearTimeout(this.timer); + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } this.isChineseInput = false; } onCompositionStart = () => { this.isChineseInput = true; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } }; onCompositionEnd = () => { @@ -100,9 +80,13 @@ class Search extends Component { // chrome:compositionstart -> onChange -> compositionend // not chrome:compositionstart -> compositionend -> onChange // The onChange event will setState and change input value, then setTimeout to initiate the search - setTimeout(() => { - this.onSearch(!this.props.isLibView || !enableSeafileAI); - }, 1); + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.timer = setTimeout(() => { + this.onSearch(); + }, SEARCH_DELAY_TIME); }; onDocumentKeydown = (e) => { @@ -112,8 +96,7 @@ class Search extends Component { if (this.inputRef && this.inputRef.current) { this.inputRef.current.focus(); } - } - else if (isHotkey('esc', e)) { + } else if (isHotkey('esc', e)) { e.preventDefault(); this.inputRef && this.inputRef.current && this.inputRef.current.blur(); this.resetToDefault(); @@ -189,25 +172,27 @@ class Search extends Component { if (this.inputValue === newValue.trim()) return; this.inputValue = newValue.trim(); if (!this.isChineseInput) { - this.onSearch(!this.props.isLibView || !enableSeafileAI); + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.timer = setTimeout(() => { + this.onSearch(); + }, SEARCH_DELAY_TIME); } }); }; onKeydownHandler = (event) => { if (isHotkey('enter', event)) { - if (!enableSeafileAI || !this.props.isLibView) return; - this.onSearch(true); + this.onSearch(); } }; - onSearch = (isGetSearchResult) => { + onSearch = () => { const { value } = this.state; const { repoID } = this.props; - const _this = this; - this.timer && clearTimeout(this.timer); - - if (_this.inputValue === '' || _this.getValueLength(_this.inputValue) < 3) { + if (this.inputValue === '' || this.getValueLength(this.inputValue) < 3) { this.setState({ highlightIndex: 0, resultItems: [], @@ -216,14 +201,12 @@ class Search extends Component { }); return; } - if (!isGetSearchResult) return; - const queryData = { q: value, search_repo: repoID ? repoID : 'all', search_ftypes: 'all', }; - this.timer = setTimeout(_this.getSearchResult(queryData), 500); + this.getSearchResult(queryData); }; getSearchResult = (queryData) => { @@ -273,124 +256,31 @@ class Search extends Component { this.updateSearchPageURL(queryData); queryData['per_page'] = PER_PAGE; queryData['page'] = page; - if (enableSeafileAI && this.props.isLibView) { - queryData['search_filename_only'] = true; - } - if (enableSeafileAI && this.props.isLibView && this.state.indexState === INDEX_STATE.FINISHED) { - this.onCombinedSearch(queryData, cancelToken, page); - } else { - this.onNormalSearch(queryData, cancelToken, page); - } - } - }; - - onNormalSearch = (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, - }); - return; - } - this.setState({ - highlightIndex: 0, - resultItems: [], - isLoading: false, - isResultGetted: true, - hasMore: res.data.has_more, - }); - }).catch(error => { - /* eslint-disable */ - console.log(error); - this.setState({ isLoading: false }); - }); - }; - - onSimilaritySearch = (queryData, cancelToken, page) => { - const { indexState } = this.state; - if (indexState === INDEX_STATE.UNCREATED) { - toaster.warning(gettext('Please create index first.')); - return; - } - if (indexState === INDEX_STATE.RUNNING) { - toaster.warning(gettext('Indexing, please try again later.')); - return; - } - 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, - }); - return; - } - this.setState({ - highlightIndex: 0, - resultItems: [], - isLoading: false, - isResultGetted: true, - hasMore: res.data.has_more, - }); - }).catch(error => { - /* eslint-disable */ - console.log(error); - this.setState({ isLoading: false }); - }); - }; - - onCombinedSearch = (queryData, cancelToken, page) => { - const { indexState } = this.state; - if (indexState === INDEX_STATE.UNCREATED) { - toaster.warning(gettext('Please create index first.')); - return; - } - if (indexState === INDEX_STATE.RUNNING) { - toaster.warning(gettext('Indexing, please try again later.')); - return; - } - - let results = [] - let normalSearchQueryData = Object.assign({}, queryData, {'search_filename_only': true}); - seafileAPI.searchFiles(normalSearchQueryData, cancelToken).then(res => { - if (res.data.total > 0) { - results = [...results, ...this.formatResultItems(res.data.results)] - } - seafileAPI.similaritySearchFiles(queryData, cancelToken).then(res => { + seafileAPI.searchFiles(queryData, cancelToken).then(res => { this.source = null; - if (res.data && res.data.children_list) { - results = [...results, ...this.formatSimilarityItems(res.data.children_list)] + 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, + }); + return; } - - let tempPathObj = {} - let searchResults = [] - results.forEach(item => { - if (!tempPathObj[item.path]) { - tempPathObj[item.path] = true - searchResults.push(item) - } - }) this.setState({ - resultItems: searchResults, - isResultGetted: true, + highlightIndex: 0, + resultItems: [], isLoading: false, - page: page + 1, - hasMore: false, + isResultGetted: true, + hasMore: res.data.has_more, }); - }) - }).catch(error => { - /* eslint-disable */ - console.log(error); - this.setState({ isLoading: false }); - }); + }).catch(error => { + /* eslint-disable */ + console.log(error); + this.setState({ isLoading: false }); + }); + } }; onResultListScroll = (e) => { @@ -449,24 +339,6 @@ 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 = ''; this.setState({ @@ -534,71 +406,10 @@ class Search extends Component { }); }; - queryIndexTaskStatus = (taskId) => { - if (!taskId) return; - this.indexStateTimer = setInterval(() => { - seafileAPI.queryIndexTaskStatus(taskId).then(res => { - const isFinished = res.data.is_finished; - if (isFinished) { - this.setState({ indexState: INDEX_STATE.FINISHED }); - this.indexStateTimer && clearInterval(this.indexStateTimer); - this.indexStateTimer = null; - } - }).catch(error => { - this.indexStateTimer && clearInterval(this.indexStateTimer); - this.indexStateTimer = null; - const errorMsg = Utils.getErrorMsg(error); - toaster.danger(errorMsg); - this.setState({ indexState: INDEX_STATE.UNCREATED }); - }); - }, 3000); - }; - - onCreateIndex = () => { - this.setState({ indexState: INDEX_STATE.RUNNING }); - seafileAPI.createLibraryIndex(this.props.repoID).then(res => { - const taskId = res.data.task_id; - toaster.notify(gettext('Indexing the library. Semantic search will be available within a few minutes.')) - this.queryIndexTaskStatus(taskId); - }).catch(error => { - const errorMsg = Utils.getErrorMsg(error); - toaster.danger(errorMsg); - this.setState({ indexState: INDEX_STATE.UNCREATED }); - }); - }; - - renderSwitch = () => { - const { indexState } = this.state; - if (indexState === INDEX_STATE.FINISHED || indexState === INDEX_STATE.RUNNING) { - return ( - - ); - } else if (indexState === '' || indexState === INDEX_STATE.UNCREATED) { - return ( - - ); - } - return null; - } - render() { let width = this.state.width !== 'default' ? this.state.width : ''; let style = {'width': width}; - const { isMaskShow, isCloseShow } = this.state; + const { isMaskShow } = this.state; const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + f )`}`; return ( @@ -630,7 +441,6 @@ class Search extends Component { onScroll={this.onResultListScroll} ref={this.searchContainer} > - {isCloseShow && this.props.isLibView && enableSeafileAI && this.renderSwitch()} {this.renderSearchResult()} diff --git a/frontend/src/components/toolbar/common-toolbar.js b/frontend/src/components/toolbar/common-toolbar.js index e2caf54d05..efbf685b37 100644 --- a/frontend/src/components/toolbar/common-toolbar.js +++ b/frontend/src/components/toolbar/common-toolbar.js @@ -1,7 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { isPro, gettext, showLogoutIcon } from '../../utils/constants'; +import { isPro, gettext, showLogoutIcon, enableSeafileAI } from '../../utils/constants'; import Search from '../search/search'; +import AISearch from '../search/ai-search'; import SearchByName from '../search/search-by-name'; import Notification from '../common/notification'; import Account from '../common/account'; @@ -17,25 +18,44 @@ const propTypes = { class CommonToolbar extends React.Component { - render() { - const { repoID, repoName } = this.props; - return ( -
- {isPro && ( + renderSearch = () => { + const { repoID, repoName, isLibView, searchPlaceholder } = this.props; + const placeholder = searchPlaceholder || gettext('Search files'); + + if (isPro) { + if (enableSeafileAI && isLibView) { + return ( + + ); + } else { + return ( - )} - {this.props.isLibView && !isPro && - - } + ); + } + } else { + if (isLibView) { + return ( + + ); + } + return null; + } + }; + + render() { + return ( +
+ {this.renderSearch()} {showLogoutIcon && ()}