Skip to content

Commit

Permalink
Merge pull request #1550 from thehyve/llm-search-changes-rebase
Browse files Browse the repository at this point in the history
Refactor ask AI implementation in Mercury.
  • Loading branch information
ewelinagr authored Aug 21, 2024
2 parents 28d0a19 + 0e9e633 commit 1297181
Show file tree
Hide file tree
Showing 15 changed files with 734 additions and 613 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const HEADERS = {'Content-Type': 'application/json', Accept: 'application
/**
* Search for resources based on name or description, given query as a simple text.
*/
class FulltextAPI {
class AskAIAPI {
remoteURL = '/api/aisearch/';

// Perform a single search, just a query without any context or follow-up.
Expand All @@ -25,7 +25,7 @@ class FulltextAPI {
.catch(handleHttpError('Error while performing search'));
}

// Initialize a chat, no query is send, use 'chat' to ask something.
// Initialize a chat, no query is sent, use 'chat' to ask something.
initializeChat(): Promise<Response> {
return axios
.get(this.remoteURL + 'newchat', {headers: HEADERS})
Expand Down Expand Up @@ -65,4 +65,4 @@ class FulltextAPI {
}
}

export default FulltextAPI;
export default AskAIAPI;
239 changes: 239 additions & 0 deletions projects/mercury/src/ask-ai/AskAIChat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import React, {useEffect, useState} from 'react';
import {Button, Card, Grid, IconButton, Modal, Paper, TextField, Tooltip, Typography} from '@mui/material';
import ClearIcon from '@mui/icons-material/Clear';
import CloseIcon from '@mui/icons-material/Close';
import SearchIcon from '@mui/icons-material/Search';
import withStyles from '@mui/styles/withStyles';
import InputAdornment from '@mui/material/InputAdornment';
import styles from './AskAIChat.styles';
import LinkedDataEntityPage from '../metadata/common/LinkedDataEntityPage';
import {LocalSearchAPI} from '../search/SearchAPI';
import LoadingOverlayWrapper from '../common/components/LoadingOverlayWrapper';

// TODO make this configurable instead of hardcoded value
const getMetadataEntityType = () => {
return 'https://www.fns-cloud.eu/study';
};

const AskAIChat = props => {
const {query, responseDocuments, messages, loading, responseInfo, clearChat, setQuery, classes} = props;
const [documentIri, setDocumentIri] = useState('');
const [openMetadataDialog, setOpenMetadataDialog] = useState(false);
const [inputQuery, setInputQuery] = useState(query);

const handleOpenMetadataDialog = () => setOpenMetadataDialog(true);
const handleCloseMetadataDialog = () => setOpenMetadataDialog(false);

const extractDocumentIri = webResult => {
if (webResult && webResult.length > 0) {
setDocumentIri(webResult[0].id);
} else {
setDocumentIri('');
}
};

const showDocument = documentId => {
if (documentId !== null && documentId.length > 0) {
LocalSearchAPI.lookupSearch(documentId, getMetadataEntityType()).then(extractDocumentIri);
}
};

const onMetadataDialogClose = () => setDocumentIri('');

useEffect(() => {
if (documentIri !== '') {
handleOpenMetadataDialog();
} else {
handleCloseMetadataDialog();
}
}, [documentIri]);

useEffect(() => {
if (query === '') {
setInputQuery('');
}
}, [query]);

// TODO this requires further refactoring, since it is a duplication of LinkedDataLink.js
const renderMetadataDialog = () => (
<Modal
open={openMetadataDialog}
onClose={onMetadataDialogClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<div className={classes.modalWrapper}>
<Card className={classes.modalContent}>
<Tooltip title="Close - click or press Esc">
<CloseIcon onClick={onMetadataDialogClose} className={classes.closeButton} />
</Tooltip>
<LinkedDataEntityPage title="Metadata" subject={documentIri} />
</Card>
</div>
</Modal>
);

const renderMessages = () => {
return (
<div>
{messages &&
messages.map(message => {
if (message.userInput?.input) {
return (
<div className={classes.chatInput}>
<Typography variant="body3" color="primary.dark">
{'> ' + message.userInput.input}
</Typography>
</div>
);
}
if (message.reply?.summary?.summaryText) {
return (
<div className={classes.chatReply}>
<Typography variant="body1">{message.reply.summary.summaryText}</Typography>
</div>
);
}
return null;
})}
</div>
);
};

const renderDocumentReferences = () => {
return (
<div>
{responseDocuments && responseDocuments.length > 0 && (
<Typography variant="h6" color="primary">
Source metadata:
</Typography>
)}
<div className={classes.documentContainer}>
{responseDocuments &&
responseDocuments.map(
document =>
document?.content &&
document.content.length > 0 && (
<div className={classes.chatDocument} onClick={() => showDocument(document.title)}>
<Typography variant="button">Id: {document.title}</Typography>
<Typography variant="body1">{document.content}</Typography>
</div>
)
)}
</div>
</div>
);
};

const renderSearchBar = () => {
return (
<TextField
id="outlined-search"
label="Type your question here"
type="search"
variant="outlined"
className={classes.searchInput}
onChange={event => {
setInputQuery(event.target.value);
}}
onKeyDown={event => {
if (event.key === 'Enter') {
setQuery(inputQuery);
}
}}
value={inputQuery}
InputProps={{
classes: {
input: classes.inputInput,
adornedEnd: classes.adornedEnd
},
endAdornment: (
<InputAdornment position="end">
<IconButton
className={classes.searchIcon}
color="primary"
onClick={() => setQuery(inputQuery)}
>
<SearchIcon />
</IconButton>
</InputAdornment>
)
}}
/>
);
};

return (
<Paper>
<Grid
container
className={classes.searchGrid}
direction="column"
justifyContent="flex-start"
alignItems="stretch"
>
<Grid item container spacing="10" className={classes.searchInputGrid}>
<Grid item xs={2} className={classes.clearChatButtonSection}>
<Button
variant="contained"
size="small"
onClick={clearChat}
startIcon={<ClearIcon />}
className={classes.clearChatButton}
>
Clear chat
</Button>
</Grid>
<Grid item xs={10} className={classes.searchSection}>
{renderSearchBar()}
</Grid>
</Grid>
<LoadingOverlayWrapper loading={loading}>
<Grid
item
container
className={classes.chatResponseSection}
direction="column"
justifyContent="flex-start"
alignItems="stretch"
>
{!responseInfo && !(messages && messages.length > 0) ? (
<Grid
item
container
alignItems="stretch"
justifyContent="center"
direction="column"
className={classes.chatSectionBeforeResponse}
>
<Grid item>
<Typography variant="h3" align="center">
Ask AI
</Typography>
</Grid>
<Grid item>
<Typography variant="subtitle1" align="center">
What would you like to know more about?
</Typography>
</Grid>
</Grid>
) : (
<Grid item container alignItems="stretch" justifyContent="center" direction="column">
<Grid item className={classes.responseMessage}>
<Typography variant="body1">{responseInfo}</Typography>
</Grid>
<Grid item>{renderMessages()}</Grid>
<Grid item>
{responseDocuments && responseDocuments.length > 0 && renderDocumentReferences()}
</Grid>
</Grid>
)}
</Grid>
</LoadingOverlayWrapper>
</Grid>
<div>{renderMetadataDialog()}</div>
</Paper>
);
};

export default withStyles(styles)(AskAIChat);
127 changes: 127 additions & 0 deletions projects/mercury/src/ask-ai/AskAIChat.styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {alpha} from '@mui/material/styles';

const styles = theme => ({
searchGrid: {
height: '100%',
width: '100%'
},
searchInputGrid: {
height: 100
},
clearChatButtonSection: {
display: 'flex',
alignItems: 'center'
},
clearChatButton: {
margin: '20px 0 20px 20px'
},
searchSection: {
display: 'flex',
alignItems: 'center',
width: '100%',
padding: 20
},
searchIcon: {
padding: 0
},
searchInput: {
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.primary.main,
backgroundColor: alpha(theme.palette.primary.main, 0.15),
color: theme.palette.primary.contrastText,
'&:hover': {
backgroundColor: alpha(theme.palette.primary.main, 0.25),
borderColor: theme.palette.primary.main
},
width: '100%',
marginTop: 20,
marginRight: 10
},
chatResponseSection: {
padding: '20px 60px 20px 60px',
width: '100%'
},
chatSectionBeforeResponse: {
paddingTop: 30,
paddingBottom: 40
},
documentContainer: {
overflow: 'auto'
},
chatDocument: {
// backgroundColor: theme.palette.mellow.light,
border: '1px solid ' + theme.palette.primary.light,
borderRadius: theme.shape.borderRadius,
margin: 0,
marginBottom: 10,
paddingLeft: 10,
paddingRight: 10,
cursor: 'pointer'
},
chatResponse: {
backgroundColor: 'white',
width: '100%',
height: '100%'
},
chatInput: {
borderBottom: '2px solid ' + theme.palette.primary.main,
marginBottom: 10
},
chatReply: {
marginLeft: 30,
marginBottom: 10
},
responseMessage: {
color: theme.palette.primary.main,
fontWeight: 'bold',
marginBottom: 10
},
responseDocumentsContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%'
},
modalWrapper: {
position: 'relative',
'& .MuiBreadcrumbs-root .MuiTypography-root': {
color: theme.palette.primary.contrastText
},
top: '10%',
left: '50%',
transform: 'translate(-50%, 0px)',
outline: 'none',
maxHeight: '80vh',
display: 'flex',
flexDirection: 'column',
width: 800
},
modalContent: {
background: theme.palette.primary.dark,
color: theme.palette.primary.contrastText,
bgcolor: 'background.paper',
border: '0px solid #000',
borderRadius: theme.shape.borderRadius,
boxShadow: 0,
outline: 'none',
overflowY: 'auto',
height: '100%',
width: '100%'
},
closeButton: {
float: 'right',
marginTop: 8,
marginRight: 8
},
adornedEnd: {
paddingRight: theme.spacing(1)
}
// clickableDiv: {
// cursor: 'pointer',
// '&:hover': {
// textDecoration: 'underline'
// }
// }
});

export default styles;
Loading

0 comments on commit 1297181

Please sign in to comment.