code cleanup

This commit is contained in:
Henry 2023-11-03 01:33:08 +00:00
parent 6ab20cd445
commit 4f6cab47f8
16 changed files with 942 additions and 89 deletions

View File

@ -9,7 +9,7 @@ import { Server } from 'socket.io'
import logger from './utils/logger'
import { expressRequestLogger } from './utils/logger'
import { v4 as uuidv4 } from 'uuid'
import { Between, IsNull, FindOptionsWhere } from 'typeorm'
import {
IChatFlow,
IncomingInput,
@ -18,7 +18,9 @@ import {
INodeData,
IDatabaseExport,
ICredentialReturnResponse,
chatType
chatType,
IChatMessage,
IReactFlowEdge
} from './Interface'
import {
getNodeModulesPackagePath,
@ -42,10 +44,11 @@ import {
getApiKey,
transformToCredentialEntity,
decryptCredentialData,
clearSessionMemory,
clearAllSessionMemory,
replaceInputsWithConfig,
getEncryptionKey,
checkMemorySessionId
checkMemorySessionId,
clearSessionMemoryFromViewMessageDialog
} from './utils'
import { cloneDeep, omit } from 'lodash'
import { getDataSource } from './DataSource'
@ -397,7 +400,39 @@ export class App {
// Get all chatmessages from chatflowid
this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
const chatmessages = await this.getChatMessage(req.params.id, undefined)
const sortOrder = req.query?.order as string | undefined
const chatId = req.query?.chatId as string | undefined
const memoryType = req.query?.memoryType as string | undefined
const sessionId = req.query?.sessionId as string | undefined
const startDate = req.query?.startDate as string | undefined
const endDate = req.query?.endDate as string | undefined
let chatTypeFilter = req.query?.chatType as chatType | undefined
if (chatTypeFilter) {
try {
const chatTypeFilterArray = JSON.parse(chatTypeFilter)
if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) {
chatTypeFilter = undefined
} else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) {
chatTypeFilter = chatType.EXTERNAL
} else if (chatTypeFilterArray.includes(chatType.INTERNAL)) {
chatTypeFilter = chatType.INTERNAL
}
} catch (e) {
return res.status(500).send(e)
}
}
const chatmessages = await this.getChatMessage(
req.params.id,
chatTypeFilter,
sortOrder,
chatId,
memoryType,
sessionId,
startDate,
endDate
)
return res.json(chatmessages)
})
@ -416,27 +451,41 @@ export class App {
// Delete all chatmessages from chatId
this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
const chatId = req.params.id
const chatMessage = await this.AppDataSource.getRepository(ChatMessage).findOneBy({
chatId: chatId
})
if (!chatMessage) {
res.status(404).send(`chatmessage ${chatId} not found`)
return
}
const chatflowid = req.params.id
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: chatMessage.chatflowid
id: chatflowid
})
if (!chatflow) {
res.status(404).send(`Chatflow ${chatMessage.chatflowid} not found`)
res.status(404).send(`Chatflow ${chatflowid} not found`)
return
}
const chatId = (req.query?.chatId as string) ?? (await getChatId(chatflowid))
const memoryType = req.query?.memoryType as string | undefined
const sessionId = req.query?.sessionId as string | undefined
const chatType = req.query?.chatType as string | undefined
const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined
const flowData = chatflow.flowData
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
const nodes = parsedFlowData.nodes
clearSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, req.query.sessionId as string)
const results = await this.AppDataSource.getRepository(ChatMessage).delete({ chatId: chatId })
if (isClearFromViewMessageDialog)
clearSessionMemoryFromViewMessageDialog(
nodes,
this.nodesPool.componentNodes,
chatId,
this.AppDataSource,
sessionId,
memoryType
)
else clearAllSessionMemory(nodes, this.nodesPool.componentNodes, chatId, this.AppDataSource, sessionId)
const deleteOptions: FindOptionsWhere<ChatMessage> = { chatflowid, chatId }
if (memoryType) deleteOptions.memoryType = memoryType
if (sessionId) deleteOptions.sessionId = sessionId
if (chatType) deleteOptions.chatType = chatType
const results = await this.AppDataSource.getRepository(ChatMessage).delete(deleteOptions)
return res.json(results)
})
@ -831,30 +880,51 @@ export class App {
/**
* Method that get chat messages.
*
* @param chatflowid -
* @param chatType -
*
* @param {string} chatflowid
* @param {chatType} chatType
* @param {string} sortOrder
* @param {string} chatId
* @param {string} memoryType
* @param {string} sessionId
* @param {string} startDate
* @param {string} endDate
*/
async getChatMessage(chatflowid: string, chatType: chatType | undefined): Promise<ChatMessage[]> {
async getChatMessage(
chatflowid: string,
chatType: chatType | undefined,
sortOrder: string = 'ASC',
chatId?: string,
memoryType?: string,
sessionId?: string,
startDate?: string,
endDate?: string
): Promise<ChatMessage[]> {
let fromDate
if (startDate) fromDate = new Date(startDate)
let toDate
if (endDate) toDate = new Date(endDate)
return await this.AppDataSource.getRepository(ChatMessage).find({
where: {
chatflowid: chatflowid,
chatType: chatType
chatflowid,
chatType,
chatId,
memoryType: memoryType ?? (chatId ? IsNull() : undefined),
sessionId: sessionId ?? (chatId ? IsNull() : undefined),
createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined
},
order: {
createdDate: 'ASC'
createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
}
})
}
/**
* Method that add chat messages.
*
* @param chatMessage -
*
* @param {Partial<IChatMessage>} chatMessage
*/
async addChatMessage(chatMessage: any): Promise<ChatMessage> {
async addChatMessage(chatMessage: Partial<IChatMessage>): Promise<ChatMessage> {
const newChatMessage = new ChatMessage()
Object.assign(newChatMessage, chatMessage)
@ -863,18 +933,24 @@ export class App {
}
/**
* Method that find memory label.
*
* @param nodes -
*
* Method that find memory label that is connected within chatflow
* In a chatflow, there should only be 1 memory node
* @param {IReactFlowNode[]} nodes
* @param {IReactFlowEdge[]} edges
* @returns {string | undefined}
*/
findMemoryLabel(nodes: any[]): string | undefined {
const memoryNode = nodes.find((node) => {
return node.data.category === 'Memory'
})
findMemoryLabel(nodes: IReactFlowNode[], edges: IReactFlowEdge[]): string | undefined {
const memoryNodes = nodes.filter((node) => node.data.category === 'Memory')
const memoryNodeIds = memoryNodes.map((mem) => mem.data.id)
for (const edge of edges) {
if (memoryNodeIds.includes(edge.source)) {
const memoryNode = nodes.find((node) => node.data.id === edge.source)
return memoryNode ? memoryNode.data.label : undefined
}
}
return undefined
}
/**
* Process Prediction
@ -883,7 +959,7 @@ export class App {
* @param {Server} socketIO
* @param {boolean} isInternal
*/
async processPrediction(req: Request, res: Response, socketIO?: Server, isInternal = false) {
async processPrediction(req: Request, res: Response, socketIO?: Server, isInternal: boolean = false) {
try {
const chatflowid = req.params.id
let incomingInput: IncomingInput = req.body
@ -895,7 +971,7 @@ export class App {
})
if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
const chatId = incomingInput.chatId ?? uuidv4()
const chatId = incomingInput.chatId ?? incomingInput.overrideConfig?.sessionId ?? uuidv4()
const userMessageDateTime = new Date()
if (!isInternal) {
@ -1033,9 +1109,9 @@ export class App {
logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
let sessionId = undefined
let memoryLabel = undefined
if (nodeToExecuteData.instance) sessionId = checkMemorySessionId(nodeToExecuteData.instance, chatId)
if (sessionId) memoryLabel = this.findMemoryLabel(nodes)
const memoryType = this.findMemoryLabel(nodes, edges)
let result = isStreamValid
? await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
@ -1057,31 +1133,35 @@ export class App {
result = typeof result === 'string' ? { text: result } : result
await this.addChatMessage({
const userMessage: Omit<IChatMessage, 'id'> = {
role: 'userMessage',
content: incomingInput.question,
chatflowid: chatflowid,
chatflowid,
chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
chatId: chatId,
memoryType: memoryLabel ? memoryLabel : undefined,
sessionId: sessionId ? sessionId : undefined,
chatId,
memoryType,
sessionId,
createdDate: userMessageDateTime
})
}
await this.addChatMessage(userMessage)
const apiMessage: any = {
const apiMessage: Omit<IChatMessage, 'id' | 'createdDate'> = {
role: 'apiMessage',
content: result.text,
chatflowid: chatflowid,
chatflowid,
chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
chatId: chatId,
memoryType: memoryLabel ? memoryLabel : undefined,
sessionId: sessionId ? sessionId : undefined
chatId,
memoryType,
sessionId
}
if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments)
await this.addChatMessage(apiMessage)
logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
if (incomingInput.chatId) result.chatId = chatId
// Only return ChatId when its Internal OR incoming input has ChatId, to avoid confusion when calling API
if (incomingInput.chatId || isInternal) result.chatId = chatId
return res.json(result)
} catch (e: any) {
logger.error('[server]: Error:', e)
@ -1104,7 +1184,7 @@ export class App {
* @param {string} chatflowid
* @returns {string}
*/
export async function getChatId(chatflowid: string) {
export async function getChatId(chatflowid: string): Promise<string> {
// first chatmessage id as the unique chat id
const firstChatMessage = await getDataSource()
.getRepository(ChatMessage)

View File

@ -298,14 +298,14 @@ export const buildLangchain = async (
}
/**
* Clear memory
* Clear all session memories on the canvas
* @param {IReactFlowNode[]} reactFlowNodes
* @param {IComponentNodes} componentNodes
* @param {string} chatId
* @param {DataSource} appDataSource
* @param {string} sessionId
*/
export const clearSessionMemory = async (
export const clearAllSessionMemory = async (
reactFlowNodes: IReactFlowNode[],
componentNodes: IComponentNodes,
chatId: string,
@ -317,10 +317,47 @@ export const clearSessionMemory = async (
const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newNodeInstance = new nodeModule.nodeClass()
if (sessionId && node.data.inputs) node.data.inputs.sessionId = sessionId
if (newNodeInstance.clearSessionMemory)
if (newNodeInstance.clearSessionMemory) {
await newNodeInstance?.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger })
}
}
}
/**
* Clear specific session memory from View Message Dialog UI
* @param {IReactFlowNode[]} reactFlowNodes
* @param {IComponentNodes} componentNodes
* @param {string} chatId
* @param {DataSource} appDataSource
* @param {string} sessionId
* @param {string} memoryType
*/
export const clearSessionMemoryFromViewMessageDialog = async (
reactFlowNodes: IReactFlowNode[],
componentNodes: IComponentNodes,
chatId: string,
appDataSource: DataSource,
sessionId?: string,
memoryType?: string
) => {
if (!sessionId) return
for (const node of reactFlowNodes) {
if (node.data.category !== 'Memory') continue
if (node.data.label !== memoryType) continue
const nodeInstanceFilePath = componentNodes[node.data.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newNodeInstance = new nodeModule.nodeClass()
if (sessionId && node.data.inputs) node.data.inputs.sessionId = sessionId
if (newNodeInstance.clearSessionMemory) {
await newNodeInstance?.clearSessionMemory(node.data, { chatId, appDataSource, databaseEntities, logger })
return
}
}
}
/**
@ -933,5 +970,5 @@ export const checkMemorySessionId = (instance: any, chatId: string): string => {
instance.memory.sessionId = chatId
instance.memory.chatHistory.sessionId = chatId
}
return instance.memory.sessionId
return instance.memory ? instance.memory.sessionId ?? instance.memory.chatHistory.sessionId : undefined
}

View File

@ -31,7 +31,7 @@
"react": "^18.2.0",
"react-code-blocks": "^0.0.9-0",
"react-color": "^2.19.3",
"react-datepicker": "^4.8.0",
"react-datepicker": "^4.21.0",
"react-device-detect": "^1.17.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.6",

View File

@ -1,10 +1,13 @@
import client from './client'
const getChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`)
const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`)
const getInternalChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`)
const getAllChatmessageFromChatflow = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'DESC', ...params } })
const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', ...params } })
const deleteChatmessage = (id, params = {}) => client.delete(`/chatmessage/${id}`, { params: { ...params } })
export default {
getChatmessageFromChatflow,
getInternalChatmessageFromChatflow,
getAllChatmessageFromChatflow,
getChatmessageFromPK,
deleteChatmessage
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -71,7 +71,7 @@ const ProfileSection = ({ username, handleLogout }) => {
try {
const response = await databaseApi.getExportDatabase()
const exportItems = response.data
let dataStr = JSON.stringify(exportItems)
let dataStr = JSON.stringify(exportItems, null, 2)
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
let exportFileDefaultName = `DB.json`

View File

@ -1,8 +1,8 @@
// assets
import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch } from '@tabler/icons'
import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } from '@tabler/icons'
// constant
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch }
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage }
// ==============================|| SETTINGS MENU ITEMS ||============================== //
@ -11,6 +11,13 @@ const settings = {
title: '',
type: 'group',
children: [
{
id: 'viewMessages',
title: 'View Messages',
type: 'item',
url: '',
icon: icons.IconMessage
},
{
id: 'duplicateChatflow',
title: 'Duplicate Chatflow',

View File

@ -80,6 +80,9 @@ export default function themePalette(theme) {
asyncSelect: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.grey50
},
timeMessage: {
main: theme.customization.isDarkMode ? theme.colors?.darkLevel2 : theme.colors?.grey200
},
canvasHeader: {
deployLight: theme.colors?.primaryLight,
deployDark: theme.colors?.primaryDark,

View File

@ -0,0 +1,695 @@
import { createPortal } from 'react-dom'
import { useDispatch, useSelector } from 'react-redux'
import { useState, useEffect, forwardRef } from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import rehypeMathjax from 'rehype-mathjax'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
// material-ui
import {
Button,
Tooltip,
ListItemButton,
Box,
Stack,
Dialog,
DialogContent,
DialogTitle,
ListItem,
ListItemText,
Chip
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import DatePicker from 'react-datepicker'
import robotPNG from 'assets/images/robot.png'
import userPNG from 'assets/images/account.png'
import msgEmptySVG from 'assets/images/message_empty.svg'
import { IconFileExport, IconEraser, IconX } from '@tabler/icons'
// Project import
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
import SourceDocDialog from 'ui-component/dialog/SourceDocDialog'
import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown'
import { StyledButton } from 'ui-component/button/StyledButton'
// store
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
// API
import chatmessageApi from 'api/chatmessage'
import useApi from 'hooks/useApi'
import useConfirm from 'hooks/useConfirm'
// Utils
import { isValidURL, removeDuplicateURL } from 'utils/genericHelper'
import useNotifier from 'utils/useNotifier'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import 'views/chatmessage/ChatMessage.css'
import 'react-datepicker/dist/react-datepicker.css'
const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) {
return (
<ListItemButton style={{ borderRadius: 15, border: '1px solid #e0e0e0' }} onClick={onClick} ref={ref}>
{value}
</ListItemButton>
)
})
DatePickerCustomInput.propTypes = {
value: PropTypes.string,
onClick: PropTypes.func
}
const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const { confirm } = useConfirm()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [chatlogs, setChatLogs] = useState([])
const [allChatlogs, setAllChatLogs] = useState([])
const [chatMessages, setChatMessages] = useState([])
const [selectedMessageIndex, setSelectedMessageIndex] = useState(0)
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
const [sourceDialogProps, setSourceDialogProps] = useState({})
const [chatTypeFilter, setChatTypeFilter] = useState([])
const [startDate, setStartDate] = useState(new Date().setMonth(new Date().getMonth() - 1))
const [endDate, setEndDate] = useState(new Date())
const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow)
const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK)
const onStartDateSelected = (date) => {
setStartDate(date)
getChatmessageApi.request(dialogProps.chatflow.id, {
startDate: date,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
})
}
const onEndDateSelected = (date) => {
setEndDate(date)
getChatmessageApi.request(dialogProps.chatflow.id, {
endDate: date,
startDate: startDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
})
}
const onChatTypeSelected = (chatTypes) => {
setChatTypeFilter(chatTypes)
getChatmessageApi.request(dialogProps.chatflow.id, {
chatType: chatTypes.length ? chatTypes : undefined,
startDate: startDate,
endDate: endDate
})
}
const exportMessages = () => {
const obj = {}
for (let i = 0; i < allChatlogs.length; i += 1) {
const chatmsg = allChatlogs[i]
const chatPK = getChatPK(chatmsg)
const msg = {
content: chatmsg.content,
role: chatmsg.role === 'apiMessage' ? 'bot' : 'user',
time: chatmsg.createdDate
}
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) {
obj[chatPK] = {
id: chatmsg.chatId,
source: chatmsg.chatType === 'INTERNAL' ? 'UI' : 'API/Embed',
sessionId: chatmsg.sessionId ?? null,
memoryType: chatmsg.memoryType ?? null,
messages: [msg]
}
} else if (Object.prototype.hasOwnProperty.call(obj, chatPK)) {
obj[chatPK].messages = [...obj[chatPK].messages, msg]
}
}
const exportMessages = []
for (const key in obj) {
exportMessages.push({
...obj[key]
})
}
for (let i = 0; i < exportMessages.length; i += 1) {
exportMessages[i].messages = exportMessages[i].messages.reverse()
}
const dataStr = JSON.stringify(exportMessages, null, 2)
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
const exportFileDefaultName = `${dialogProps.chatflow.id}-Message.json`
let linkElement = document.createElement('a')
linkElement.setAttribute('href', dataUri)
linkElement.setAttribute('download', exportFileDefaultName)
linkElement.click()
}
const clearChat = async (chatmsg) => {
const description =
chatmsg.sessionId && chatmsg.memoryType
? `Are you sure you want to clear session id: ${chatmsg.sessionId} from ${chatmsg.memoryType}?`
: `Are you sure you want to clear messages?`
const confirmPayload = {
title: `Clear Session`,
description,
confirmButtonName: 'Clear',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
const chatflowid = dialogProps.chatflow.id
if (isConfirmed) {
try {
const obj = { chatflowid, isClearFromViewMessageDialog: true }
if (chatmsg.chatId) obj.chatId = chatmsg.chatId
if (chatmsg.chatType) obj.chatType = chatmsg.chatType
if (chatmsg.memoryType) obj.memoryType = chatmsg.memoryType
if (chatmsg.sessionId) obj.sessionId = chatmsg.sessionId
await chatmessageApi.deleteChatmessage(chatflowid, obj)
const description =
chatmsg.sessionId && chatmsg.memoryType
? `Succesfully cleared session id: ${chatmsg.sessionId} from ${chatmsg.memoryType}`
: `Succesfully cleared messages`
enqueueSnackbar({
message: description,
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
getChatmessageApi.request(chatflowid)
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: errorData,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const getChatMessages = (chatmessages) => {
let prevDate = ''
const loadedMessages = []
for (let i = 0; i < chatmessages.length; i += 1) {
const chatmsg = chatmessages[i]
if (!prevDate) {
prevDate = chatmsg.createdDate.split('T')[0]
loadedMessages.push({
message: chatmsg.createdDate,
type: 'timeMessage'
})
} else {
const currentDate = chatmsg.createdDate.split('T')[0]
if (currentDate !== prevDate) {
prevDate = currentDate
loadedMessages.push({
message: chatmsg.createdDate,
type: 'timeMessage'
})
}
}
const obj = {
...chatmsg,
message: chatmsg.content,
type: chatmsg.role
}
if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
loadedMessages.push(obj)
}
setChatMessages(loadedMessages)
}
const getChatPK = (chatmsg) => {
const chatId = chatmsg.chatId
const memoryType = chatmsg.memoryType ?? 'null'
const sessionId = chatmsg.sessionId ?? 'null'
return `${chatId}_${memoryType}_${sessionId}`
}
const transformChatPKToParams = (chatPK) => {
const chatId = chatPK.split('_')[0]
const memoryType = chatPK.split('_')[1]
const sessionId = chatPK.split('_')[2]
const params = { chatId }
if (memoryType !== 'null') params.memoryType = memoryType
if (sessionId !== 'null') params.sessionId = sessionId
return params
}
const processChatLogs = (allChatMessages) => {
const seen = {}
const filteredChatLogs = []
for (let i = 0; i < allChatMessages.length; i += 1) {
const PK = getChatPK(allChatMessages[i])
const item = allChatMessages[i]
if (!Object.prototype.hasOwnProperty.call(seen, PK)) {
seen[PK] = {
counter: 1,
item: allChatMessages[i]
}
} else if (Object.prototype.hasOwnProperty.call(seen, PK) && seen[PK].counter === 1) {
seen[PK] = {
counter: 2,
item: {
...seen[PK].item,
apiContent:
seen[PK].item.role === 'apiMessage' ? `Bot: ${seen[PK].item.content}` : `User: ${seen[PK].item.content}`,
userContent: item.role === 'apiMessage' ? `Bot: ${item.content}` : `User: ${item.content}`
}
}
filteredChatLogs.push(seen[PK].item)
}
}
setChatLogs(filteredChatLogs)
if (filteredChatLogs.length) return getChatPK(filteredChatLogs[0])
return undefined
}
const handleItemClick = (idx, chatmsg) => {
setSelectedMessageIndex(idx)
getChatmessageFromPKApi.request(dialogProps.chatflow.id, transformChatPKToParams(getChatPK(chatmsg)))
}
const onURLClick = (data) => {
window.open(data, '_blank')
}
const onSourceDialogClick = (data) => {
setSourceDialogProps({ data })
setSourceDialogOpen(true)
}
useEffect(() => {
if (getChatmessageFromPKApi.data) {
getChatMessages(getChatmessageFromPKApi.data)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatmessageFromPKApi.data])
useEffect(() => {
if (getChatmessageApi.data) {
setAllChatLogs(getChatmessageApi.data)
const chatPK = processChatLogs(getChatmessageApi.data)
setSelectedMessageIndex(0)
if (chatPK) getChatmessageFromPKApi.request(dialogProps.chatflow.id, transformChatPKToParams(chatPK))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatmessageApi.data])
useEffect(() => {
if (dialogProps.chatflow) {
getChatmessageApi.request(dialogProps.chatflow.id)
}
return () => {
setChatLogs([])
setAllChatLogs([])
setChatMessages([])
setChatTypeFilter([])
setSelectedMessageIndex(0)
setStartDate(new Date().setMonth(new Date().getMonth() - 1))
setEndDate(new Date())
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth={chatlogs && chatlogs.length == 0 ? 'md' : 'lg'}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{dialogProps.title}
<div style={{ flex: 1 }} />
<Button variant='outlined' onClick={() => exportMessages()} startIcon={<IconFileExport />}>
Export
</Button>
</div>
</DialogTitle>
<DialogContent>
<>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%', marginBottom: 10 }}>
<div style={{ marginRight: 10 }}>
<b style={{ marginRight: 10 }}>From Date</b>
<DatePicker
selected={startDate}
onChange={(date) => onStartDateSelected(date)}
selectsStart
startDate={startDate}
endDate={endDate}
customInput={<DatePickerCustomInput />}
/>
</div>
<div style={{ marginRight: 10 }}>
<b style={{ marginRight: 10 }}>To Date</b>
<DatePicker
selected={endDate}
onChange={(date) => onEndDateSelected(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
minDate={startDate}
maxDate={new Date()}
customInput={<DatePickerCustomInput />}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', minWidth: '200px', marginRight: 10 }}>
<b style={{ marginRight: 10 }}>Source</b>
<MultiDropdown
key={JSON.stringify(chatTypeFilter)}
name='chatType'
options={[
{
label: 'UI',
name: 'INTERNAL'
},
{
label: 'API/Embed',
name: 'EXTERNAL'
}
]}
onSelect={(newValue) => onChatTypeSelected(newValue)}
value={chatTypeFilter}
formControlSx={{ mt: 0 }}
/>
</div>
<div style={{ flex: 1 }}></div>
</div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{chatlogs && chatlogs.length == 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
<Box sx={{ p: 5, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '20vh', width: 'auto' }}
src={msgEmptySVG}
alt='msgEmptySVG'
/>
</Box>
<div>No Messages</div>
</Stack>
)}
{chatlogs && chatlogs.length > 0 && (
<div style={{ flexBasis: '40%' }}>
<Box
sx={{
overflowY: 'auto',
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
maxHeight: 'calc(100vh - 260px)'
}}
>
{chatlogs.map((chatmsg, index) => (
<ListItemButton
key={index}
sx={{
p: 0,
borderRadius: `${customization.borderRadius}px`,
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
mt: 1,
ml: 1,
mr: 1,
mb: index === chatlogs.length - 1 ? 1 : 0
}}
selected={selectedMessageIndex === index}
onClick={() => handleItemClick(index, chatmsg)}
>
<ListItem alignItems='center'>
<ListItemText
primary={
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: 10 }}>
<span>{chatmsg?.userContent}</span>
<div
style={{
maxHeight: '100px',
maxWidth: '400px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
{chatmsg?.apiContent}
</div>
</div>
}
secondary={moment(chatmsg.createdDate).format('MMMM Do YYYY, h:mm:ss a')}
/>
</ListItem>
</ListItemButton>
))}
</Box>
</div>
)}
{chatlogs && chatlogs.length > 0 && (
<div style={{ flexBasis: '60%', paddingRight: '30px' }}>
{chatMessages && chatMessages.length > 1 && (
<div style={{ display: 'flex', flexDirection: 'row' }}>
<div style={{ flex: 1, marginLeft: '20px', marginBottom: '15px', marginTop: '10px' }}>
{chatMessages[1].sessionId && (
<div>
Session Id:&nbsp;<b>{chatMessages[1].sessionId}</b>
</div>
)}
{chatMessages[1].chatType && (
<div>
Source:&nbsp;<b>{chatMessages[1].chatType === 'INTERNAL' ? 'UI' : 'API/Embed'}</b>
</div>
)}
{chatMessages[1].memoryType && (
<div>
Memory:&nbsp;<b>{chatMessages[1].memoryType}</b>
</div>
)}
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignContent: 'center',
alignItems: 'end'
}}
>
<StyledButton
sx={{ height: 'max-content', width: 'max-content' }}
variant='outlined'
color='error'
title='Clear Message'
onClick={() => clearChat(chatMessages[1])}
startIcon={<IconEraser />}
>
Clear
</StyledButton>
{chatMessages[1].sessionId && (
<Tooltip
title={
'At your left 👈 you will see the Memory node that was used in this conversation. You need to have the matching Memory node with same parameters in the canvas, in order to delete the session conversations stored on the Memory node'
}
placement='bottom'
>
<h5 style={{ cursor: 'pointer', color: theme.palette.primary.main }}>
Why my session is not deleted?
</h5>
</Tooltip>
)}
</div>
</div>
)}
<div
style={{
marginLeft: '20px',
border: '1px solid #e0e0e0',
borderRadius: `${customization.borderRadius}px`
}}
className='cloud-message'
>
<div style={{ width: '100%', height: '100%' }}>
{chatMessages &&
chatMessages.map((message, index) => {
if (message.type === 'apiMessage' || message.type === 'userMessage') {
return (
<Box
sx={{
background:
message.type === 'apiMessage' ? theme.palette.asyncSelect.main : '',
pl: 1,
pr: 1
}}
key={index}
style={{ display: 'flex', justifyContent: 'center', alignContent: 'center' }}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img
style={{ marginLeft: '10px' }}
src={robotPNG}
alt='AI'
width='25'
height='25'
className='boticon'
/>
) : (
<img
style={{ marginLeft: '10px' }}
src={userPNG}
alt='Me'
width='25'
height='25'
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={dialogProps.chatflow.id}
isDialog={true}
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%' }}>
{removeDuplicateURL(message).map((source, index) => {
const URL = isValidURL(source.metadata.source)
return (
<Chip
size='small'
key={index}
label={
URL
? URL.pathname.substring(0, 15) === '/'
? URL.host
: `${URL.pathname.substring(0, 15)}...`
: `${source.pageContent.substring(0, 15)}...`
}
component='a'
sx={{ mr: 1, mb: 1 }}
variant='outlined'
clickable
onClick={() =>
URL
? onURLClick(source.metadata.source)
: onSourceDialogClick(source)
}
/>
)
})}
</div>
)}
</div>
</Box>
)
} else {
return (
<Box
sx={{
background: theme.palette.timeMessage.main,
p: 2
}}
key={index}
style={{ display: 'flex', justifyContent: 'center', alignContent: 'center' }}
>
{moment(message.message).format('MMMM Do YYYY, h:mm:ss a')}
</Box>
)
}
})}
</div>
</div>
</div>
)}
</div>
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
</>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
ViewMessagesDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func
}
export default ViewMessagesDialog

View File

@ -18,7 +18,7 @@ const StyledPopper = styled(Popper)({
}
})
export const MultiDropdown = ({ name, value, options, onSelect, disabled = false, disableClearable = false }) => {
export const MultiDropdown = ({ name, value, options, onSelect, formControlSx = {}, disabled = false, disableClearable = false }) => {
const customization = useSelector((state) => state.customization)
const findMatchingOptions = (options = [], internalValue) => {
let values = []
@ -30,7 +30,7 @@ export const MultiDropdown = ({ name, value, options, onSelect, disabled = false
let [internalValue, setInternalValue] = useState(value ?? [])
return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
<FormControl sx={{ mt: 1, width: '100%', ...formControlSx }} size='small'>
<Autocomplete
id={name}
disabled={disabled}
@ -75,5 +75,6 @@ MultiDropdown.propTypes = {
options: PropTypes.array,
onSelect: PropTypes.func,
disabled: PropTypes.bool,
formControlSx: PropTypes.object,
disableClearable: PropTypes.bool
}

View File

@ -415,6 +415,23 @@ export const getInputVariables = (paramValue) => {
return inputVariables
}
export const removeDuplicateURL = (message) => {
const visitedURLs = []
const newSourceDocuments = []
if (!message.sourceDocuments) return newSourceDocuments
message.sourceDocuments.forEach((source) => {
if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) {
visitedURLs.push(source.metadata.source)
newSourceDocuments.push(source)
} else if (!isValidURL(source.metadata.source)) {
newSourceDocuments.push(source)
}
})
return newSourceDocuments
}
export const isValidURL = (url) => {
try {
return new URL(url)

View File

@ -15,6 +15,7 @@ import Settings from 'views/settings'
import SaveChatflowDialog from 'ui-component/dialog/SaveChatflowDialog'
import APICodeDialog from 'views/chatflows/APICodeDialog'
import AnalyseFlowDialog from 'ui-component/dialog/AnalyseFlowDialog'
import ViewMessagesDialog from 'ui-component/dialog/ViewMessagesDialog'
// API
import chatflowsApi from 'api/chatflows'
@ -44,6 +45,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const [apiDialogProps, setAPIDialogProps] = useState({})
const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false)
const [analyseDialogProps, setAnalyseDialogProps] = useState({})
const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false)
const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({})
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
const canvas = useSelector((state) => state.canvas)
@ -59,6 +62,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
chatflow: chatflow
})
setAnalyseDialogOpen(true)
} else if (setting === 'viewMessages') {
setViewMessagesDialogProps({
title: 'View Messages',
chatflow: chatflow
})
setViewMessagesDialogOpen(true)
} else if (setting === 'duplicateChatflow') {
try {
localStorage.setItem('duplicatedFlowData', chatflow.flowData)
@ -69,7 +78,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
} else if (setting === 'exportChatflow') {
try {
const flowData = JSON.parse(chatflow.flowData)
let dataStr = JSON.stringify(generateExportFlowData(flowData))
let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2)
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
let exportFileDefaultName = `${chatflow.name} Chatflow.json`
@ -367,6 +376,11 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
/>
<APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} />
<AnalyseFlowDialog show={analyseDialogOpen} dialogProps={analyseDialogProps} onCancel={() => setAnalyseDialogOpen(false)} />
<ViewMessagesDialog
show={viewMessagesDialogOpen}
dialogProps={viewMessagesDialogProps}
onCancel={() => setViewMessagesDialogOpen(false)}
/>
</>
)
}

View File

@ -134,3 +134,13 @@
justify-content: center;
align-items: center;
}
.cloud-message {
width: 100%;
height: calc(100vh - 260px);
overflow-y: scroll;
border-radius: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -30,7 +30,7 @@ import { baseURL, maxScroll } from 'store/constant'
import robotPNG from 'assets/images/robot.png'
import userPNG from 'assets/images/account.png'
import { isValidURL } from 'utils/genericHelper'
import { isValidURL, removeDuplicateURL } from 'utils/genericHelper'
export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const theme = useTheme()
@ -53,7 +53,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const [chatId, setChatId] = useState(undefined)
const inputRef = useRef(null)
const getChatmessageApi = useApi(chatmessageApi.getChatmessageFromChatflow)
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
const onSourceDialogClick = (data) => {
@ -65,21 +65,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
window.open(data, '_blank')
}
const removeDuplicateURL = (message) => {
const visitedURLs = []
const newSourceDocuments = []
message.sourceDocuments.forEach((source) => {
if (isValidURL(source.metadata.source) && !visitedURLs.includes(source.metadata.source)) {
visitedURLs.push(source.metadata.source)
newSourceDocuments.push(source)
} else if (!isValidURL(source.metadata.source)) {
newSourceDocuments.push(source)
}
})
return newSourceDocuments
}
const scrollToBottom = () => {
if (ps.current) {
ps.current.scrollTo({ top: maxScroll })

View File

@ -86,7 +86,7 @@ export const ChatPopUp = ({ chatflowid }) => {
if (isConfirmed) {
try {
const chatId = localStorage.getItem(`${chatflowid}_INTERNAL`)
await chatmessageApi.deleteChatmessage(chatId)
await chatmessageApi.deleteChatmessage(chatflowid, { chatId, chatType: 'INTERNAL' })
localStorage.removeItem(`${chatflowid}_INTERNAL`)
resetChatDialog()
enqueueSnackbar({

View File

@ -227,7 +227,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
delete toolData.id
delete toolData.createdDate
delete toolData.updatedDate
let dataStr = JSON.stringify(toolData)
let dataStr = JSON.stringify(toolData, null, 2)
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
let exportFileDefaultName = `${toolName}-CustomTool.json`