add return source documents functioanality

This commit is contained in:
Henry
2023-06-08 23:51:34 +01:00
parent ab00214ec2
commit b071790a5a
11 changed files with 371 additions and 91 deletions
@@ -27,9 +27,10 @@ import PerfectScrollbar from 'react-perfect-scrollbar'
import MainCard from 'ui-component/cards/MainCard'
import Transitions from 'ui-component/extended/Transitions'
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
import AboutDialog from 'ui-component/dialog/AboutDialog'
// assets
import { IconLogout, IconSettings, IconFileExport, IconFileDownload } from '@tabler/icons'
import { IconLogout, IconSettings, IconFileExport, IconFileDownload, IconInfoCircle } from '@tabler/icons'
// API
import databaseApi from 'api/database'
@@ -49,6 +50,7 @@ const ProfileSection = ({ username, handleLogout }) => {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [aboutDialogOpen, setAboutDialogOpen] = useState(false)
const anchorRef = useRef(null)
const uploadRef = useRef(null)
@@ -215,6 +217,18 @@ const ProfileSection = ({ username, handleLogout }) => {
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Export Database</Typography>} />
</ListItemButton>
<ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={() => {
setOpen(false)
setAboutDialogOpen(true)
}}
>
<ListItemIcon>
<IconInfoCircle stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>About Flowise</Typography>} />
</ListItemButton>
{localStorage.getItem('username') && localStorage.getItem('password') && (
<ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }}
@@ -237,6 +251,7 @@ const ProfileSection = ({ username, handleLogout }) => {
</Popper>
<input ref={uploadRef} type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} />
<BackdropLoader open={loading} />
<AboutDialog show={aboutDialogOpen} onCancel={() => setAboutDialogOpen(false)} />
</>
)
}
@@ -0,0 +1,85 @@
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Dialog, DialogContent, DialogTitle, TableContainer, Table, TableHead, TableRow, TableCell, TableBody, Paper } from '@mui/material'
import moment from 'moment'
import axios from 'axios'
const fetchLatestVer = async ({ api }) => {
let apiReturn = await axios
.get(api)
.then(async function (response) {
return response.data
})
.catch(function (error) {
console.error(error)
})
return apiReturn
}
const AboutDialog = ({ show, onCancel }) => {
const portalElement = document.getElementById('portal')
const [data, setData] = useState({})
useEffect(() => {
if (show) {
const fetchData = async (api) => {
let response = await fetchLatestVer({ api })
setData(response)
}
fetchData('https://api.github.com/repos/FlowiseAI/Flowise/releases/latest')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [show])
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
Flowise Version
</DialogTitle>
<DialogContent>
{data && (
<TableContainer component={Paper}>
<Table aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Latest Version</TableCell>
<TableCell>Published At</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component='th' scope='row'>
<a target='_blank' rel='noreferrer' href={data.html_url}>
{data.name}
</a>
</TableCell>
<TableCell>{moment(data.published_at).fromNow()}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
)}
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
AboutDialog.propTypes = {
show: PropTypes.bool,
onCancel: PropTypes.func
}
export default AboutDialog
@@ -0,0 +1,57 @@
import { createPortal } from 'react-dom'
import { useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
import ReactJson from 'react-json-view'
const SourceDocDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const customization = useSelector((state) => state.customization)
const [data, setData] = useState({})
useEffect(() => {
if (dialogProps.data) setData(dialogProps.data)
return () => {
setData({})
}
}, [dialogProps])
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
Source Document
</DialogTitle>
<DialogContent>
<ReactJson
theme={customization.isDarkMode ? 'ocean' : 'rjv-default'}
style={{ padding: 10, borderRadius: 10 }}
src={data}
name={null}
quotesOnKeys={false}
enableClipboard={false}
displayDataTypes={false}
/>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
SourceDocDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func
}
export default SourceDocDialog
+126 -69
View File
@@ -7,13 +7,14 @@ import rehypeMathjax from 'rehype-mathjax'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box } from '@mui/material'
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconSend } from '@tabler/icons'
// project import
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
import SourceDocDialog from 'ui-component/dialog/SourceDocDialog'
import './ChatMessage.css'
// api
@@ -43,11 +44,18 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
])
const [socketIOClientId, setSocketIOClientId] = useState('')
const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false)
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
const [sourceDialogProps, setSourceDialogProps] = useState({})
const inputRef = useRef(null)
const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow)
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
const onSourceDialogClick = (data) => {
setSourceDialogProps({ data })
setSourceDialogOpen(true)
}
const scrollToBottom = () => {
if (ps.current) {
ps.current.scrollTo({ top: maxScroll })
@@ -56,13 +64,14 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput])
const addChatMessage = async (message, type) => {
const addChatMessage = async (message, type, sourceDocuments) => {
try {
const newChatMessageBody = {
role: type,
content: message,
chatflowid: chatflowid
}
if (sourceDocuments) newChatMessageBody.sourceDocuments = JSON.stringify(sourceDocuments)
await chatmessageApi.createNewChatmessage(chatflowid, newChatMessageBody)
} catch (error) {
console.error(error)
@@ -78,6 +87,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
})
}
const updateLastMessageSourceDocuments = (sourceDocuments) => {
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages
allMessages[allMessages.length - 1].sourceDocuments = sourceDocuments
return allMessages
})
}
// Handle errors
const handleError = (message = 'Oops! There seems to be an error. Please try again.') => {
message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '')
@@ -114,8 +132,20 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (response.data) {
const data = response.data
if (!isChatFlowAvailableToStream) setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
addChatMessage(data, 'apiMessage')
if (typeof data === 'object' && data.text && data.sourceDocuments) {
if (!isChatFlowAvailableToStream) {
setMessages((prevMessages) => [
...prevMessages,
{ message: data.text, sourceDocuments: data.sourceDocuments, type: 'apiMessage' }
])
}
addChatMessage(data.text, 'apiMessage', data.sourceDocuments)
} else {
if (!isChatFlowAvailableToStream) {
setMessages((prevMessages) => [...prevMessages, { message: data, type: 'apiMessage' }])
}
addChatMessage(data, 'apiMessage')
}
setLoading(false)
setUserInput('')
setTimeout(() => {
@@ -146,10 +176,12 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (getChatmessageApi.data) {
const loadedMessages = []
for (const message of getChatmessageApi.data) {
loadedMessages.push({
const obj = {
message: message.content,
type: message.role
})
}
if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments)
loadedMessages.push(obj)
}
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
}
@@ -196,6 +228,8 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }])
})
socket.on('sourceDocuments', updateLastMessageSourceDocuments)
socket.on('token', updateLastMessage)
}
@@ -225,69 +259,91 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
messages.map((message, index) => {
return (
// The latest message sent by the user will be animated while waiting for a response
<Box
sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
}}
key={index}
style={{ display: 'flex' }}
className={
message.type === 'userMessage' && loading && index === messages.length - 1
? customization.isDarkMode
? 'usermessagewaiting-dark'
: 'usermessagewaiting-light'
: message.type === 'usermessagewaiting'
? 'apimessage'
: 'usermessage'
}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
alt='AI'
width='30'
height='30'
className='boticon'
/>
) : (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
alt='Me'
width='30'
height='30'
className='usericon'
/>
)}
<div className='markdownanswer'>
{/* Messages are being rendered in Markdown format */}
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
chatflowid={chatflowid}
isDialog={isDialog}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{message.message}
</MemoizedReactMarkdown>
</div>
</Box>
<>
<Box
sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
}}
key={index}
style={{ display: 'flex' }}
className={
message.type === 'userMessage' && loading && index === messages.length - 1
? customization.isDarkMode
? 'usermessagewaiting-dark'
: 'usermessagewaiting-light'
: message.type === 'usermessagewaiting'
? 'apimessage'
: 'usermessage'
}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png'
alt='AI'
width='30'
height='30'
className='boticon'
/>
) : (
<img
src='https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png'
alt='Me'
width='30'
height='30'
className='usericon'
/>
)}
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
<div className='markdownanswer'>
{/* Messages are being rendered in Markdown format */}
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
chatflowid={chatflowid}
isDialog={isDialog}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{message.message}
</MemoizedReactMarkdown>
</div>
{message.sourceDocuments && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{message.sourceDocuments.map((source, index) => {
return (
<Chip
size='small'
key={index}
label={`${source.pageContent.substring(0, 15)}...`}
component='a'
sx={{ mr: 1, mb: 1 }}
variant='outlined'
clickable
onClick={() => onSourceDialogClick(source)}
/>
)
})}
</div>
)}
</div>
</Box>
</>
)
})}
</div>
@@ -328,6 +384,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
</form>
</div>
</div>
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
</>
)
}