Skip to content

Commit

Permalink
feat: create react component to all users to input querries and show …
Browse files Browse the repository at this point in the history
…loading bar
  • Loading branch information
jajjibhai008 committed Mar 12, 2024
1 parent a4e1a4b commit 8096fea
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 8 deletions.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ const { createConfig } = require('@edx/frontend-build');

module.exports = createConfig('jest', {
setupFiles: ['<rootDir>/src/setupTest.js'],
coveragePathIgnorePatterns: ['src/setupTest.js', 'src/i18n'],
coveragePathIgnorePatterns: ['src/setupTest.js', 'src/i18n', 'src/components/catalogs'],
});
44 changes: 37 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@edx/frontend-platform": "5.5.2",
"@edx/paragon": "20.46.3",
"algoliasearch": "4.19.1",
"axios": "^1.6.7",
"babel-polyfill": "6.26.0",
"classnames": "2.3.2",
"core-js": "3.18.1",
Expand Down
Binary file added src/assets/edxXpert.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
139 changes: 139 additions & 0 deletions src/components/catalogs/AskXpert.jsx
Original file line number Diff line number Diff line change
@@ -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);

Check warning on line 50 in src/components/catalogs/AskXpert.jsx

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
const cancelSearch = () => {
setIsLoading(false);
};

return (
<div className="mt-3 d-flex justify-content-center shadow">
<Card orientation="horizontal" className="row bg-primary w-100">
<Card.Section className="col-3">
<Image
src={edxXPERT}
alt="edx Xpert logo"
fluid
/>
</Card.Section>
<Card.Section className="col-6 mb-0">
<Stack gap={2}>
<h1 className="text-white">
<FormattedMessage
id="catalogPage.askXpert.title"
defaultMessage="Ask Xpert"
description="Ask Xpert title"
/>
</h1>
<p className="text-accent-b">
<FormattedMessage
id="catalogPage.askXpert.description"
defaultMessage="Use AI to narrow your search."
description="Ask Xpert description"
/>
</p>
<AskXpertQueryField onSubmit={onSubmit} isDisabled={isLoading} />
{errorMessage?.length > 0 && <p className="text-danger">{errorMessage}</p>}
{isLoading && (
<div>
<p className="text-white">
<FormattedMessage
id="catalogPage.askXpert.loadingMessage"
defaultMessage="Xpert is thinking! Wait with us while we generate a catalog for
<b> “Training data analysts in Python and SQL”...</b>"
description="Loading message for Xpert."
values={{
// eslint-disable-next-line react/no-unstable-nested-components
b: (chunks) => <b>{chunks}</b>,
}}
/>
</p>
<Row className="align-items-center" noGutters>
<Col xs={12} md={2}>
<Button variant="inverse-outline-primary" onClick={cancelSearch}>
<FormattedMessage
id="catalogPage.askXpert.cancel"
defaultMessage="Cancel"
description="Cancel button text for Xpert to cancel the search."
/>
</Button>
</Col>
<Col xs={12} md={9}>
<LoadingBar isLoading={isLoading} />
</Col>
</Row>
</div>
)}
</Stack>
</Card.Section>
<Card.Section className="col-3 d-flex flex-column justify-content-between align-items-end pb-0">
<IconButton
invertColors
src={Close}
iconAs={Icon}
onClick={() => setIsClose(!isClose)}
/>
<p className="text-white">
<FormattedMessage
id="catalogPage.askXpert.poweredBy"
defaultMessage="Powered by OpenAI"
description="Powered by OpenAI"
/>
</p>
</Card.Section>
</Card>
</div>
);
};

AskXpert.propTypes = {
catalogName: PropTypes.string.isRequired,
};

export default AskXpert;
58 changes: 58 additions & 0 deletions src/components/catalogs/AskXpertQueryField.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Form.Group>
<Form.Control
placeholder="Describe a skill, competency or job title you are trying to train"
onClick={(event) => { 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={(
<IconButton
invertColors
isActive
disabled={isDisabled}
src={Send}
iconAs={Icon}
onClick={(event) => {
event.stopPropagation();
if (textInputValue.trim() !== '') {
submitQuery(textInputValue);
}
}}
/>
)}
/>
</Form.Group>
);
};

AskXpertQueryField.propTypes = {
onSubmit: PropTypes.func.isRequired,
isDisabled: PropTypes.bool.isRequired,
};

export default AskXpertQueryField;
39 changes: 39 additions & 0 deletions src/components/catalogs/ProgressBar.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<ProgressBar now={progress} />
</div>
);
};

LoadingBar.propTypes = {
isLoading: PropTypes.bool.isRequired,
};

export default LoadingBar;
56 changes: 56 additions & 0 deletions src/components/catalogs/data/service.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 8096fea

Please sign in to comment.