From 6190789d6a8a05775ea16093496b99300e6f2b79 Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Tue, 21 Nov 2023 16:20:29 +0800 Subject: [PATCH 1/6] 01 backend refactor according to the review --- seahub/ai/apis.py | 52 +++++++++++++++++++++++++++++++++++++++++++++- seahub/ai/utils.py | 5 +++++ seahub/urls.py | 3 ++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/seahub/ai/apis.py b/seahub/ai/apis.py index 94bade56c2..4054ddf3b1 100644 --- a/seahub/ai/apis.py +++ b/seahub/ai/apis.py @@ -15,7 +15,7 @@ from seahub.api2.utils import api_error 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_sdoc_info_recursively, similarity_search_in_library, \ - update_library_sdoc_index, delete_library_index, query_task_status, query_library_index_state + update_library_sdoc_index, delete_library_index, query_task_status, query_library_index_state, question_answering_search_in_library from seaserv import seafile_api @@ -122,6 +122,56 @@ class SimilaritySearchInLibrary(APIView): return Response(resp_json, resp.status_code) +class QuestionAnsweringSearchInLibrary(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def post(self, request): + query = request.data.get('query') + repo_id = request.data.get('repo_id') + + try: + count = int(request.data.get('count')) + except: + count = 10 + + if not query: + return api_error(status.HTTP_400_BAD_REQUEST, 'query invalid') + + if not repo_id: + return api_error(status.HTTP_400_BAD_REQUEST, 'repo_id invalid') + + parent_dir = '/' + username = request.user.username + + try: + sdoc_info_list = get_sdoc_info_recursively(username, repo_id, parent_dir, []) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + sdoc_files_info = {file.get('path'): file for file in sdoc_info_list} + params = { + 'query': query, + 'associate_id': repo_id, + 'sdoc_files_info': sdoc_files_info, + 'count': count, + } + + try: + resp = question_answering_search_in_library(params) + if resp.status_code == 500: + logger.error('ask in library 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 LibrarySdocIndex(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) diff --git a/seahub/ai/utils.py b/seahub/ai/utils.py index 214728ee10..3f97ffa3c4 100644 --- a/seahub/ai/utils.py +++ b/seahub/ai/utils.py @@ -68,6 +68,11 @@ def similarity_search_in_library(params): resp = requests.post(url, json=params, headers=headers) return resp +def question_answering_search_in_library(params): + headers = gen_headers() + url = urljoin(SEAFILE_AI_SERVER_URL, '/api/v1/question-answering-search-in-library/') + resp = requests.post(url, json=params, headers=headers) + return resp def update_library_sdoc_index(params): headers = gen_headers() diff --git a/seahub/urls.py b/seahub/urls.py index 47ce9ed54f..30214e6d65 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -202,7 +202,7 @@ from seahub.seadoc.views import sdoc_revision, sdoc_revisions from seahub.ocm.settings import OCM_ENDPOINT from seahub.ai.apis import LibrarySdocIndexes, SimilaritySearchInLibrary, LibrarySdocIndex, RepoFiles, TaskStatus, \ - LibraryIndexState + LibraryIndexState, QuestionAnsweringSearchInLibrary urlpatterns = [ path('accounts/', include('seahub.base.registration_urls')), @@ -964,6 +964,7 @@ 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/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/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'), From c2363961be2f703494171d21db18a0c76520ee4f Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Wed, 22 Nov 2023 17:31:08 +0800 Subject: [PATCH 2/6] change frontend code --- frontend/src/assets/icons/arrow.svg | 17 ++ .../src/assets/icons/helpful-selected.svg | 15 ++ frontend/src/assets/icons/helpful.svg | 18 ++ .../src/assets/icons/helpless-selected.svg | 15 ++ frontend/src/assets/icons/helpless.svg | 18 ++ frontend/src/assets/icons/send.svg | 14 ++ .../src/components/search/ai-search-ask.css | 62 ++++++ .../src/components/search/ai-search-ask.js | 209 ++++++++++++++++++ .../ai-search-widgets/ai-search-help.css | 25 +++ .../ai-search-widgets/ai-search-help.js | 23 ++ .../ai-search-widgets/ai-search-refrences.css | 24 ++ .../ai-search-widgets/ai-search-refrences.js | 33 +++ .../ai-search-widgets/ai-search-robot.js | 17 ++ frontend/src/components/search/ai-search.js | 107 +++++---- frontend/src/components/search/constant.js | 20 +- frontend/src/components/search/search.js | 21 +- frontend/src/components/search/wiki-search.js | 20 +- frontend/src/css/search.css | 4 - 18 files changed, 583 insertions(+), 79 deletions(-) create mode 100644 frontend/src/assets/icons/arrow.svg create mode 100644 frontend/src/assets/icons/helpful-selected.svg create mode 100644 frontend/src/assets/icons/helpful.svg create mode 100644 frontend/src/assets/icons/helpless-selected.svg create mode 100644 frontend/src/assets/icons/helpless.svg create mode 100644 frontend/src/assets/icons/send.svg create mode 100644 frontend/src/components/search/ai-search-ask.css create mode 100644 frontend/src/components/search/ai-search-ask.js create mode 100644 frontend/src/components/search/ai-search-widgets/ai-search-help.css create mode 100644 frontend/src/components/search/ai-search-widgets/ai-search-help.js create mode 100644 frontend/src/components/search/ai-search-widgets/ai-search-refrences.css create mode 100644 frontend/src/components/search/ai-search-widgets/ai-search-refrences.js create mode 100644 frontend/src/components/search/ai-search-widgets/ai-search-robot.js diff --git a/frontend/src/assets/icons/arrow.svg b/frontend/src/assets/icons/arrow.svg new file mode 100644 index 0000000000..6befa780a1 --- /dev/null +++ b/frontend/src/assets/icons/arrow.svg @@ -0,0 +1,17 @@ + + + + +arrow +Created with Sketch. + + + + + + diff --git a/frontend/src/assets/icons/helpful-selected.svg b/frontend/src/assets/icons/helpful-selected.svg new file mode 100644 index 0000000000..f95262298b --- /dev/null +++ b/frontend/src/assets/icons/helpful-selected.svg @@ -0,0 +1,15 @@ + + + + +helpful-selected + + + + diff --git a/frontend/src/assets/icons/helpful.svg b/frontend/src/assets/icons/helpful.svg new file mode 100644 index 0000000000..82f2a1bb46 --- /dev/null +++ b/frontend/src/assets/icons/helpful.svg @@ -0,0 +1,18 @@ + + + + +helpful + + + + diff --git a/frontend/src/assets/icons/helpless-selected.svg b/frontend/src/assets/icons/helpless-selected.svg new file mode 100644 index 0000000000..2faa3aa5a2 --- /dev/null +++ b/frontend/src/assets/icons/helpless-selected.svg @@ -0,0 +1,15 @@ + + + + +helpless-selected + + + + diff --git a/frontend/src/assets/icons/helpless.svg b/frontend/src/assets/icons/helpless.svg new file mode 100644 index 0000000000..20a4b8fdb0 --- /dev/null +++ b/frontend/src/assets/icons/helpless.svg @@ -0,0 +1,18 @@ + + + + +helpless + + + + diff --git a/frontend/src/assets/icons/send.svg b/frontend/src/assets/icons/send.svg new file mode 100644 index 0000000000..b34f6df166 --- /dev/null +++ b/frontend/src/assets/icons/send.svg @@ -0,0 +1,14 @@ + + + + +send + + + + diff --git a/frontend/src/components/search/ai-search-ask.css b/frontend/src/components/search/ai-search-ask.css new file mode 100644 index 0000000000..e2461a59c4 --- /dev/null +++ b/frontend/src/components/search/ai-search-ask.css @@ -0,0 +1,62 @@ +.ai-search-ask .ai-search-ask-header { + display: flex; + align-items: center; + padding: 1rem; + border-bottom: 1px solid rgba(0, 40, 100, 0.12); +} + +.ai-search-ask .ai-search-ask-header .ai-search-ask-return { + padding: 0 4px; + transform: rotate(180deg); + line-height: 10px; + cursor: pointer; +} + +.ai-search-ask .ai-search-ask-header .ai-search-ask-return .seafile-multicolor-icon-arrow { + opacity: 0.6; +} + +.ai-search-ask .ai-search-ask-header .ai-search-ask-return:hover .seafile-multicolor-icon-arrow { + opacity: 0.8; +} + +.ai-search-ask .ai-search-ask-body { + display: flex; + max-height: 400px; + overflow-y: auto; +} + +.ai-search-ask .ai-search-ask-body .ai-search-ask-body-left { + flex-shrink: 0; + margin-right: 1rem; +} + +.ai-search-ask .ai-search-ask-body .ai-search-ask-body-right { + line-height: 1.8; + font-size: 14px; + width: 100%; +} + +.ai-search-ask .ai-search-ask-footer { + border-top: 1px solid rgba(0, 40, 100, 0.12); + margin: 0 1rem; + padding: 1rem 0; +} + +.ai-search-ask .ai-search-ask-footer .ai-search-ask-footer-btn { + width: 16px; + height: 16px; + position: absolute; + right: 8px; + top: 8px; + background-color: #fff; + cursor: pointer; +} + +.ai-search-ask .ai-search-ask-footer .ai-search-ask-footer-btn .seafile-multicolor-icon-send { + color: #ff8000; +} + +.ai-search-ask .ai-search-ask-footer .ai-search-ask-footer-btn:hover .seafile-multicolor-icon-send { + color: #d96d00; +} diff --git a/frontend/src/components/search/ai-search-ask.js b/frontend/src/components/search/ai-search-ask.js new file mode 100644 index 0000000000..e8c92d0cd3 --- /dev/null +++ b/frontend/src/components/search/ai-search-ask.js @@ -0,0 +1,209 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import isHotkey from 'is-hotkey'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext } from '../../utils/constants'; +import toaster from '../toast'; +import Loading from '../loading'; +import Icon from '../icon'; +import { Utils } from '../../utils/utils'; +import { SEARCH_DELAY_TIME, getValueLength } from './constant'; +import AISearchRefrences from './ai-search-widgets/ai-search-refrences'; +import AISearchHelp from './ai-search-widgets/ai-search-help'; +import AISearchRobot from './ai-search-widgets/ai-search-robot'; + +import './ai-search-ask.css'; + +const INDEX_STATE = { + RUNNING: 'running', + UNCREATED: 'uncreated', + FINISHED: 'finished' +}; + +export default class AISearchAsk extends Component { + + static propTypes = { + value: PropTypes.string, + token: PropTypes.string, + repoID: PropTypes.string, + repoName: PropTypes.string, + indexState: PropTypes.string, + onItemClickHandler: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + value: props.value, + isLoading: true, + answeringResult: '', + hitFiles: [], + }; + this.timer = null; + this.isChineseInput = false; + } + + componentDidMount() { + document.addEventListener('compositionstart', this.onCompositionStart); + document.addEventListener('compositionend', this.onCompositionEnd); + this.onSearch(); + } + + componentWillUnmount() { + document.removeEventListener('compositionstart', this.onCompositionStart); + document.removeEventListener('compositionend', this.onCompositionEnd); + this.isChineseInput = false; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + onCompositionStart = () => { + this.isChineseInput = true; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + }; + + onCompositionEnd = () => { + this.isChineseInput = false; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.timer = setTimeout(() => { + this.onSearch(); + }, SEARCH_DELAY_TIME); + }; + + onChange = (event) => { + const newValue = event.target.value; + this.setState({ value: newValue }, () => { + if (!this.isChineseInput) { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.timer = setTimeout(() => { + this.onSearch(); + }, SEARCH_DELAY_TIME); + } + }); + }; + + onKeydown = (event) => { + if (isHotkey('enter', event)) { + this.onSearch(); + } + }; + + formatQuestionAnsweringItems(data) { + let items = []; + for (let i = 0; i < data.length; i++) { + items[i] = {}; + items[i]['index'] = [i]; + items[i]['name'] = data[i].substring(data[i].lastIndexOf('/')+1); + items[i]['path'] = data[i]; + items[i]['repo_id'] = this.props.repoID; + items[i]['is_dir'] = false; + items[i]['link_content'] = decodeURI(data[i]).substring(1); + items[i]['content'] = data[i].sentence; + items[i]['thumbnail_url'] = ''; + } + return items; + } + + onSearch = () => { + const { indexState, repoID, token } = this.props; + 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; + } + if (this.state.isLoading || getValueLength(this.state.value.trim()) < 3) { + return; + } + this.setState({ isLoading: true }); + const searchParams = { + q: this.state.value.trim(), + search_repo: repoID || 'all', + }; + seafileAPI.questionAnsweringFiles(searchParams, token).then(res => { + const { answering_result } = res.data || {}; + const hit_files = answering_result !== 'false' ? res.data.hit_files : []; + this.setState({ + isLoading: false, + answeringResult: answering_result === 'false' ? 'No result' : answering_result, + hitFiles: this.formatQuestionAnsweringItems(hit_files), + }); + }).catch(error => { + /* eslint-disable */ + console.log(error); + this.setState({ isLoading: false }); + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + }; + + render() { + return ( +
+
+
+ +
+ + + + {gettext('Return')} +
+ + {this.state.isLoading ? +
+ +
+ : +
+
+ +
+
+
{this.state.answeringResult}
+ + {this.state.hitFiles.length > 0 && + + } +
+
+ } + +
+
+ + + + +
+
+
+
+ ) + } +} diff --git a/frontend/src/components/search/ai-search-widgets/ai-search-help.css b/frontend/src/components/search/ai-search-widgets/ai-search-help.css new file mode 100644 index 0000000000..a33efbc459 --- /dev/null +++ b/frontend/src/components/search/ai-search-widgets/ai-search-help.css @@ -0,0 +1,25 @@ +.ai-search-help { + padding: 20px 0 20px 0; + border-bottom: 1px solid rgba(0, 40, 100, 0.12); +} + +.ai-search-help .ai-search-help-title { + margin-bottom: 10px; +} + +.ai-search-help .ai-search-help-container { + display: flex; +} + +.ai-search-help .ai-search-help-container .ai-search-help-detail { + border: 1px solid #ccc; + max-width: 200px; + margin-right: 8px; + padding: 4px 8px; + border-radius: 3px; +} + +.ai-search-help .ai-search-help-detail:hover { + cursor: pointer; + background-color: rgb(245, 245, 245); +} diff --git a/frontend/src/components/search/ai-search-widgets/ai-search-help.js b/frontend/src/components/search/ai-search-widgets/ai-search-help.js new file mode 100644 index 0000000000..ef5da9c4e2 --- /dev/null +++ b/frontend/src/components/search/ai-search-widgets/ai-search-help.js @@ -0,0 +1,23 @@ +import React from 'react'; +import Icon from '../../icon'; +import { gettext } from '../../../utils/constants'; + +import './ai-search-help.css'; + +export default function AISearchHelp() { + return ( +
+
{gettext('Is this answer helpful to you')}{':'}
+
+
+ + Yes +
+
+ + No +
+
+
+ ); +} diff --git a/frontend/src/components/search/ai-search-widgets/ai-search-refrences.css b/frontend/src/components/search/ai-search-widgets/ai-search-refrences.css new file mode 100644 index 0000000000..ab8d93ba0a --- /dev/null +++ b/frontend/src/components/search/ai-search-widgets/ai-search-refrences.css @@ -0,0 +1,24 @@ +.ai-search-refrences { + margin-top: 10px; +} + +.ai-search-refrences .ai-search-refrences-title { + margin-bottom: 6px; +} + +.ai-search-refrences .ai-search-refrences-container { + display: flex; +} + +.ai-search-refrences .ai-search-refrences-container .ai-search-refrences-detail { + border: 1px solid #ccc; + max-width: 200px; + margin-right: 8px; + padding: 4px 8px; + border-radius: 3px; +} + +.ai-search-refrences .ai-search-refrences-detail:hover { + cursor: pointer; + background-color: rgb(245, 245, 245); +} diff --git a/frontend/src/components/search/ai-search-widgets/ai-search-refrences.js b/frontend/src/components/search/ai-search-widgets/ai-search-refrences.js new file mode 100644 index 0000000000..69108dfc63 --- /dev/null +++ b/frontend/src/components/search/ai-search-widgets/ai-search-refrences.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../../utils/constants'; + +import './ai-search-refrences.css'; + +function AISearchRefrences({hitFiles, onItemClickHandler}) { + return ( +
+
{gettext('Reference documents')}{':'}
+
+ {hitFiles.map((hitFile, index) => { + return ( +
onItemClickHandler(hitFile)} + key={index} + > + {`${index + 1}. ${hitFile.name}`} +
+ ); + })} +
+
+ ); +} + +AISearchRefrences.propTypes = { + hitFiles: PropTypes.array.isRequired, + onItemClickHandler: PropTypes.func.isRequired, +}; + +export default AISearchRefrences; diff --git a/frontend/src/components/search/ai-search-widgets/ai-search-robot.js b/frontend/src/components/search/ai-search-widgets/ai-search-robot.js new file mode 100644 index 0000000000..897eeb92c0 --- /dev/null +++ b/frontend/src/components/search/ai-search-widgets/ai-search-robot.js @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { mediaUrl } from '../../../utils/constants'; + +function AISearchRobot({style}) { + return ( +
+ +
+ ); +} + +AISearchRobot.propTypes = { + style: PropTypes.object, +}; + +export default AISearchRobot; diff --git a/frontend/src/components/search/ai-search.js b/frontend/src/components/search/ai-search.js index 4406210758..be4dc83365 100644 --- a/frontend/src/components/search/ai-search.js +++ b/frontend/src/components/search/ai-search.js @@ -1,6 +1,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import isHotkey from 'is-hotkey'; +import classnames from 'classnames'; import MediaQuery from 'react-responsive'; import { seafileAPI } from '../../utils/seafile-api'; import { gettext, siteRoot } from '../../utils/constants'; @@ -9,7 +10,9 @@ 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'; +import { SEARCH_DELAY_TIME, getValueLength } from './constant'; +import AISearchAsk from './ai-search-ask'; +import AISearchRobot from './ai-search-widgets/ai-search-robot'; const INDEX_STATE = { RUNNING: 'running', @@ -17,6 +20,11 @@ const INDEX_STATE = { FINISHED: 'finished' }; +const SEARCH_MODE = { + QA: 'question-answering', + COMBINED: 'combined-search', +}; + const PER_PAGE = 10; const controlKey = isMac() ? '⌘' : 'Ctrl'; @@ -47,6 +55,7 @@ export default class AISearch extends Component { isSearchInputShow: false, // for mobile searchPageUrl: this.baseSearchPageURL, indexState: '', + searchMode: SEARCH_MODE.COMBINED, }; this.inputValue = ''; this.highlightRef = null; @@ -84,33 +93,31 @@ export default class AISearch extends Component { document.removeEventListener('compositionstart', this.onCompositionStart); document.removeEventListener('compositionend', this.onCompositionEnd); this.isChineseInput = false; - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } + this.clearTimer(); if (this.indexStateTimer) { clearInterval(this.indexStateTimer); this.indexStateTimer = null; } } - onCompositionStart = () => { - this.isChineseInput = true; + clearTimer = () => { if (this.timer) { clearTimeout(this.timer); this.timer = null; } }; + onCompositionStart = () => { + this.isChineseInput = true; + this.clearTimer(); + }; + 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.clearTimer(); this.timer = setTimeout(() => { this.onSearch(); }, SEARCH_DELAY_TIME); @@ -199,10 +206,7 @@ export default class AISearch extends Component { if (this.inputValue === newValue.trim()) return; this.inputValue = newValue.trim(); if (!this.isChineseInput) { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } + this.clearTimer(); this.timer = setTimeout(() => { this.onSearch(); }, SEARCH_DELAY_TIME); @@ -219,7 +223,7 @@ export default class AISearch extends Component { onSearch = () => { const { value } = this.state; const { repoID } = this.props; - if (this.inputValue === '' || this.getValueLength(this.inputValue) < 3) { + if (this.inputValue === '' || getValueLength(this.inputValue) < 3) { this.setState({ highlightIndex: 0, resultItems: [], @@ -329,6 +333,9 @@ export default class AISearch extends Component { hasMore: false, }); }).catch(error => { + if (error && error.message === "prev request is cancelled") { + return; + } let errMessage = Utils.getErrorMsg(error); toaster.danger(errMessage); this.setState({ isLoading: false }); @@ -362,23 +369,6 @@ export default class AISearch extends Component { 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++) { @@ -429,8 +419,18 @@ export default class AISearch extends Component { }); } + openAsk = () => { + this.clearTimer(); + this.setState({ searchMode: SEARCH_MODE.QA }); + } + + closeAsk = () => { + this.clearTimer(); + this.setState({ searchMode: SEARCH_MODE.COMBINED }); + } + renderSearchResult() { - const { resultItems, highlightIndex, width } = this.state; + const { resultItems, highlightIndex, width, searchMode, answeringResult } = this.state; if (!width || width === 'default') return null; if (!this.state.isResultShow) return null; @@ -441,14 +441,33 @@ export default class AISearch extends Component { } if (!resultItems.length) { return ( -
{gettext('No results matching.')}
+ <> +
  • + +
    +
    {gettext('Ask Seafile AI')}{': '}{this.state.value.trim()}
    +
    +
  • +
    {gettext('No results matching.')}
    + ); } const results = (