diff --git a/projects/mercury/src/llm/FulltextAPI.js b/projects/mercury/src/ask-ai/AskAIAPI.js similarity index 95% rename from projects/mercury/src/llm/FulltextAPI.js rename to projects/mercury/src/ask-ai/AskAIAPI.js index 3caa15a7f..d281b694b 100644 --- a/projects/mercury/src/llm/FulltextAPI.js +++ b/projects/mercury/src/ask-ai/AskAIAPI.js @@ -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. @@ -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 { return axios .get(this.remoteURL + 'newchat', {headers: HEADERS}) @@ -65,4 +65,4 @@ class FulltextAPI { } } -export default FulltextAPI; +export default AskAIAPI; diff --git a/projects/mercury/src/ask-ai/AskAIChat.js b/projects/mercury/src/ask-ai/AskAIChat.js new file mode 100644 index 000000000..05b1aad7f --- /dev/null +++ b/projects/mercury/src/ask-ai/AskAIChat.js @@ -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 = () => ( + +
+ + + + + + +
+
+ ); + + const renderMessages = () => { + return ( +
+ {messages && + messages.map(message => { + if (message.userInput?.input) { + return ( +
+ + {'> ' + message.userInput.input} + +
+ ); + } + if (message.reply?.summary?.summaryText) { + return ( +
+ {message.reply.summary.summaryText} +
+ ); + } + return null; + })} +
+ ); + }; + + const renderDocumentReferences = () => { + return ( +
+ {responseDocuments && responseDocuments.length > 0 && ( + + Source metadata: + + )} +
+ {responseDocuments && + responseDocuments.map( + document => + document?.content && + document.content.length > 0 && ( +
showDocument(document.title)}> + Id: {document.title} + {document.content} +
+ ) + )} +
+
+ ); + }; + + const renderSearchBar = () => { + return ( + { + setInputQuery(event.target.value); + }} + onKeyDown={event => { + if (event.key === 'Enter') { + setQuery(inputQuery); + } + }} + value={inputQuery} + InputProps={{ + classes: { + input: classes.inputInput, + adornedEnd: classes.adornedEnd + }, + endAdornment: ( + + setQuery(inputQuery)} + > + + + + ) + }} + /> + ); + }; + + return ( + + + + + + + + {renderSearchBar()} + + + + + {!responseInfo && !(messages && messages.length > 0) ? ( + + + + Ask AI + + + + + What would you like to know more about? + + + + ) : ( + + + {responseInfo} + + {renderMessages()} + + {responseDocuments && responseDocuments.length > 0 && renderDocumentReferences()} + + + )} + + + +
{renderMetadataDialog()}
+
+ ); +}; + +export default withStyles(styles)(AskAIChat); diff --git a/projects/mercury/src/ask-ai/AskAIChat.styles.js b/projects/mercury/src/ask-ai/AskAIChat.styles.js new file mode 100644 index 000000000..37e7c2c5d --- /dev/null +++ b/projects/mercury/src/ask-ai/AskAIChat.styles.js @@ -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; diff --git a/projects/mercury/src/ask-ai/AskAIHistory.js b/projects/mercury/src/ask-ai/AskAIHistory.js new file mode 100644 index 000000000..1cd25c7ca --- /dev/null +++ b/projects/mercury/src/ask-ai/AskAIHistory.js @@ -0,0 +1,62 @@ +import React from 'react'; +import {Card, CardContent, CardHeader, Grid, IconButton, List, ListItemButton, Typography} from '@mui/material'; +import HistoryIcon from '@mui/icons-material/History'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import withStyles from '@mui/styles/withStyles'; +import styles from './AskAIHistory.styles'; +import LoadingOverlayWrapper from '../common/components/LoadingOverlayWrapper'; + +const AskAIHistory = props => { + const {conversationHistory, conversationId, historyLoading, restoreChat, deleteChat, classes} = props; + + return ( + + } /> + + + } + > + {conversationHistory && conversationHistory.length > 0 ? ( + conversationHistory.map(item => ( + restoreChat(item.id)} + > + + + {item.start} + + + deleteChat(item.id)} + size="small" + > + + + + + {item.topic} + + + + )) + ) : ( + + No chat history available + + )} + + + + + ); +}; + +export default withStyles(styles)(AskAIHistory); diff --git a/projects/mercury/src/ask-ai/AskAIHistory.styles.js b/projects/mercury/src/ask-ai/AskAIHistory.styles.js new file mode 100644 index 000000000..05a6ac508 --- /dev/null +++ b/projects/mercury/src/ask-ai/AskAIHistory.styles.js @@ -0,0 +1,33 @@ +const styles = theme => ({ + historyContentContainer: { + height: '100%' + }, + historyList: { + display: 'block', + position: 'relative', + overflow: 'auto' + }, + historyListItem: { + border: '1.5px solid ' + theme.palette.primary.main, + borderRadius: theme.shape.borderRadius, + marginBottom: 6, + paddingBottom: 6, + marginRight: 8 + }, + historyDateAndButtonDiv: { + width: '100%', + display: 'inline' + }, + blockDisplay: { + display: 'block' + }, + noChatHistoryMessage: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + paddingTop: 80, + paddingBottom: 70 + } +}); + +export default styles; diff --git a/projects/mercury/src/ask-ai/AskAIPage.js b/projects/mercury/src/ask-ai/AskAIPage.js new file mode 100644 index 000000000..0f4f1503b --- /dev/null +++ b/projects/mercury/src/ask-ai/AskAIPage.js @@ -0,0 +1,73 @@ +import React, {useContext} from 'react'; + +import {Grid} from '@mui/material'; +import MetadataViewContext from '../metadata/views/MetadataViewContext'; +import UserContext from '../users/UserContext'; +import AskAIChat from './AskAIChat'; +import {getSearchQueryFromString} from '../search/searchUtils'; +import AskAIHistory from './AskAIHistory'; +import usePageTitleUpdater from '../common/hooks/UsePageTitleUpdater'; +import {useAskAIData} from './UseAskAIData'; + +const AskAIPage = props => { + const {currentUser, location: {search} = ''} = props; + const initQuery = getSearchQueryFromString(search); + const {views} = useContext(MetadataViewContext); + const canViewMetadata = currentUser && currentUser.canViewPublicMetadata && views && views.length > 0; + const { + query, + setQuery, + conversationHistory, + conversationId, + responseDocuments, + messages, + loading, + historyLoading, + responseInfo, + deleteChat, + restoreChat, + clearChat + } = useAskAIData(initQuery); + + usePageTitleUpdater('Ask AI'); + + return ( + + + {canViewMetadata && ( + + )} + + + + + + ); +}; + +const ContextualAskAIPage = props => { + const {currentUser} = useContext(UserContext); + + return ; +}; + +export default ContextualAskAIPage; diff --git a/projects/mercury/src/ask-ai/UseAskAIData.js b/projects/mercury/src/ask-ai/UseAskAIData.js new file mode 100644 index 000000000..d2cbd8b4a --- /dev/null +++ b/projects/mercury/src/ask-ai/UseAskAIData.js @@ -0,0 +1,189 @@ +import {useEffect, useState} from 'react'; +import AskAIAPI from './AskAIAPI'; +import {handleHttpError} from '../common/utils/httpUtils'; + +type AskAIResponse = { + searchResults: any[], + conversation: any, + conversationId: string, + reply: any +}; + +export const useAskAIData = initQuery => { + const [query, setQuery] = useState(initQuery); + const [loading, setLoading] = useState(false); + const [historyLoading, setHistoryLoading] = useState(false); + const [error, setError] = useState(); + const [messages, setMessages] = useState([]); + const [responseInfo, setResponseInfo] = useState(''); + const [responseDocuments, setResponseDocuments] = useState([]); + const [conversationId, setConversationId] = useState(''); + const [conversationHistory, setConversationHistory] = useState([]); + + const askAIAPI = new AskAIAPI(); + + const processResponseDocuments = data => { + const documents = []; + if (data.searchResults) { + data.searchResults.forEach(result => + result.document.derivedStructData.extractive_answers.forEach(element => { + documents.push({title: result.document.derivedStructData.title, content: element.content}); + }) + ); + } + setResponseDocuments(documents); + }; + + const processHistoryResponseMessages = data => { + const id = data.conversation ? data.conversation.conversationId : data.conversationId; + const oldMessages = data.conversation ? data.conversation.messages : data.messages; + const oldResponseMessage = data.reply ? data.reply.summary.summaryText : ''; + + setResponseInfo(oldResponseMessage); + setConversationId(id); + setMessages(oldMessages); + askAIAPI.getConversation(id).then(conversation => processResponseDocuments(conversation)); + }; + + const processResponseMessages = (data: AskAIResponse) => { + const newMessages = data.conversation?.messages || data.messages; + const id = data.conversation?.conversationId || data.conversationId; + + if (!newMessages?.length) { + setResponseInfo('No chat messages found.'); + setResponseDocuments([]); + } else if (data.reply?.summary?.summaryText.includes('not enough information')) { + setResponseInfo('Apologies, there is not enough information available to answer this query.'); + setConversationId(id); + setMessages(newMessages); + } else if ( + ['not enough information', "I don't know what you mean", "not sure what you're asking about"].some(text => + data.reply?.summary?.summaryText.includes(text) + ) + ) { + setResponseInfo(''); + setConversationId(id); + setMessages(newMessages); + } else { + setResponseInfo(''); + setConversationId(id); + setMessages(newMessages); + processResponseDocuments(data); + } + }; + + const handleSearchError = e => { + setResponseInfo(e); + setLoading(false); + }; + + const performSearch = () => { + if (conversationId === '') { + return askAIAPI.search(query); + } + return askAIAPI.chat(query, conversationId); + }; + + const search = () => { + setResponseInfo(''); + if (query === '') { + return; + } + setLoading(true); + performSearch() + .then(processResponseMessages) + .then(() => setLoading(false)) + .catch(() => { + handleHttpError('Connection error.'); + handleSearchError('Ask AI search is not available at the moment'); + }) + .finally(() => setLoading(false)); + }; + + const getAllConversationHistory = () => { + setHistoryLoading(true); + askAIAPI + .getAllConversations() + .then(data => { + if (data && data.length > 0) { + data.sort((a, b) => new Date(b.start) - new Date(a.start)); + + if (JSON.stringify(data) !== JSON.stringify(conversationHistory)) { + setConversationHistory(data); + } + } + }) + .catch(() => { + handleHttpError('Error retrieving chat history.'); + }) + .finally(() => setHistoryLoading(false)); + }; + + const deleteChat = id => { + setHistoryLoading(true); + askAIAPI + .deleteChat(id) + .then(() => { + setMessages([]); + setResponseDocuments([]); + setConversationId(''); + getAllConversationHistory(); + }) + .catch(e => { + console.error('Error deleting chat', e); + setError(e); + }) + .finally(() => setHistoryLoading(false)); + }; + + const clearChat = () => { + setQuery(''); + setResponseInfo(''); + setMessages([]); + setResponseDocuments([]); + setConversationId(''); + getAllConversationHistory(); + }; + + const restoreChat = id => { + setLoading(true); + setQuery(''); + setResponseDocuments([]); + setMessages([]); + setConversationId(id); + askAIAPI + .getHistory(id) + .then(processHistoryResponseMessages) + .catch(() => { + handleHttpError('Error retrieving chat history.'); + handleSearchError('Error retrieving chat history.'); + }) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + getAllConversationHistory(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + search(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [query]); + + return { + query, + setQuery, + loading, + historyLoading, + error, + responseDocuments, + messages, + responseInfo, + conversationId, + conversationHistory, + restoreChat, + clearChat, + deleteChat + }; +}; diff --git a/projects/mercury/src/common/components/BreadCrumbs.js b/projects/mercury/src/common/components/BreadCrumbs.js index 3e5b67206..5d22f207a 100644 --- a/projects/mercury/src/common/components/BreadCrumbs.js +++ b/projects/mercury/src/common/components/BreadCrumbs.js @@ -54,7 +54,8 @@ BreadCrumbs.propTypes = { const styles = theme => ({ root: { padding: theme.spacing(1, 2), - display: 'flex' + display: 'flex', + minHeight: 20 }, link: { display: 'flex' diff --git a/projects/mercury/src/dashboard/DashboardPage.js b/projects/mercury/src/dashboard/DashboardPage.js index 21fc94be9..eee654a7a 100644 --- a/projects/mercury/src/dashboard/DashboardPage.js +++ b/projects/mercury/src/dashboard/DashboardPage.js @@ -12,6 +12,7 @@ import DomainInfo from './DomainInfo'; import {APPLICATION_DOCS_URL, APPLICATION_NAME, THE_HYVE_URL} from '../constants'; import InternalMetadataSourceContext from '../metadata/metadata-sources/InternalMetadataSourceContext'; import FeaturesContext from '../common/contexts/FeaturesContext'; +import usePageTitleUpdater from '../common/hooks/UsePageTitleUpdater'; const DashboardPage = props => { const {currentUser, classes} = props; @@ -23,6 +24,8 @@ const DashboardPage = props => { const {isFeatureEnabled} = useContext(FeaturesContext); const isLlsSearchEnabled = isFeatureEnabled('LlmSearch'); + usePageTitleUpdater('Home'); + const handleLlmInputChange = event => { if (event.keyCode === 13) { history.push('/ask/?' + queryString.stringify({q: event.target.value})); diff --git a/projects/mercury/src/layout/MainMenu.js b/projects/mercury/src/layout/MainMenu.js index c6c6044aa..cc426f913 100644 --- a/projects/mercury/src/layout/MainMenu.js +++ b/projects/mercury/src/layout/MainMenu.js @@ -123,14 +123,14 @@ const MainMenu = ({open, classes}) => { className={classNames( classes.mainMenuButton, !open && classes.mainMenuButtonSmall, - pathname.startsWith('/search') && classes.mainMenuButtonSelected + pathname.startsWith('/ask') && classes.mainMenuButtonSelected )} key="search" component={NavLink} to="/ask" startIcon={} > - {open && 'Chat'} + {open && 'Ask AI'} )} {externalStorages && diff --git a/projects/mercury/src/layout/UserMenu.js b/projects/mercury/src/layout/UserMenu.js index 5b1fea9cd..8c0dcfc04 100644 --- a/projects/mercury/src/layout/UserMenu.js +++ b/projects/mercury/src/layout/UserMenu.js @@ -57,9 +57,6 @@ const styles = theme => ({ userMenu: { backgroundColor: COLORS.fsBlueLightTransp25, cursor: 'default' - }, - customFont: { - fontFamily: 'sans-serif' } }); diff --git a/projects/mercury/src/llm/Conversation.js b/projects/mercury/src/llm/Conversation.js deleted file mode 100644 index 7cf62bb51..000000000 --- a/projects/mercury/src/llm/Conversation.js +++ /dev/null @@ -1,415 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import { - Box, - Button, - CircularProgress, - Fade, - Grid, - IconButton, - List, - ListItemButton, - Modal, - Paper, - TextField, - Tooltip, - Typography -} from '@mui/material'; -import AddIcon from '@mui/icons-material/Add'; -import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; -import CloseIcon from '@mui/icons-material/Close'; -import SearchIcon from '@mui/icons-material/Search'; -import withStyles from '@mui/styles/withStyles'; -import FulltextAPI from './FulltextAPI'; -import styles from './Conversation.styles'; -import {handleHttpError} from '../common/utils/httpUtils'; -import LinkedDataEntityPage from '../metadata/common/LinkedDataEntityPage'; -import {LocalSearchAPI} from '../search/SearchAPI'; - -const Conversation = props => { - const {initialQuery = '', classes} = props; - - const [query, setQuery] = useState(initialQuery); - - const [messages, setMessages] = useState([]); - const [responseMessage, setResponseMessage] = useState(''); - const [responseArticles, setResponseArticles] = useState([]); - - const [conversationId, setConversationId] = useState(''); - const [conversationHistory, setConversationHistory] = useState([]); - const [restoreChatStatus, setRestoreChatStatus] = useState(false); - - const [openModal, setOpenModal] = useState(false); - - const handleOpenModal = () => setOpenModal(true); - const handleCloseModal = () => setOpenModal(false); - - const [articleIri, setArticleIri] = useState(''); - - const [loading, setLoading] = useState(false); - - const processResponseArticles = data => { - const articles = []; - - if (data.searchResults) { - data.searchResults.forEach(result => - result.document.derivedStructData.extractive_answers.forEach(element => { - articles.push({title: result.document.derivedStructData.title, content: element.content}); - }) - ); - } - - setResponseArticles(articles); - }; - - const processResponseMessages = data => { - const newMessages = data.conversation ? data.conversation.messages : data.messages; - const id = data.conversation ? data.conversation.conversationId : data.conversationId; - - if (!newMessages || newMessages.length === 0) { - setResponseMessage('No chat messages found.'); - setResponseArticles([]); - } else if ( - data.reply && - data.reply.summary && - data.reply.summary.summaryText.includes('not enough information') - ) { - setResponseMessage('Apologies, there is not enough information available to answer this query.'); - setConversationId(id); - setMessages(newMessages); - } else if ( - data.reply && - data.reply.summary && - (data.reply.summary.summaryText.includes('not enough information') || - data.reply.summary.summaryText.includes("I don't know what you mean") || - data.reply.summary.summaryText.includes("not sure what you're asking about")) - ) { - setResponseMessage(''); - setConversationId(id); - setMessages(newMessages); - } else { - setResponseMessage(''); - setConversationId(id); - setMessages(newMessages); - processResponseArticles(data); - } - }; - - const processHistoryResponseMessages = data => { - const id = data.conversation ? data.conversation.conversationId : data.conversationId; - const oldMessages = data.conversation ? data.conversation.messages : data.messages; - const oldResponseMessage = data.reply ? data.reply.summary.summaryText : ''; - - setResponseMessage(oldResponseMessage); - setConversationId(id); - setMessages(oldMessages); - new FulltextAPI().getConversation(id).then(conversation => processResponseArticles(conversation)); - }; - - const restoreChat = id => { - new FulltextAPI() - .getHistory(id) - .then(processHistoryResponseMessages) - .then(() => setLoading(false)) - .catch(() => handleHttpError('Error retrieving chat history.')); - }; - - const processConversationHistory = data => { - if (data && data.length > 0) { - data.sort((a, b) => new Date(b.start) - new Date(a.start)); - - if (JSON.stringify(data) !== JSON.stringify(conversationHistory)) { - setConversationHistory(data); - } - } - }; - - const getAllConversationHistory = () => { - new FulltextAPI() - .getAllConversations() - .then(processConversationHistory) - .catch(() => { - handleHttpError('Error retrieving chat history.'); - setLoading(false); - }); - }; - - const prepareRestoreChat = id => { - setLoading(true); - setQuery(''); - setResponseArticles([]); - setMessages([]); - setConversationId(id); - setRestoreChatStatus(true); - getAllConversationHistory(); - }; - - const startNewConversation = () => { - setQuery(''); - setResponseMessage(''); - setMessages([]); - setResponseArticles([]); - setConversationId(''); - getAllConversationHistory(); - }; - - const deleteChat = id => { - new FulltextAPI() - .deleteChat(id) - .then(getAllConversationHistory) - .then(startNewConversation) - .then(() => setLoading(false)) - .catch(() => handleHttpError('Error deleting chat history with id ' + id)); - }; - - const processSearchQueryChange = newQuery => { - if (newQuery !== query) { - if (responseMessage !== '') { - setResponseMessage(''); - setResponseArticles([]); - } - - setQuery(newQuery); - } - }; - - const handleSearchError = error => { - setResponseMessage(error); - setLoading(false); - }; - - const performSearch = () => { - if (conversationId === '') { - const searchResult = new FulltextAPI().search(query); - return searchResult; - } - - return new FulltextAPI().chat(query, conversationId); - }; - - const prepareFetchSearch = () => { - setLoading(true); - setResponseMessage(''); - - if (query === '') { - return; - } - - performSearch() - .then(processResponseMessages) - .then(() => setLoading(false)) - .catch(() => { - handleHttpError('Connection error.'); - handleSearchError('Fairspace article search is not available at the moment'); - }); - }; - - const extractArticleIri = webResult => { - if (webResult && webResult.length > 0) { - setArticleIri(webResult[0].id); - } else { - setArticleIri(''); - } - }; - - const showArticle = articleId => { - if (articleId !== null && articleId.length > 0) { - LocalSearchAPI.lookupSearch(articleId, 'https://www.fns-cloud.eu/study').then(extractArticleIri); - } - }; - - useEffect(() => { - if (articleIri !== '') { - handleOpenModal(); - } else { - handleCloseModal(); - } - }, [articleIri]); - - const renderModal = () => ( - -
- - - - - - -
-
- ); - - const renderMessages = () => { - return ( -
- {messages && - messages.map(message => { - if (message.userInput && message.userInput.input) { - return ( -
- - {'> ' + message.userInput.input} - -
- ); - } - if (message.reply && message.reply.summary && message.reply.summary.summaryText) { - return ( -
- {message.reply.summary.summaryText} -
- ); - } - return null; - })} -
- ); - }; - - const renderArticleReferences = () => { - return ( -
- {responseArticles && responseArticles.length > 0 && ( - Source studies: - )} -
- {responseArticles && - responseArticles.map( - article => - article && - article.content && - article.content.length > 0 && ( -
-
showArticle(article.title)}> - Id: {article.title} - {article.content} -
-
- ) - )} -
-
- ); - }; - - const renderHistoryList = () => { - return ( - } - > - {conversationHistory && conversationHistory.length > 0 - ? conversationHistory.map(item => ( - prepareRestoreChat(item.id)} - > -
- - {item.start} - -
{item.topic}
-
- deleteChat(item.id)} - > - - -
- )) - : null} -
- ); - }; - - useEffect(() => { - getAllConversationHistory(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [conversationHistory]); - - useEffect(() => { - if (restoreChatStatus) { - restoreChat(conversationId); - } - setRestoreChatStatus(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [conversationId]); - - return ( - - - - - - - - - - { - processSearchQueryChange(event.target.value); - }} - onKeyDown={event => { - if (event.key === 'Enter') { - processSearchQueryChange(event.target.value); - prepareFetchSearch(); - } - }} - value={query} - /> - prepareFetchSearch()} - > - - - - - - - - {renderHistoryList()} - - - - - - - {responseMessage} - - - {renderMessages()} - - - {responseArticles && responseArticles.length > 0 && renderArticleReferences()} - - - -
{renderModal()}
-
- ); -}; - -export default withStyles(styles)(Conversation); diff --git a/projects/mercury/src/llm/Conversation.styles.js b/projects/mercury/src/llm/Conversation.styles.js deleted file mode 100644 index e5e6ac4dd..000000000 --- a/projects/mercury/src/llm/Conversation.styles.js +++ /dev/null @@ -1,156 +0,0 @@ -import {alpha} from '@mui/material/styles'; -import * as consts from '../constants'; - -const styles = theme => ({ - customFont: { - fontFamily: 'sans-serif' - }, - mainPage: { - width: consts.MAIN_CONTENT_WIDTH, - maxWidth: '700px', - maxHeight: consts.MAIN_CONTENT_MAX_HEIGHT, - padding: 20, - marginTop: 20 - }, - header: { - borderBottom: '2px solid ' + theme.palette.primary.main, - marginBottom: 50 - }, - outerDiv: { - display: 'flex', - justifyContent: 'center', - width: '100%' - }, - mainGrid: { - width: '100%' - }, - searchContainer: { - padding: 20, - maxWidth: 1200, - width: '100%' - }, - gridRow: { - minHeight: '200px' - }, - gridCell: { - padding: 20, - marginBottom: 20 - }, - historyList: { - width: 295, - maxWidth: '100%', - maxHeight: '1200px', - display: 'block', - position: 'relative', - overflow: 'auto', - marginRight: '25px' - }, - historyListContainer: { - borderTop: '1.5px solid ' + theme.palette.primary.light - // backgroundColor: theme.palette.mellow.light - }, - historyListItem: { - borderColor: theme.palette.primary.dark, - backgroundColor: 'white', - borderRadius: 5, - borderStyle: 'solid', - borderWidth: 1, - marginBottom: 6, - paddingBottom: 6, - marginRight: 8 - }, - deleteHistoryButton: { - position: 'absolute', - marginLeft: '75%' - }, - newConversation: { - display: 'flex', - height: 40, - padding: 0, - top: 0 - }, - searchSection: { - padding: 10, - marginBottom: 20, - borderRadius: 15, - maxWidth: 500, - backgroundColor: alpha(theme.palette.primary.main, 0.15) - }, - allResults: { - marginTop: 20, - marginBottom: 20, - width: '100%' - }, - articleContainer: { - overflow: 'auto' - }, - chatArticle: { - backgroundColor: theme.palette.mellow.light, - margin: 0, - marginBottom: 10, - paddingLeft: 10, - paddingRight: 10, - // mouse pointer on hover click - cursor: 'pointer' - }, - chatResponse: { - backgroundColor: 'white', - width: '100%', - height: '100%' - }, - chatInput: { - backgroundColor: 'white', - marginBottom: 10 - }, - chatReply: { - marginLeft: 30, - marginBottom: 10 - }, - searchIcon: { - marginTop: 8, - float: 'right' - }, - searchInput: { - width: 'calc(100% - 50px)' - }, - responseMessage: { - color: theme.palette.primary.main, - fontWeight: 'bold', - marginBottom: 10 - }, - modalDialog: { - background: theme.palette.primary.main, - position: 'relative', - top: 0, - width: 800, - bgcolor: 'primary', - border: '0px solid #000', - boxShadow: 0, - outline: 'none', - p: 4 - }, - modalContent: { - position: 'relative', - top: '10%', - left: '50%', - transform: 'translate(-50%, 0px)', - maxHeight: '80%', - padding: 2, - backgroundColor: theme.palette.primary.main, - width: 800, - overflowY: 'auto' - }, - closeButton: { - float: 'right', - marginTop: 8, - marginRight: 8 - }, - clickableDiv: { - cursor: 'pointer', - '&:hover': { - textDecoration: 'underline' - } - } -}); - -export default styles; diff --git a/projects/mercury/src/llm/LlmSearchPage.js b/projects/mercury/src/llm/LlmSearchPage.js deleted file mode 100644 index 85a49aac8..000000000 --- a/projects/mercury/src/llm/LlmSearchPage.js +++ /dev/null @@ -1,32 +0,0 @@ -import React, {useContext} from 'react'; - -import {Grid} from '@mui/material'; -import MetadataViewContext from '../metadata/views/MetadataViewContext'; -import UserContext from '../users/UserContext'; -import Conversation from './Conversation'; -import {getSearchQueryFromString} from '../search/searchUtils'; - -const LlmSearchPage = props => { - const {currentUser, location: {search} = ''} = props; - const query = getSearchQueryFromString(search); - const {views} = useContext(MetadataViewContext); - const canViewMetadata = currentUser && currentUser.canViewPublicMetadata && views && views.length > 0; - - return ( - - - - {canViewMetadata && } - - - - ); -}; - -const ContextualLlmSearchPage = props => { - const {currentUser} = useContext(UserContext); - - return ; -}; - -export default ContextualLlmSearchPage; diff --git a/projects/mercury/src/routes/WorkspaceRoutes.js b/projects/mercury/src/routes/WorkspaceRoutes.js index 3c0f484b5..d9d6e5083 100644 --- a/projects/mercury/src/routes/WorkspaceRoutes.js +++ b/projects/mercury/src/routes/WorkspaceRoutes.js @@ -5,7 +5,7 @@ import * as queryString from 'query-string'; import WorkspaceOverview from '../workspaces/WorkspaceOverview'; import Collections from '../collections/CollectionsPage'; import Dashboard from '../dashboard/DashboardPage'; -import LlmSearchPage from '../llm/LlmSearchPage'; +import LlmSearchPage from '../ask-ai/AskAIPage'; import FilesPage from '../file/FilesPage'; import {MetadataWrapper} from '../metadata/LinkedDataWrapper'; import LinkedDataEntityPage from '../metadata/common/LinkedDataEntityPage';