diff --git a/frontend/src/assets/icons/arrow.svg b/frontend/src/assets/icons/arrow.svg
new file mode 100644
index 00000000000..6befa780a16
--- /dev/null
+++ b/frontend/src/assets/icons/arrow.svg
@@ -0,0 +1,17 @@
+
+
+
diff --git a/frontend/src/assets/icons/helpful-selected.svg b/frontend/src/assets/icons/helpful-selected.svg
new file mode 100644
index 00000000000..f95262298bb
--- /dev/null
+++ b/frontend/src/assets/icons/helpful-selected.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/frontend/src/assets/icons/helpful.svg b/frontend/src/assets/icons/helpful.svg
new file mode 100644
index 00000000000..82f2a1bb460
--- /dev/null
+++ b/frontend/src/assets/icons/helpful.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/frontend/src/assets/icons/helpless-selected.svg b/frontend/src/assets/icons/helpless-selected.svg
new file mode 100644
index 00000000000..2faa3aa5a2f
--- /dev/null
+++ b/frontend/src/assets/icons/helpless-selected.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/frontend/src/assets/icons/helpless.svg b/frontend/src/assets/icons/helpless.svg
new file mode 100644
index 00000000000..20a4b8fdb0c
--- /dev/null
+++ b/frontend/src/assets/icons/helpless.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/frontend/src/assets/icons/send.svg b/frontend/src/assets/icons/send.svg
new file mode 100644
index 00000000000..b34f6df1667
--- /dev/null
+++ b/frontend/src/assets/icons/send.svg
@@ -0,0 +1,14 @@
+
+
+
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 00000000000..b9014eca61f
--- /dev/null
+++ b/frontend/src/components/search/ai-search-ask.css
@@ -0,0 +1,79 @@
+.search-container.show.ai-search-ask {
+ width: 800px;
+}
+
+.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;
+}
+
+@media (max-width: 768px) {
+
+ .search-container.show.ai-search-ask {
+ width: 100%;
+ }
+
+ .ai-search-ask .search-input {
+ box-shadow: none;
+ width: 100% !important;
+ }
+
+}
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 00000000000..d47a77c92c8
--- /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: false,
+ 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 00000000000..a33efbc4590
--- /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 00000000000..171b24d83e4
--- /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')}{':'}
+
+
+
+ {gettext('Yes')}
+
+
+
+ {gettext('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 00000000000..ab8d93ba0a5
--- /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 00000000000..69108dfc63a
--- /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 00000000000..897eeb92c08
--- /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 44062107586..02026408072 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,26 +419,55 @@ 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;
- if (!this.state.isResultGetted || this.getValueLength(this.inputValue) < 3) {
+ if (!this.state.isResultGetted || getValueLength(this.inputValue) < 3) {
return (
);
}
if (!resultItems.length) {
return (
- {gettext('No results matching.')}
+ <>
+
+
+
+
{gettext('Ask AI')}{': '}{this.state.value.trim()}
+
+
+ {gettext('No results matching.')}
+ >
);
}
const results = (
+ -
+
+
+
+ {gettext('Ask AI')}{': '}{this.state.value.trim()}
+
+
+
{resultItems.map((item, index) => {
- const isHighlight = index === highlightIndex;
+ const isHighlight = (index + 1) === highlightIndex;
return (
+ );
+ }
+
return (
diff --git a/frontend/src/components/search/constant.js b/frontend/src/components/search/constant.js
index 259f60cdb1c..d5cdf7f292a 100644
--- a/frontend/src/components/search/constant.js
+++ b/frontend/src/components/search/constant.js
@@ -1,3 +1,21 @@
const SEARCH_DELAY_TIME = 1000;
-export { SEARCH_DELAY_TIME };
+const getValueLength = (str) => {
+ let code;
+ let len = 0;
+ for (let i = 0; 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;
+};
+
+export { SEARCH_DELAY_TIME, getValueLength };
diff --git a/frontend/src/components/search/search.js b/frontend/src/components/search/search.js
index 39376358136..3a2c775533a 100644
--- a/frontend/src/components/search/search.js
+++ b/frontend/src/components/search/search.js
@@ -8,7 +8,7 @@ import SearchResultItem from './search-result-item';
import { Utils } from '../../utils/utils';
import { isMac } from '../../utils/extra-attributes';
import toaster from '../toast';
-import { SEARCH_DELAY_TIME } from './constant';
+import { SEARCH_DELAY_TIME, getValueLength } from './constant';
const propTypes = {
repoID: PropTypes.string,
@@ -305,23 +305,6 @@ class Search 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++) {
@@ -359,7 +342,7 @@ class Search extends Component {
if (!width || width === 'default') return null;
if (!this.state.isResultShow) return null;
- if (!this.state.isResultGetted || this.getValueLength(this.inputValue) < 3) {
+ if (!this.state.isResultGetted || getValueLength(this.inputValue) < 3) {
return (
);
diff --git a/frontend/src/components/search/wiki-search.js b/frontend/src/components/search/wiki-search.js
index 36c6b7f4e15..92befc88e57 100644
--- a/frontend/src/components/search/wiki-search.js
+++ b/frontend/src/components/search/wiki-search.js
@@ -7,6 +7,7 @@ import SearchResultItem from './search-result-item';
import More from '../more';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
+import { getValueLength } from './constant';
const propTypes = {
repoID: PropTypes.string,
@@ -134,23 +135,6 @@ class Search extends Component {
this.source.cancel('prev request is cancelled');
}
- 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 = [];
let length = data.length > 5 ? 5 : data.length;
@@ -199,7 +183,7 @@ class Search extends Component {
if (!this.state.isResultShow) {
return;
}
- if (!this.state.isResultGetted || this.getValueLength(this.inputValue) < 3) {
+ if (!this.state.isResultGetted || getValueLength(this.inputValue) < 3) {
return (
);
diff --git a/frontend/src/css/search.css b/frontend/src/css/search.css
index 7f6dbe543de..dcb7de2e27b 100644
--- a/frontend/src/css/search.css
+++ b/frontend/src/css/search.css
@@ -104,10 +104,6 @@
flex: 1;
}
-.search-result-list .item-content .item-name {
- color: #eb8205!important;
-}
-
.search-result-container .search-result-item {
display: flex;
padding: 10px 0 10px 8px;
diff --git a/media/img/ask-ai.png b/media/img/ask-ai.png
new file mode 100644
index 00000000000..899e964482e
Binary files /dev/null and b/media/img/ask-ai.png differ
diff --git a/seahub/ai/apis.py b/seahub/ai/apis.py
index 94bade56c2d..4054ddf3b1e 100644
--- a/seahub/ai/apis.py
+++ b/seahub/ai/apis.py
@@ -15,7 +15,7 @@
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 @@ def post(self, request):
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 214728ee109..3f97ffa3c41 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 47ce9ed54ff..30214e6d655 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, SimilaritySearchInLibrary, LibrarySdocIndex, RepoFiles, TaskStatus, \
- LibraryIndexState
+ LibraryIndexState, QuestionAnsweringSearchInLibrary
urlpatterns = [
path('accounts/', include('seahub.base.registration_urls')),
@@ -964,6 +964,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/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'),