{
+ await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`)
+ await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`)
+ }
+}
diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts
index 4a14fc407..fdd83064a 100644
--- a/packages/server/src/database/migrations/sqlite/index.ts
+++ b/packages/server/src/database/migrations/sqlite/index.ts
@@ -8,6 +8,8 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic'
import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory'
import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity'
import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage'
+import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow'
+import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage'
export const sqliteMigrations = [
Init1693835579790,
@@ -19,5 +21,7 @@ export const sqliteMigrations = [
AddAnalytic1694432361423,
AddChatHistory1694657778173,
AddAssistantEntity1699325775451,
- AddUsedToolsToChatMessage1699481607341
+ AddUsedToolsToChatMessage1699481607341,
+ AddCategoryToChatFlow1699900910291,
+ AddFileAnnotationsToChatMessage1700271021237
]
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index ba6c3ce0e..2f7d31e25 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -138,6 +138,7 @@ export class App {
'/api/v1/node-icon/',
'/api/v1/components-credentials-icon/',
'/api/v1/chatflows-streaming',
+ '/api/v1/openai-assistants-file',
'/api/v1/ip'
]
this.app.use((req, res, next) => {
@@ -355,8 +356,12 @@ export class App {
this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
- // Update chatflowpool inSync to false, to build Langchain again because data has been changed
- this.chatflowPool.updateInSync(chatflow.id, false)
+ // chatFlowPool is initialized only when a flow is opened
+ // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined
+ if (this.chatflowPool) {
+ // Update chatflowpool inSync to false, to build Langchain again because data has been changed
+ this.chatflowPool.updateInSync(chatflow.id, false)
+ }
return res.json(result)
})
@@ -782,8 +787,8 @@ export class App {
await openai.beta.assistants.update(assistantDetails.id, {
name: assistantDetails.name,
- description: assistantDetails.description,
- instructions: assistantDetails.instructions,
+ description: assistantDetails.description ?? '',
+ instructions: assistantDetails.instructions ?? '',
model: assistantDetails.model,
tools: filteredTools,
file_ids: uniqWith(
@@ -952,7 +957,7 @@ export class App {
const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id })
- await openai.beta.assistants.del(assistantDetails.id)
+ if (req.query.isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id)
return res.json(results)
} catch (error: any) {
@@ -961,6 +966,14 @@ export class App {
}
})
+ // Download file from assistant
+ this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => {
+ const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName)
+ res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath))
+ const fileStream = fs.createReadStream(filePath)
+ fileStream.pipe(res)
+ })
+
// ----------------------------------------
// Configuration
// ----------------------------------------
@@ -1135,28 +1148,52 @@ export class App {
// API Keys
// ----------------------------------------
+ const addChatflowsCount = async (keys: any, res: Response) => {
+ if (keys) {
+ const updatedKeys: any[] = []
+ //iterate through keys and get chatflows
+ for (const key of keys) {
+ const chatflows = await this.AppDataSource.getRepository(ChatFlow)
+ .createQueryBuilder('cf')
+ .where('cf.apikeyid = :apikeyid', { apikeyid: key.id })
+ .getMany()
+ const linkedChatFlows: any[] = []
+ chatflows.map((cf) => {
+ linkedChatFlows.push({
+ flowName: cf.name,
+ category: cf.category,
+ updatedDate: cf.updatedDate
+ })
+ })
+ key.chatFlows = linkedChatFlows
+ updatedKeys.push(key)
+ }
+ return res.json(updatedKeys)
+ }
+ return res.json(keys)
+ }
// Get api keys
this.app.get('/api/v1/apikey', async (req: Request, res: Response) => {
const keys = await getAPIKeys()
- return res.json(keys)
+ return addChatflowsCount(keys, res)
})
// Add new api key
this.app.post('/api/v1/apikey', async (req: Request, res: Response) => {
const keys = await addAPIKey(req.body.keyName)
- return res.json(keys)
+ return addChatflowsCount(keys, res)
})
// Update api key
this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => {
const keys = await updateAPIKey(req.params.id, req.body.keyName)
- return res.json(keys)
+ return addChatflowsCount(keys, res)
})
// Delete new api key
this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => {
const keys = await deleteAPIKey(req.params.id)
- return res.json(keys)
+ return addChatflowsCount(keys, res)
})
// Verify api key
@@ -1499,6 +1536,7 @@ export class App {
}
if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments)
if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools)
+ if (result?.fileAnnotations) apiMessage.fileAnnotations = JSON.stringify(result.fileAnnotations)
await this.addChatMessage(apiMessage)
logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts
index 239773a9a..0c6f23624 100644
--- a/packages/server/src/utils/index.ts
+++ b/packages/server/src/utils/index.ts
@@ -842,7 +842,7 @@ export const isFlowValidForStream = (reactFlowNodes: IReactFlowNode[], endingNod
let isValidChainOrAgent = false
if (endingNodeData.category === 'Chains') {
// Chains that are not available to stream
- const blacklistChains = ['openApiChain']
+ const blacklistChains = ['openApiChain', 'vectaraQAChain']
isValidChainOrAgent = !blacklistChains.includes(endingNodeData.name)
} else if (endingNodeData.category === 'Agents') {
// Agent that are available to stream
@@ -985,10 +985,14 @@ export const redactCredentialWithPasswordType = (
* @param {any} instance
* @param {string} chatId
*/
-export const checkMemorySessionId = (instance: any, chatId: string): string => {
+export const checkMemorySessionId = (instance: any, chatId: string): string | undefined => {
if (instance.memory && instance.memory.isSessionIdUsingChatMessageId && chatId) {
instance.memory.sessionId = chatId
instance.memory.chatHistory.sessionId = chatId
}
- return instance.memory ? instance.memory.sessionId ?? instance.memory.chatHistory.sessionId : undefined
+
+ if (instance.memory && instance.memory.sessionId) return instance.memory.sessionId
+ else if (instance.memory && instance.memory.chatHistory && instance.memory.chatHistory.sessionId)
+ return instance.memory.chatHistory.sessionId
+ return undefined
}
diff --git a/packages/ui/src/api/assistants.js b/packages/ui/src/api/assistants.js
index 63dd5e18a..ac941126d 100644
--- a/packages/ui/src/api/assistants.js
+++ b/packages/ui/src/api/assistants.js
@@ -12,7 +12,8 @@ const createNewAssistant = (body) => client.post(`/assistants`, body)
const updateAssistant = (id, body) => client.put(`/assistants/${id}`, body)
-const deleteAssistant = (id) => client.delete(`/assistants/${id}`)
+const deleteAssistant = (id, isDeleteBoth) =>
+ isDeleteBoth ? client.delete(`/assistants/${id}?isDeleteBoth=true`) : client.delete(`/assistants/${id}`)
export default {
getAllAssistants,
diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js
new file mode 100644
index 000000000..b242d2cb2
--- /dev/null
+++ b/packages/ui/src/ui-component/button/FlowListMenu.js
@@ -0,0 +1,291 @@
+import { useState } from 'react'
+import { useDispatch } from 'react-redux'
+import PropTypes from 'prop-types'
+
+import { styled, alpha } from '@mui/material/styles'
+import Menu from '@mui/material/Menu'
+import MenuItem from '@mui/material/MenuItem'
+import EditIcon from '@mui/icons-material/Edit'
+import Divider from '@mui/material/Divider'
+import FileCopyIcon from '@mui/icons-material/FileCopy'
+import FileDownloadIcon from '@mui/icons-material/Downloading'
+import FileDeleteIcon from '@mui/icons-material/Delete'
+import FileCategoryIcon from '@mui/icons-material/Category'
+import Button from '@mui/material/Button'
+import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
+import { IconX } from '@tabler/icons'
+
+import chatflowsApi from 'api/chatflows'
+
+import useApi from '../../hooks/useApi'
+import useConfirm from 'hooks/useConfirm'
+import { uiBaseURL } from '../../store/constant'
+import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions'
+
+import ConfirmDialog from '../dialog/ConfirmDialog'
+import SaveChatflowDialog from '../dialog/SaveChatflowDialog'
+import TagDialog from '../dialog/TagDialog'
+
+import { generateExportFlowData } from '../../utils/genericHelper'
+import useNotifier from '../../utils/useNotifier'
+
+const StyledMenu = styled((props) => (
+
+))(({ theme }) => ({
+ '& .MuiPaper-root': {
+ borderRadius: 6,
+ marginTop: theme.spacing(1),
+ minWidth: 180,
+ boxShadow:
+ 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
+ '& .MuiMenu-list': {
+ padding: '4px 0'
+ },
+ '& .MuiMenuItem-root': {
+ '& .MuiSvgIcon-root': {
+ fontSize: 18,
+ color: theme.palette.text.secondary,
+ marginRight: theme.spacing(1.5)
+ },
+ '&:active': {
+ backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
+ }
+ }
+ }
+}))
+
+export default function FlowListMenu({ chatflow, updateFlowsApi }) {
+ const { confirm } = useConfirm()
+ const dispatch = useDispatch()
+ const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
+
+ useNotifier()
+ const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
+ const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
+
+ const [flowDialogOpen, setFlowDialogOpen] = useState(false)
+ const [categoryDialogOpen, setCategoryDialogOpen] = useState(false)
+ const [categoryDialogProps, setCategoryDialogProps] = useState({})
+ const [anchorEl, setAnchorEl] = useState(null)
+ const open = Boolean(anchorEl)
+
+ const handleClick = (event) => {
+ setAnchorEl(event.currentTarget)
+ }
+
+ const handleClose = () => {
+ setAnchorEl(null)
+ }
+
+ const handleFlowRename = () => {
+ setAnchorEl(null)
+ setFlowDialogOpen(true)
+ }
+
+ const saveFlowRename = async (chatflowName) => {
+ const updateBody = {
+ name: chatflowName,
+ chatflow
+ }
+ try {
+ await updateChatflowApi.request(chatflow.id, updateBody)
+ await updateFlowsApi.request()
+ } 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) => (
+
+ )
+ }
+ })
+ }
+ }
+
+ const handleFlowCategory = () => {
+ setAnchorEl(null)
+ if (chatflow.category) {
+ setCategoryDialogProps({
+ category: chatflow.category.split(';')
+ })
+ }
+ setCategoryDialogOpen(true)
+ }
+
+ const saveFlowCategory = async (categories) => {
+ setCategoryDialogOpen(false)
+ // save categories as string
+ const categoryTags = categories.join(';')
+ const updateBody = {
+ category: categoryTags,
+ chatflow
+ }
+ try {
+ await updateChatflowApi.request(chatflow.id, updateBody)
+ await updateFlowsApi.request()
+ } 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) => (
+
+ )
+ }
+ })
+ }
+ }
+
+ const handleDelete = async () => {
+ setAnchorEl(null)
+ const confirmPayload = {
+ title: `Delete`,
+ description: `Delete chatflow ${chatflow.name}?`,
+ confirmButtonName: 'Delete',
+ cancelButtonName: 'Cancel'
+ }
+ const isConfirmed = await confirm(confirmPayload)
+
+ if (isConfirmed) {
+ try {
+ await chatflowsApi.deleteChatflow(chatflow.id)
+ await updateFlowsApi.request()
+ } 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) => (
+
+ )
+ }
+ })
+ }
+ }
+ }
+
+ const handleDuplicate = () => {
+ setAnchorEl(null)
+ try {
+ localStorage.setItem('duplicatedFlowData', chatflow.flowData)
+ window.open(`${uiBaseURL}/canvas`, '_blank')
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ const handleExport = () => {
+ setAnchorEl(null)
+ try {
+ const flowData = JSON.parse(chatflow.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`
+
+ let linkElement = document.createElement('a')
+ linkElement.setAttribute('href', dataUri)
+ linkElement.setAttribute('download', exportFileDefaultName)
+ linkElement.click()
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ return (
+
+ }
+ >
+ Options
+
+
+
+ setFlowDialogOpen(false)}
+ onConfirm={saveFlowRename}
+ />
+ setCategoryDialogOpen(false)}
+ onSubmit={saveFlowCategory}
+ />
+
+ )
+}
+
+FlowListMenu.propTypes = {
+ chatflow: PropTypes.object,
+ updateFlowsApi: PropTypes.object
+}
diff --git a/packages/ui/src/ui-component/button/StyledButton.jsx b/packages/ui/src/ui-component/button/StyledButton.jsx
index 6e0c70786..29e17f804 100644
--- a/packages/ui/src/ui-component/button/StyledButton.jsx
+++ b/packages/ui/src/ui-component/button/StyledButton.jsx
@@ -1,5 +1,6 @@
import { styled } from '@mui/material/styles'
import { Button } from '@mui/material'
+import MuiToggleButton from '@mui/material/ToggleButton'
export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({
color: 'white',
@@ -9,3 +10,10 @@ export const StyledButton = styled(Button)(({ theme, color = 'primary' }) => ({
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
}
}))
+
+export const StyledToggleButton = styled(MuiToggleButton)(({ theme, color = 'primary' }) => ({
+ '&.Mui-selected, &.Mui-selected:hover': {
+ color: 'white',
+ backgroundColor: theme.palette[color].main
+ }
+}))
diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js
new file mode 100644
index 000000000..82c35dde6
--- /dev/null
+++ b/packages/ui/src/ui-component/dialog/TagDialog.js
@@ -0,0 +1,110 @@
+import { useState, useEffect } from 'react'
+import Dialog from '@mui/material/Dialog'
+import Box from '@mui/material/Box'
+import Button from '@mui/material/Button'
+import TextField from '@mui/material/TextField'
+import Chip from '@mui/material/Chip'
+import PropTypes from 'prop-types'
+import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material'
+
+const TagDialog = ({ isOpen, dialogProps, onClose, onSubmit }) => {
+ const [inputValue, setInputValue] = useState('')
+ const [categoryValues, setCategoryValues] = useState([])
+
+ const handleInputChange = (event) => {
+ setInputValue(event.target.value)
+ }
+
+ const handleInputKeyDown = (event) => {
+ if (event.key === 'Enter' && inputValue.trim()) {
+ event.preventDefault()
+ if (!categoryValues.includes(inputValue)) {
+ setCategoryValues([...categoryValues, inputValue])
+ setInputValue('')
+ }
+ }
+ }
+
+ const handleDeleteTag = (categoryToDelete) => {
+ setCategoryValues(categoryValues.filter((category) => category !== categoryToDelete))
+ }
+
+ const handleSubmit = (event) => {
+ event.preventDefault()
+ let newCategories = [...categoryValues]
+ if (inputValue.trim() && !categoryValues.includes(inputValue)) {
+ newCategories = [...newCategories, inputValue]
+ setCategoryValues(newCategories)
+ }
+ onSubmit(newCategories)
+ }
+
+ useEffect(() => {
+ if (dialogProps.category) setCategoryValues(dialogProps.category)
+
+ return () => {
+ setInputValue('')
+ setCategoryValues([])
+ }
+ }, [dialogProps])
+
+ return (
+
+ )
+}
+
+TagDialog.propTypes = {
+ isOpen: PropTypes.bool,
+ dialogProps: PropTypes.object,
+ onClose: PropTypes.func,
+ onSubmit: PropTypes.func
+}
+
+export default TagDialog
diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
index c5374bdf6..cadd4abd9 100644
--- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
+++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
@@ -7,6 +7,7 @@ import rehypeMathjax from 'rehype-mathjax'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
+import axios from 'axios'
// material-ui
import {
@@ -25,33 +26,34 @@ import {
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'
+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, IconDownload } 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'
+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'
+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'
+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 { isValidURL, removeDuplicateURL } from 'utils/genericHelper'
+import useNotifier from 'utils/useNotifier'
+import { baseURL } from 'store/constant'
-import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
+import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
-import '@/views/chatmessage/ChatMessage.css'
+import 'views/chatmessage/ChatMessage.css'
import 'react-datepicker/dist/react-datepicker.css'
const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) {
@@ -130,6 +132,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
}
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools)
+ if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
if (!Object.prototype.hasOwnProperty.call(obj, chatPK)) {
obj[chatPK] = {
@@ -253,6 +256,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
}
if (chatmsg.sourceDocuments) obj.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
if (chatmsg.usedTools) obj.usedTools = JSON.parse(chatmsg.usedTools)
+ if (chatmsg.fileAnnotations) obj.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
loadedMessages.push(obj)
}
@@ -318,6 +322,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
window.open(data, '_blank')
}
+ const downloadFile = async (fileAnnotation) => {
+ try {
+ const response = await axios.post(
+ `${baseURL}/api/v1/openai-assistants-file`,
+ { fileName: fileAnnotation.fileName },
+ { responseType: 'blob' }
+ )
+ const blob = new Blob([response.data], { type: response.headers['content-type'] })
+ const downloadUrl = window.URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = downloadUrl
+ link.download = fileAnnotation.fileName
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ } catch (error) {
+ console.error('Download failed:', error)
+ }
+ }
+
const onSourceDialogClick = (data, title) => {
setSourceDialogProps({ data, title })
setSourceDialogOpen(true)
@@ -648,10 +672,37 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
{message.message}
+ {message.fileAnnotations && (
+
+ {message.fileAnnotations.map((fileAnnotation, index) => {
+ return (
+
+ )
+ })}
+
+ )}
{message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => {
- const URL = isValidURL(source.metadata.source)
+ const URL =
+ source.metadata && source.metadata.source
+ ? isValidURL(source.metadata.source)
+ : undefined
return (
({
+ [`&.${tableCellClasses.head}`]: {
+ backgroundColor: theme.palette.common.black,
+ color: theme.palette.common.white
+ },
+ [`&.${tableCellClasses.body}`]: {
+ fontSize: 14
+ }
+}))
+
+const StyledTableRow = styled(TableRow)(({ theme }) => ({
+ '&:nth-of-type(odd)': {
+ backgroundColor: theme.palette.action.hover
+ },
+ // hide last border
+ '&:last-child td, &:last-child th': {
+ border: 0
+ }
+}))
+
+export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) => {
+ const navigate = useNavigate()
+ const goToCanvas = (selectedChatflow) => {
+ navigate(`/canvas/${selectedChatflow.id}`)
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Name
+
+
+ Category
+
+
+ Nodes
+
+
+ Last Modified Date
+
+
+ Actions
+
+
+
+
+ {data.filter(filterFunction).map((row, index) => (
+
+
+
+
+
+
+
+
+
+ {row.category &&
+ row.category
+ .split(';')
+ .map((tag, index) => (
+
+ ))}
+
+
+
+ {images[row.id] && (
+
+ {images[row.id].slice(0, images[row.id].length > 5 ? 5 : images[row.id].length).map((img) => (
+
+

+
+ ))}
+ {images[row.id].length > 5 && (
+
+ + {images[row.id].length - 5} More
+
+ )}
+
+ )}
+
+ {moment(row.updatedDate).format('MMMM Do, YYYY')}
+
+
+
+
+
+
+ ))}
+
+
+
+ >
+ )
+}
+
+FlowListTable.propTypes = {
+ data: PropTypes.object,
+ images: PropTypes.array,
+ filterFunction: PropTypes.func,
+ updateFlowsApi: PropTypes.object
+}
diff --git a/packages/ui/src/ui-component/toolbar/Toolbar.js b/packages/ui/src/ui-component/toolbar/Toolbar.js
new file mode 100644
index 000000000..f72ba339f
--- /dev/null
+++ b/packages/ui/src/ui-component/toolbar/Toolbar.js
@@ -0,0 +1,24 @@
+import * as React from 'react'
+import ViewListIcon from '@mui/icons-material/ViewList'
+import ViewModuleIcon from '@mui/icons-material/ViewModule'
+import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
+import { StyledToggleButton } from '../button/StyledButton'
+
+export default function Toolbar() {
+ const [view, setView] = React.useState('list')
+
+ const handleChange = (event, nextView) => {
+ setView(nextView)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js
index 32331b142..da1e5ad83 100644
--- a/packages/ui/src/utils/genericHelper.js
+++ b/packages/ui/src/utils/genericHelper.js
@@ -423,10 +423,14 @@ export const removeDuplicateURL = (message) => {
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)) {
+ if (source.metadata && source.metadata.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)
+ }
+ } else {
newSourceDocuments.push(source)
}
})
diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx
index d2febacd2..68113af5b 100644
--- a/packages/ui/src/views/apikey/index.jsx
+++ b/packages/ui/src/views/apikey/index.jsx
@@ -1,47 +1,188 @@
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
-import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
+import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
// material-ui
import {
Button,
Box,
+ Chip,
Stack,
Table,
TableBody,
- TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Popover,
- Typography
+ Collapse,
+ Typography,
+ Toolbar,
+ TextField,
+ InputAdornment,
+ ButtonGroup
} from '@mui/material'
-import { useTheme } from '@mui/material/styles'
+import TableCell, { tableCellClasses } from '@mui/material/TableCell'
+import { useTheme, styled } from '@mui/material/styles'
// project imports
-import MainCard from '@/ui-component/cards/MainCard'
-import { StyledButton } from '@/ui-component/button/StyledButton'
+import MainCard from 'ui-component/cards/MainCard'
+import { StyledButton } from 'ui-component/button/StyledButton'
import APIKeyDialog from './APIKeyDialog'
-import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
+import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
// API
-import apiKeyApi from '@/api/apikey'
+import apiKeyApi from 'api/apikey'
// Hooks
-import useApi from '@/hooks/useApi'
-import useConfirm from '@/hooks/useConfirm'
+import useApi from 'hooks/useApi'
+import useConfirm from 'hooks/useConfirm'
// utils
-import useNotifier from '@/utils/useNotifier'
+import useNotifier from 'utils/useNotifier'
// Icons
-import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons'
-import APIEmptySVG from '@/assets/images/api_empty.svg'
+import {
+ IconTrash,
+ IconEdit,
+ IconCopy,
+ IconChevronsUp,
+ IconChevronsDown,
+ IconX,
+ IconSearch,
+ IconPlus,
+ IconEye,
+ IconEyeOff
+} from '@tabler/icons'
+import APIEmptySVG from 'assets/images/api_empty.svg'
+import * as PropTypes from 'prop-types'
+import moment from 'moment/moment'
// ==============================|| APIKey ||============================== //
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+ [`&.${tableCellClasses.head}`]: {
+ backgroundColor: theme.palette.action.hover
+ }
+}))
+const StyledTableRow = styled(TableRow)(() => ({
+ // hide last border
+ '&:last-child td, &:last-child th': {
+ border: 0
+ }
+}))
+
+function APIKeyRow(props) {
+ const [open, setOpen] = useState(false)
+ return (
+ <>
+
+ {props.apiKey.keyName}
+
+ {props.showApiKeys.includes(props.apiKey.apiKey)
+ ? props.apiKey.apiKey
+ : `${props.apiKey.apiKey.substring(0, 2)}${'•'.repeat(18)}${props.apiKey.apiKey.substring(
+ props.apiKey.apiKey.length - 5
+ )}`}
+
+
+
+
+ {props.showApiKeys.includes(props.apiKey.apiKey) ? : }
+
+
+
+ Copied!
+
+
+
+
+ {props.apiKey.chatFlows.length}{' '}
+ {props.apiKey.chatFlows.length > 0 && (
+ setOpen(!open)}>
+ {props.apiKey.chatFlows.length > 0 && open ? : }
+
+ )}
+
+ {props.apiKey.createdAt}
+
+
+
+
+
+
+
+
+
+
+
+ {open && (
+
+
+
+
+
+
+
+
+ Chatflow Name
+
+ Modified On
+ Category
+
+
+
+ {props.apiKey.chatFlows.map((flow, index) => (
+
+ {flow.flowName}
+ {moment(flow.updatedDate).format('DD-MMM-YY')}
+
+
+ {flow.category &&
+ flow.category
+ .split(';')
+ .map((tag, index) => (
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+
+ )}
+ >
+ )
+}
+
+APIKeyRow.propTypes = {
+ apiKey: PropTypes.any,
+ showApiKeys: PropTypes.arrayOf(PropTypes.any),
+ onCopyClick: PropTypes.func,
+ onShowAPIClick: PropTypes.func,
+ open: PropTypes.bool,
+ anchorEl: PropTypes.any,
+ onClose: PropTypes.func,
+ theme: PropTypes.any,
+ onEditClick: PropTypes.func,
+ onDeleteClick: PropTypes.func
+}
const APIKey = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
@@ -59,6 +200,14 @@ const APIKey = () => {
const [showApiKeys, setShowApiKeys] = useState([])
const openPopOver = Boolean(anchorEl)
+ const [search, setSearch] = useState('')
+ const onSearchChange = (event) => {
+ setSearch(event.target.value)
+ }
+ function filterKeys(data) {
+ return data.keyName.toLowerCase().indexOf(search.toLowerCase()) > -1
+ }
+
const { confirm } = useConfirm()
const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys)
@@ -106,7 +255,10 @@ const APIKey = () => {
const deleteKey = async (key) => {
const confirmPayload = {
title: `Delete`,
- description: `Delete key ${key.keyName}?`,
+ description:
+ key.chatFlows.length === 0
+ ? `Delete key [${key.keyName}] ? `
+ : `Delete key [${key.keyName}] ?\n There are ${key.chatFlows.length} chatflows using this key.`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
@@ -171,12 +323,53 @@ const APIKey = () => {
<>
- API Keys
-
-
- }>
- Create Key
-
+
+
+ API Keys
+
+
+
+ )
+ }}
+ />
+
+
+
+ }
+ >
+ Create Key
+
+
+
+
+
{apiKeys.length <= 0 && (
@@ -193,72 +386,33 @@ const APIKey = () => {
Key Name
API Key
+ Usage
Created
- {apiKeys.map((key, index) => (
-
-
- {key.keyName}
-
-
- {showApiKeys.includes(key.apiKey)
- ? key.apiKey
- : `${key.apiKey.substring(0, 2)}${'•'.repeat(18)}${key.apiKey.substring(
- key.apiKey.length - 5
- )}`}
- {
- navigator.clipboard.writeText(key.apiKey)
- setAnchorEl(event.currentTarget)
- setTimeout(() => {
- handleClosePopOver()
- }, 1500)
- }}
- >
-
-
- onShowApiKeyClick(key.apiKey)}>
- {showApiKeys.includes(key.apiKey) ? : }
-
-
-
- Copied!
-
-
-
- {key.createdAt}
-
- edit(key)}>
-
-
-
-
- deleteKey(key)}>
-
-
-
-
+ {apiKeys.filter(filterKeys).map((key, index) => (
+ {
+ navigator.clipboard.writeText(key.apiKey)
+ setAnchorEl(event.currentTarget)
+ setTimeout(() => {
+ handleClosePopOver()
+ }, 1500)
+ }}
+ onShowAPIClick={() => onShowApiKeyClick(key.apiKey)}
+ open={openPopOver}
+ anchorEl={anchorEl}
+ onClose={handleClosePopOver}
+ theme={theme}
+ onEditClick={() => edit(key)}
+ onDeleteClick={() => deleteKey(key)}
+ />
))}
diff --git a/packages/ui/src/views/assistants/AssistantDialog.jsx b/packages/ui/src/views/assistants/AssistantDialog.jsx
index f741f70cb..30087baed 100644
--- a/packages/ui/src/views/assistants/AssistantDialog.jsx
+++ b/packages/ui/src/views/assistants/AssistantDialog.jsx
@@ -2,33 +2,32 @@ import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
-import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
+import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import { v4 as uuidv4 } from 'uuid'
import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material'
-import { StyledButton } from '@/ui-component/button/StyledButton'
-import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
-import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
-import { Dropdown } from '@/ui-component/dropdown/Dropdown'
-import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
-import CredentialInputHandler from '@/views/canvas/CredentialInputHandler'
-import { File } from '@/ui-component/file/File'
-import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
+import { StyledButton } from 'ui-component/button/StyledButton'
+import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
+import { Dropdown } from 'ui-component/dropdown/Dropdown'
+import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown'
+import CredentialInputHandler from 'views/canvas/CredentialInputHandler'
+import { File } from 'ui-component/file/File'
+import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
+import DeleteConfirmDialog from './DeleteConfirmDialog'
// Icons
import { IconX } from '@tabler/icons'
// API
-import assistantsApi from '@/api/assistants'
+import assistantsApi from 'api/assistants'
// Hooks
-import useConfirm from '@/hooks/useConfirm'
-import useApi from '@/hooks/useApi'
+import useApi from 'hooks/useApi'
// utils
-import useNotifier from '@/utils/useNotifier'
-import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
+import useNotifier from 'utils/useNotifier'
+import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
const assistantAvailableModels = [
{
@@ -71,14 +70,8 @@ const assistantAvailableModels = [
const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
-
- const dispatch = useDispatch()
-
- // ==============================|| Snackbar ||============================== //
-
useNotifier()
- const { confirm } = useConfirm()
-
+ const dispatch = useDispatch()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
@@ -97,6 +90,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const [assistantFiles, setAssistantFiles] = useState([])
const [uploadAssistantFiles, setUploadAssistantFiles] = useState('')
const [loading, setLoading] = useState(false)
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+ const [deleteDialogProps, setDeleteDialogProps] = useState({})
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
@@ -123,20 +118,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
useEffect(() => {
if (getAssistantObjApi.data) {
- setOpenAIAssistantId(getAssistantObjApi.data.id)
- setAssistantName(getAssistantObjApi.data.name)
- setAssistantDesc(getAssistantObjApi.data.description)
- setAssistantModel(getAssistantObjApi.data.model)
- setAssistantInstructions(getAssistantObjApi.data.instructions)
- setAssistantFiles(getAssistantObjApi.data.files ?? [])
-
- let tools = []
- if (getAssistantObjApi.data.tools && getAssistantObjApi.data.tools.length) {
- for (const tool of getAssistantObjApi.data.tools) {
- tools.push(tool.type)
- }
- }
- setAssistantTools(tools)
+ syncData(getAssistantObjApi.data)
}
}, [getAssistantObjApi.data])
@@ -199,6 +181,23 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogProps])
+ const syncData = (data) => {
+ setOpenAIAssistantId(data.id)
+ setAssistantName(data.name)
+ setAssistantDesc(data.description)
+ setAssistantModel(data.model)
+ setAssistantInstructions(data.instructions)
+ setAssistantFiles(data.files ?? [])
+
+ let tools = []
+ if (data.tools && data.tools.length) {
+ for (const tool of data.tools) {
+ tools.push(tool.type)
+ }
+ }
+ setAssistantTools(tools)
+ }
+
const addNewAssistant = async () => {
setLoading(true)
try {
@@ -309,41 +308,17 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
}
}
- const deleteAssistant = async () => {
- const confirmPayload = {
- title: `Delete Assistant`,
- description: `Delete Assistant ${assistantName}?`,
- confirmButtonName: 'Delete',
- cancelButtonName: 'Cancel'
- }
- const isConfirmed = await confirm(confirmPayload)
-
- if (isConfirmed) {
- try {
- const delResp = await assistantsApi.deleteAssistant(assistantId)
- if (delResp.data) {
- enqueueSnackbar({
- message: 'Assistant deleted',
- options: {
- key: new Date().getTime() + Math.random(),
- variant: 'success',
- action: (key) => (
-
- )
- }
- })
- onConfirm()
- }
- } catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
+ const onSyncClick = async () => {
+ setLoading(true)
+ try {
+ const getResp = await assistantsApi.getAssistantObj(openAIAssistantId, assistantCredential)
+ if (getResp.data) {
+ syncData(getResp.data)
enqueueSnackbar({
- message: `Failed to delete Assistant: ${errorData}`,
+ message: 'Assistant successfully synced!',
options: {
key: new Date().getTime() + Math.random(),
- variant: 'error',
- persist: true,
+ variant: 'success',
action: (key) => (
+ {message.fileAnnotations && (
+
+ {message.fileAnnotations.map((fileAnnotation, index) => {
+ return (
+ downloadFile(fileAnnotation)}
+ endIcon={}
+ >
+ {fileAnnotation.fileName}
+
+ )
+ })}
+
+ )}
{message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => {
- const URL = isValidURL(source.metadata.source)
+ const URL =
+ source.metadata && source.metadata.source
+ ? isValidURL(source.metadata.source)
+ : undefined
return (
{
const getAllCredentialsApi = useApi(credentialsApi.getAllCredentials)
const getAllComponentsCredentialsApi = useApi(credentialsApi.getAllComponentsCredentials)
+ const [search, setSearch] = useState('')
+ const onSearchChange = (event) => {
+ setSearch(event.target.value)
+ }
+ function filterCredentials(data) {
+ return data.credentialName.toLowerCase().indexOf(search.toLowerCase()) > -1
+ }
+
const listCredential = () => {
const dialogProp = {
title: 'Add New Credential',
@@ -168,17 +192,53 @@ const Credentials = () => {
<>
- Credentials
-
-
- }
- >
- Add Credential
-
+
+
+ Credentials
+
+
+
+ )
+ }}
+ />
+
+
+
+ }
+ >
+ Add Credential
+
+
+
+
+
{credentials.length <= 0 && (
@@ -205,7 +265,7 @@ const Credentials = () => {
- {credentials.map((credential, index) => (
+ {credentials.filter(filterCredentials).map((credential, index) => (