diff --git a/src/assets/edxXpert.png b/src/assets/edxXpert.png new file mode 100644 index 00000000..76aa27c1 Binary files /dev/null and b/src/assets/edxXpert.png differ diff --git a/src/components/catalogs/AskXpert.jsx b/src/components/catalogs/AskXpert.jsx new file mode 100644 index 00000000..eba8cd6b --- /dev/null +++ b/src/components/catalogs/AskXpert.jsx @@ -0,0 +1,139 @@ +import React, { useState } from 'react'; +import { + Card, + Stack, + Button, + Row, + Col, + Image, + IconButton, + Icon, +} from '@edx/paragon'; +import { Close } from '@edx/paragon/icons'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import edxXPERT from '../../assets/edxXpert.png'; +import AskXpertQueryField from './AskXpertQueryField'; +import EnterpriseCatalogAiCurationApiService from './data/service'; +import LoadingBar from './ProgressBar'; + +const AskXpert = ({ catalogName }) => { + const [isClose, setIsClose] = useState(false); + const [results, setResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const onSubmit = async (query) => { + try { + setErrorMessage(''); + setIsLoading(true); + const queryResponse = await EnterpriseCatalogAiCurationApiService.postXpertQuery(query, catalogName); + const { status: postRequestStatusCode, data: response } = queryResponse; + if (postRequestStatusCode === 200) { + const resultsResponse = await EnterpriseCatalogAiCurationApiService.getXpertResults(response.task_id); + const { status: getRequestStatus, data: FinalResponse } = resultsResponse; + if (getRequestStatus === 200) { + setResults(FinalResponse.results); + } else { + setErrorMessage(FinalResponse.error); + } + } else { + setErrorMessage(response?.error); + } + } catch (error) { + setErrorMessage('An error occurred. Please try again.'); + } finally { + setIsLoading(false); + } + }; + // we will remove this line after passing the results to the results component + console.log('results', results); + const cancelSearch = () => { + setIsLoading(false); + }; + + return ( +
+ + + edx Xpert logo + + + +

+ +

+

+ +

+ + {errorMessage?.length > 0 &&

{errorMessage}

} + {isLoading && ( +
+

+ {chunks}, + }} + /> +

+ + + + + + + + +
+ )} +
+
+ + setIsClose(!isClose)} + /> +

+ +

+
+
+
+ ); +}; + +AskXpert.propTypes = { + catalogName: PropTypes.string.isRequired, +}; + +export default AskXpert; diff --git a/src/components/catalogs/AskXpertQueryField.jsx b/src/components/catalogs/AskXpertQueryField.jsx new file mode 100644 index 00000000..cf10d543 --- /dev/null +++ b/src/components/catalogs/AskXpertQueryField.jsx @@ -0,0 +1,58 @@ +import { + Form, + Icon, + IconButton, +} from '@edx/paragon'; +import { Send } from '@edx/paragon/icons'; +import { useState } from 'react'; + +import PropTypes from 'prop-types'; + +const AskXpertQueryField = ({ onSubmit, isDisabled }) => { + const [textInputValue, setTextInputValue] = useState(''); + const submitQuery = (value) => { + onSubmit(value); + }; + return ( + + { event.stopPropagation(); }} + onChange={(event) => { setTextInputValue(event.target.value); }} + disabled={isDisabled} + maxLength={300} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + if (textInputValue.trim() !== '') { + submitQuery(textInputValue); + } + } + }} + size="lg" + trailingElement={( + { + event.stopPropagation(); + if (textInputValue.trim() !== '') { + submitQuery(textInputValue); + } + }} + /> + )} + /> + + ); +}; + +AskXpertQueryField.propTypes = { + onSubmit: PropTypes.func.isRequired, + isDisabled: PropTypes.bool.isRequired, +}; + +export default AskXpertQueryField; diff --git a/src/components/catalogs/ProgressBar.jsx b/src/components/catalogs/ProgressBar.jsx new file mode 100644 index 00000000..7a28ab5c --- /dev/null +++ b/src/components/catalogs/ProgressBar.jsx @@ -0,0 +1,39 @@ +import React, { useEffect, useState } from 'react'; +import { + ProgressBar, +} from '@edx/paragon'; +import PropTypes from 'prop-types'; + +const LoadingBar = ({ isLoading }) => { + const [progress, setProgress] = useState(0); + + useEffect(() => { + let interval; + + if (isLoading) { + interval = setInterval(() => { + setProgress(prevProgress => { + if (prevProgress >= 90) { return 90; } + return Math.min(prevProgress + Math.floor(Math.random() * 5) + 1, 100); + }); + }, 200); // Adjust the interval duration for smoother or faster loading + } else { + // Clear the interval when loading becomes false + clearInterval(interval); + setProgress(0); + } + + return () => clearInterval(interval); // Cleanup interval + }, [isLoading]); + return ( +
+ +
+ ); +}; + +LoadingBar.propTypes = { + isLoading: PropTypes.bool.isRequired, +}; + +export default LoadingBar; diff --git a/src/components/catalogs/data/service.js b/src/components/catalogs/data/service.js new file mode 100644 index 00000000..6523f983 --- /dev/null +++ b/src/components/catalogs/data/service.js @@ -0,0 +1,56 @@ +import axios from 'axios'; + +class EnterpriseCatalogAiCurationApiService { + static enterpriseCatalogAiCurationServiceUrl = `${process.env.CATALOG_SERVICE_BASE_URL}/api/v1/ai-curation`; + + static MAX_RETRIES = 10; + + static RETRY_INTERVAL = 1000; + + static async postXpertQuery(query, catalogName) { + try { + const response = await axios.post(`${EnterpriseCatalogAiCurationApiService.enterpriseCatalogAiCurationServiceUrl}`, { + query, + // catalog_id: '7af27a1d-8012-4985-b89f-9c8bdd46b3a2', + catalog_id: catalogName, + }); + return { + status: response.status, + data: response.data, + }; + } catch (error) { + return { + status: error.response.status, + data: error.response.data, + }; + } + } + + static wait(ms) { + return new Promise(resolve => { setTimeout(resolve, ms); }); + } + + static async getXpertResults(taskId, retries = 0) { + try { + const response = await axios.get(`${process.env.CATALOG_SERVICE_BASE_URL}/api/v1/ai-curation?task_id=${taskId}`); + if (response.data.status === 'IN_PROGRESS' || response.data.status === 'PENDING') { + if (retries < this.MAX_RETRIES) { + await this.wait(this.RETRY_INTERVAL); + return this.getXpertResults(taskId, retries + 1); + } + throw new Error('Maximum retry count exceeded'); + } + return { + status: response.status, + data: response.data, + }; + } catch (error) { + return { + status: error.response.status, + data: error.response.data, + }; + } + } +} + +export default EnterpriseCatalogAiCurationApiService;