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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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;