From f21f5257cac9a50661e2408017729641f00a69f5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 14:35:47 +0530 Subject: [PATCH 01/15] UX Changes: Ability to view as table and search on the dashboard --- .../src/ui-component/button/StyledButton.js | 8 ++ .../src/ui-component/table/FlowListTable.js | 133 ++++++++++++++++++ .../ui/src/ui-component/toolbar/Toolbar.js | 24 ++++ packages/ui/src/views/chatflows/index.js | 96 ++++++++++--- 4 files changed, 240 insertions(+), 21 deletions(-) create mode 100644 packages/ui/src/ui-component/table/FlowListTable.js create mode 100644 packages/ui/src/ui-component/toolbar/Toolbar.js diff --git a/packages/ui/src/ui-component/button/StyledButton.js b/packages/ui/src/ui-component/button/StyledButton.js index 6e0c70786..29e17f804 100644 --- a/packages/ui/src/ui-component/button/StyledButton.js +++ b/packages/ui/src/ui-component/button/StyledButton.js @@ -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/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js new file mode 100644 index 000000000..819a49cb6 --- /dev/null +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -0,0 +1,133 @@ +import PropTypes from 'prop-types' +import { useNavigate } from 'react-router-dom' +import { IconEdit } from '@tabler/icons' +import moment from 'moment' +import { styled } from '@mui/material/styles' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import Paper from '@mui/material/Paper' +import { Button, Typography } from '@mui/material' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${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 }) => { + const navigate = useNavigate() + const goToCanvas = (selectedChatflow) => { + navigate(`/canvas/${selectedChatflow.id}`) + } + return ( + <> + + + + + + Name + + + Nodes + + + Last Modified Date + + + Actions + + + + + {data.filter(filterFunction).map((row, index) => ( + + + + {row.templateName || row.name} + + + + + {images[row.id] && ( +
+ {images[row.id].map((img) => ( +
+ +
+ ))} +
+ )} +
+ {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + + +
+ ))} +
+
+
+ + ) +} + +FlowListTable.propTypes = { + data: PropTypes.object, + images: PropTypes.array, + filterFunction: PropTypes.func +} 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/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 6712623e6..e01a93734 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' // material-ui -import { Grid, Box, Stack } from '@mui/material' +import { Grid, Box, Stack, Toolbar, ToggleButton, ButtonGroup, Typography, InputAdornment, TextField } from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -11,7 +11,6 @@ import MainCard from 'ui-component/cards/MainCard' import ItemCard from 'ui-component/cards/ItemCard' import { gridSpacing } from 'store/constant' import WorkflowEmptySVG from 'assets/images/workflow_empty.svg' -import { StyledButton } from 'ui-component/button/StyledButton' import LoginDialog from 'ui-component/dialog/LoginDialog' // API @@ -24,7 +23,13 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus } from '@tabler/icons' +import { IconPlus, IconSearch } from '@tabler/icons' +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 { FlowListTable } from '../../ui-component/table/FlowListTable' +import { StyledButton } from '../../ui-component/button/StyledButton' // ==============================|| CHATFLOWS ||============================== // @@ -35,10 +40,24 @@ const Chatflows = () => { const [isLoading, setLoading] = useState(true) const [images, setImages] = useState({}) + const [search, setSearch] = useState('') const [loginDialogOpen, setLoginDialogOpen] = useState(false) const [loginDialogProps, setLoginDialogProps] = useState({}) const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) + const [view, setView] = React.useState('card') + + const handleChange = (event, nextView) => { + setView(nextView) + } + + const onSearchChange = (event) => { + setSearch(event.target.value) + } + + function filterFlows(data) { + return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 + } const onLoginClick = (username, password) => { localStorage.setItem('username', username) @@ -102,26 +121,61 @@ const Chatflows = () => { return ( - -

Chatflows

- - - - }> - Add New - + + + + + Chatflows + + + + + + ) + }} + /> + + + + + + + + + + + + + + + }> + Add New + + + + + + {!isLoading && (!view || view === 'card') && getAllChatflowsApi.data && ( + + {getAllChatflowsApi.data.filter(filterFlows).map((data, index) => ( + + goToCanvas(data)} data={data} images={images[data.id]} /> + + ))} - + )} + {!isLoading && view === 'list' && getAllChatflowsApi.data && ( + + )}
- - {!isLoading && - getAllChatflowsApi.data && - getAllChatflowsApi.data.map((data, index) => ( - - goToCanvas(data)} data={data} images={images[data.id]} /> - - ))} - + {!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && ( From 77994ce2178e2c7ba456a1a950bf6c89cde83040 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 15:15:34 +0530 Subject: [PATCH 02/15] UX Changes: adding a placeholder for chatflow search. --- packages/ui/src/views/chatflows/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index e01a93734..f7d1497db 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -132,6 +132,7 @@ const Chatflows = () => { size='small' sx={{ width: 400 }} variant='outlined' + placeholder='Search Chatflows' onChange={onSearchChange} InputProps={{ startAdornment: ( From 7ef817bc996200d89c4d5f11d4a556f2ac5a16df Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 15:48:14 +0530 Subject: [PATCH 03/15] UX Changes: limiting display of node icons to 5 and with label to indicate additional. --- packages/ui/src/ui-component/table/FlowListTable.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 819a49cb6..896ce3ea6 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -37,6 +37,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) } + let nodeCount = 0 return ( <> @@ -53,7 +54,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { Name - Nodes + Nodes (Showing first 5) Last Modified Date @@ -84,7 +85,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { marginTop: 5 }} > - {images[row.id].map((img) => ( + {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 + + )} )} From 57b31130397ca0097ef04db78137cccc79d7b63e Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Tue, 14 Nov 2023 20:13:24 +0530 Subject: [PATCH 04/15] UX Changes: minor UI tweaks/adjustments and fixes for small screens --- .../src/ui-component/table/FlowListTable.js | 20 ++++++------- packages/ui/src/views/chatflows/index.js | 28 +++++++++++++------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 896ce3ea6..9dfc95221 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -37,26 +37,20 @@ export const FlowListTable = ({ data, images, filterFunction }) => { const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) } - let nodeCount = 0 + return ( <> - + Name - + Nodes (Showing first 5) - + Last Modified Date @@ -75,7 +69,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { - + {images[row.id] && (
{
)}
- {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} + + ) + } + }) + } + } + + 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 ( +
+ + + + + Rename + + + + Duplicate + + + + Export + + + + + Delete + + + + setFlowDialogOpen(false)} + onConfirm={saveFlowRename} + /> +
+ ) +} + +FlowListMenu.propTypes = { + chatflow: PropTypes.object, + updateFlowsApi: PropTypes.object +} diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 9dfc95221..8dc25e96e 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types' import { useNavigate } from 'react-router-dom' -import { IconEdit } from '@tabler/icons' import moment from 'moment' import { styled } from '@mui/material/styles' import Table from '@mui/material/Table' @@ -10,7 +9,8 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' -import { Button, Typography } from '@mui/material' +import { Button, Stack, Typography } from '@mui/material' +import FlowListMenu from '../button/FlowListMenu' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -32,7 +32,7 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ } })) -export const FlowListTable = ({ data, images, filterFunction }) => { +export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) => { const navigate = useNavigate() const goToCanvas = (selectedChatflow) => { navigate(`/canvas/${selectedChatflow.id}`) @@ -44,7 +44,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => {
- + Name @@ -53,7 +53,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { Last Modified Date - + Actions @@ -65,7 +65,7 @@ export const FlowListTable = ({ data, images, filterFunction }) => { - {row.templateName || row.name} + @@ -111,15 +111,13 @@ export const FlowListTable = ({ data, images, filterFunction }) => { {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} - - + + + {/**/} + + ))} @@ -133,5 +131,6 @@ export const FlowListTable = ({ data, images, filterFunction }) => { FlowListTable.propTypes = { data: PropTypes.object, images: PropTypes.array, - filterFunction: PropTypes.func + filterFunction: PropTypes.func, + updateFlowsApi: PropTypes.object } diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index b7784a896..f008bfa87 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -23,10 +23,8 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus, IconSearch } from '@tabler/icons' +import { IconPlus, IconSearch, IconLayoutCards, IconLayoutColumns } from '@tabler/icons' 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 { FlowListTable } from '../../ui-component/table/FlowListTable' import { StyledButton } from '../../ui-component/button/StyledButton' @@ -159,10 +157,10 @@ const Chatflows = () => { > - + - + @@ -185,7 +183,13 @@ const Chatflows = () => { )} {!isLoading && view === 'list' && getAllChatflowsApi.data && ( - + )} From a7b34848cd3fe814b4e3820c2409c77f0d67c5ca Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 16 Nov 2023 08:29:06 +0530 Subject: [PATCH 06/15] UX Changes: Ability to set category (tags) to each chatflow; corresponding changes to table display and table search --- .../server/src/database/entities/ChatFlow.ts | 3 + .../1699900910291-AddCategoryToChatFlow.ts | 12 +++ .../src/database/migrations/mysql/index.ts | 4 +- .../1699900910291-AddCategoryToChatFlow.ts | 11 +++ .../src/database/migrations/postgres/index.ts | 4 +- .../1699900910291-AddCategoryToChatFlow.ts | 11 +++ .../src/database/migrations/sqlite/index.ts | 4 +- .../src/ui-component/button/FlowListMenu.js | 55 ++++++++++- .../ui/src/ui-component/dialog/TagDialog.js | 91 +++++++++++++++++++ .../src/ui-component/table/FlowListTable.js | 35 +++++-- packages/ui/src/views/chatflows/index.js | 7 +- 11 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts create mode 100644 packages/ui/src/ui-component/dialog/TagDialog.js diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index 376a100b4..b3131c2ea 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -36,4 +36,7 @@ export class ChatFlow implements IChatFlow { @UpdateDateColumn() updatedDate: Date + + @Column({ nullable: true, type: 'text' }) + category?: string } diff --git a/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 000000000..424f3b0e0 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_flow', 'category') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_flow\` ADD COLUMN \`category\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_flow\` DROP COLUMN \`category\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a954..53d652eea 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +20,6 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 000000000..f5d964391 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN IF NOT EXISTS "category" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b5..70642eb66 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryToChatFlow' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +20,6 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts b/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts new file mode 100644 index 000000000..270b29988 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1699900910291-AddCategoryToChatFlow.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCategoryToChatFlow1699900910291 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" ADD COLUMN "category" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "category";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 4a14fc407..fe7611ea6 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -8,6 +8,7 @@ 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' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +20,6 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddCategoryToChatFlow1699900910291 ] diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index fb759505c..f4ffbd32d 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -7,6 +7,7 @@ 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 PropTypes from 'prop-types' @@ -22,6 +23,7 @@ import ConfirmDialog from '../dialog/ConfirmDialog' import SaveChatflowDialog from '../dialog/SaveChatflowDialog' import { useState } from 'react' import useApi from '../../hooks/useApi' +import TagDialog from '../dialog/TagDialog' const StyledMenu = styled((props) => ( dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [anchorEl, setAnchorEl] = React.useState(null) const open = Boolean(anchorEl) const handleClick = (event) => { @@ -79,12 +83,10 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { const handleClose = () => { setAnchorEl(null) } - const handleFlowRename = () => { setAnchorEl(null) setFlowDialogOpen(true) } - const saveFlowRename = async (chatflowName) => { const updateBody = { name: chatflowName, @@ -110,7 +112,39 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } - + const handleFlowCategory = () => { + setAnchorEl(null) + if (chatflow.category) setCategoryValues(chatflow.category.split(';')) + else setCategoryValues([]) + setCategoryDialogOpen(true) + } + const saveFlowCategory = async (categories) => { + // 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 = { @@ -143,7 +177,6 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { } } } - const handleDuplicate = () => { setAnchorEl(null) try { @@ -206,6 +239,11 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { Export + + + Update Category + + Delete @@ -222,6 +260,13 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { onCancel={() => setFlowDialogOpen(false)} onConfirm={saveFlowRename} /> + setCategoryDialogOpen(false)} + tags={categoryValues} + setTags={setCategoryValues} + onSubmit={saveFlowCategory} + /> ) } 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..be133fa4e --- /dev/null +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -0,0 +1,91 @@ +import { useState } 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 } from '@mui/material' + +const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { + const [inputValue, setInputValue] = useState('') + + const handleInputChange = (event) => { + setInputValue(event.target.value) + } + + const handleInputKeyDown = (event) => { + if (event.key === 'Enter' && inputValue.trim()) { + event.preventDefault() + if (!tags.includes(inputValue)) { + setTags([...tags, inputValue]) + setInputValue('') + } + } + } + + const handleDeleteTag = (tagToDelete) => { + setTags(tags.filter((tag) => tag !== tagToDelete)) + } + + const handleSubmit = (event) => { + event.preventDefault() + onSubmit(tags) + onClose() + } + + return ( + + + Set Chatflow Category Tags + + + +
+
+ {tags.map((tag, index) => ( + handleDeleteTag(tag)} + style={{ marginRight: 5, marginBottom: 5 }} + /> + ))} +
+ + +
+
+ + + + +
+ ) +} + +TagDialog.propTypes = { + isOpen: PropTypes.bool, + onClose: PropTypes.func, + tags: PropTypes.array, + setTags: PropTypes.func, + onSubmit: PropTypes.func +} + +export default TagDialog diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 8dc25e96e..08caed57f 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -11,6 +11,7 @@ import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' import { Button, Stack, Typography } from '@mui/material' import FlowListMenu from '../button/FlowListMenu' +import Chip from '@mui/material/Chip' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { @@ -47,13 +48,16 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Name - - Nodes (Showing first 5) + + Category - Last Modified Date + Nodes (Showing first 5) + Last Modified Date + + Actions @@ -68,8 +72,25 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - +
+   + {row.category && + row.category + .split(';') + .map((tag, index) => ( + + ))} +
+
+ {images[row.id] && (
)} - - {moment(row.updatedDate).format('dddd, MMMM Do, YYYY h:mm:ss A')} - + {moment(row.updatedDate).format('MMMM Do, YYYY')} + + {/**/} diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 44c670d62..34c6523b7 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -23,7 +23,7 @@ import useApi from 'hooks/useApi' import { baseURL } from 'store/constant' // icons -import { IconPlus, IconSearch, IconLayoutCards, IconLayoutColumns } from '@tabler/icons' +import { IconPlus, IconSearch, IconLayoutGrid, IconList } from '@tabler/icons' import * as React from 'react' import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' import { FlowListTable } from '../../ui-component/table/FlowListTable' @@ -138,7 +138,7 @@ const Chatflows = () => {

Chatflows

{ }} /> - + - - - + + + - - + + From 97247713ef7c2f0134c3114c1de84fea040a47f5 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 17 Nov 2023 11:24:55 +0530 Subject: [PATCH 08/15] UX Changes: Fixed 2 edge cases, (1) UI-tag not added, when the user clicks the submit without hitting enter and (2) server side error when the update/rename is attempted before any flow is opened --- packages/server/src/index.ts | 8 ++++++-- packages/ui/src/ui-component/dialog/TagDialog.js | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0e..5a5b2eda7 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -355,8 +355,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) }) diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js index be133fa4e..778bf2cce 100644 --- a/packages/ui/src/ui-component/dialog/TagDialog.js +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -5,7 +5,7 @@ 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 } from '@mui/material' +import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const [inputValue, setInputValue] = useState('') @@ -30,6 +30,9 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const handleSubmit = (event) => { event.preventDefault() + if (inputValue.trim() && !tags.includes(inputValue)) { + setTags([...tags, inputValue]) + } onSubmit(tags) onClose() } @@ -67,6 +70,9 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { label='Add a tag' variant='outlined' /> + + Enter a tag and press enter to add it to the list. You can add as many tags as you want. + From a0397c008e035ec64e8063b4b2c7859a5d2eadc8 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 17 Nov 2023 11:25:59 +0530 Subject: [PATCH 09/15] UX Changes: Column display fixes for 'xs' mode --- packages/ui/src/ui-component/table/FlowListTable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index 68641d442..e33a8ba13 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -48,7 +48,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Name - + Category @@ -72,7 +72,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - +
Date: Fri, 17 Nov 2023 12:29:14 +0530 Subject: [PATCH 10/15] UX Changes: Addition of search filters for API Keys and Credentials. --- packages/ui/src/views/apikey/index.js | 71 ++++++++++++++--- packages/ui/src/views/credentials/index.js | 88 ++++++++++++++++++---- 2 files changed, 136 insertions(+), 23 deletions(-) diff --git a/packages/ui/src/views/apikey/index.js b/packages/ui/src/views/apikey/index.js index a2b2e639f..e08baac2d 100644 --- a/packages/ui/src/views/apikey/index.js +++ b/packages/ui/src/views/apikey/index.js @@ -16,7 +16,11 @@ import { Paper, IconButton, Popover, - Typography + Typography, + Toolbar, + TextField, + InputAdornment, + ButtonGroup } from '@mui/material' import { useTheme } from '@mui/material/styles' @@ -37,7 +41,7 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons' +import { IconTrash, IconEdit, IconCopy, IconX, IconPlus, IconEye, IconEyeOff, IconSearch } from '@tabler/icons' import APIEmptySVG from 'assets/images/api_empty.svg' // ==============================|| APIKey ||============================== // @@ -59,6 +63,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) @@ -171,12 +183,53 @@ const APIKey = () => { <> -

API Keys 

- - - }> - Create Key - + + +

API Keys 

+ + + + ) + }} + /> + + + + } + > + Create Key + + + +
+
{apiKeys.length <= 0 && ( @@ -199,7 +252,7 @@ const APIKey = () => { - {apiKeys.map((key, index) => ( + {apiKeys.filter(filterKeys).map((key, index) => ( {key.keyName} diff --git a/packages/ui/src/views/credentials/index.js b/packages/ui/src/views/credentials/index.js index 9db990a7c..31e358314 100644 --- a/packages/ui/src/views/credentials/index.js +++ b/packages/ui/src/views/credentials/index.js @@ -4,7 +4,23 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import moment from 'moment' // material-ui -import { Button, Box, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material' +import { + Button, + Box, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Toolbar, + TextField, + InputAdornment, + ButtonGroup +} from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -25,7 +41,7 @@ import useConfirm from 'hooks/useConfirm' import useNotifier from 'utils/useNotifier' // Icons -import { IconTrash, IconEdit, IconX, IconPlus } from '@tabler/icons' +import { IconTrash, IconEdit, IconX, IconPlus, IconSearch } from '@tabler/icons' import CredentialEmptySVG from 'assets/images/credential_empty.svg' // const @@ -56,6 +72,14 @@ const Credentials = () => { 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) => (
Date: Fri, 17 Nov 2023 12:35:01 +0000 Subject: [PATCH 11/15] add fix where tags are not added when submit is clicked without enter --- .../src/ui-component/button/FlowListMenu.js | 57 ++++++++++------- .../ui/src/ui-component/dialog/TagDialog.js | 63 +++++++++++-------- .../src/ui-component/table/FlowListTable.js | 2 +- 3 files changed, 75 insertions(+), 47 deletions(-) diff --git a/packages/ui/src/ui-component/button/FlowListMenu.js b/packages/ui/src/ui-component/button/FlowListMenu.js index 441922982..b242d2cb2 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.js +++ b/packages/ui/src/ui-component/button/FlowListMenu.js @@ -1,4 +1,7 @@ -import * as React from 'react' +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' @@ -10,21 +13,22 @@ 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 PropTypes from 'prop-types' -import { uiBaseURL } from '../../store/constant' -import { generateExportFlowData } from '../../utils/genericHelper' -import chatflowsApi from 'api/chatflows' -import useConfirm from 'hooks/useConfirm' -import useNotifier from '../../utils/useNotifier' -import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '../../store/actions' import { IconX } from '@tabler/icons' -import { useDispatch } from 'react-redux' + +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 { useState } from 'react' -import useApi from '../../hooks/useApi' import TagDialog from '../dialog/TagDialog' +import { generateExportFlowData } from '../../utils/genericHelper' +import useNotifier from '../../utils/useNotifier' + const StyledMenu = styled((props) => ( ( export default function FlowListMenu({ chatflow, updateFlowsApi }) { const { confirm } = useConfirm() const dispatch = useDispatch() - const [flowDialogOpen, setFlowDialogOpen] = useState(false) - const [categoryValues, setCategoryValues] = useState([]) - - const [categoryDialogOpen, setCategoryDialogOpen] = useState(false) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) - // ==============================|| Snackbar ||============================== // useNotifier() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - const [anchorEl, setAnchorEl] = React.useState(null) + + 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, @@ -111,13 +118,19 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } + const handleFlowCategory = () => { setAnchorEl(null) - if (chatflow.category) setCategoryValues(chatflow.category.split(';')) - else setCategoryValues([]) + 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 = { @@ -144,6 +157,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { }) } } + const handleDelete = async () => { setAnchorEl(null) const confirmPayload = { @@ -176,6 +190,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { } } } + const handleDuplicate = () => { setAnchorEl(null) try { @@ -185,6 +200,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { console.error(e) } } + const handleExport = () => { setAnchorEl(null) try { @@ -261,9 +277,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { /> setCategoryDialogOpen(false)} - tags={categoryValues} - setTags={setCategoryValues} onSubmit={saveFlowCategory} />
diff --git a/packages/ui/src/ui-component/dialog/TagDialog.js b/packages/ui/src/ui-component/dialog/TagDialog.js index 778bf2cce..82c35dde6 100644 --- a/packages/ui/src/ui-component/dialog/TagDialog.js +++ b/packages/ui/src/ui-component/dialog/TagDialog.js @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import Dialog from '@mui/material/Dialog' import Box from '@mui/material/Box' import Button from '@mui/material/Button' @@ -7,8 +7,9 @@ import Chip from '@mui/material/Chip' import PropTypes from 'prop-types' import { DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' -const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { +const TagDialog = ({ isOpen, dialogProps, onClose, onSubmit }) => { const [inputValue, setInputValue] = useState('') + const [categoryValues, setCategoryValues] = useState([]) const handleInputChange = (event) => { setInputValue(event.target.value) @@ -17,34 +18,44 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { const handleInputKeyDown = (event) => { if (event.key === 'Enter' && inputValue.trim()) { event.preventDefault() - if (!tags.includes(inputValue)) { - setTags([...tags, inputValue]) + if (!categoryValues.includes(inputValue)) { + setCategoryValues([...categoryValues, inputValue]) setInputValue('') } } } - const handleDeleteTag = (tagToDelete) => { - setTags(tags.filter((tag) => tag !== tagToDelete)) + const handleDeleteTag = (categoryToDelete) => { + setCategoryValues(categoryValues.filter((category) => category !== categoryToDelete)) } const handleSubmit = (event) => { event.preventDefault() - if (inputValue.trim() && !tags.includes(inputValue)) { - setTags([...tags, inputValue]) + let newCategories = [...categoryValues] + if (inputValue.trim() && !categoryValues.includes(inputValue)) { + newCategories = [...newCategories, inputValue] + setCategoryValues(newCategories) } - onSubmit(tags) - onClose() + onSubmit(newCategories) } + useEffect(() => { + if (dialogProps.category) setCategoryValues(dialogProps.category) + + return () => { + setInputValue('') + setCategoryValues([]) + } + }, [dialogProps]) + return ( Set Chatflow Category Tags @@ -52,17 +63,20 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => {
-
- {tags.map((tag, index) => ( - handleDeleteTag(tag)} - style={{ marginRight: 5, marginBottom: 5 }} - /> - ))} -
+ {categoryValues.length > 0 && ( +
+ {categoryValues.map((category, index) => ( + handleDeleteTag(category)} + style={{ marginRight: 5, marginBottom: 5 }} + /> + ))} +
+ )} { label='Add a tag' variant='outlined' /> - + Enter a tag and press enter to add it to the list. You can add as many tags as you want. @@ -88,9 +102,8 @@ const TagDialog = ({ isOpen, onClose, tags, setTags, onSubmit }) => { TagDialog.propTypes = { isOpen: PropTypes.bool, + dialogProps: PropTypes.object, onClose: PropTypes.func, - tags: PropTypes.array, - setTags: PropTypes.func, onSubmit: PropTypes.func } diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index e33a8ba13..c879d82d5 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -9,9 +9,9 @@ import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableRow from '@mui/material/TableRow' import Paper from '@mui/material/Paper' +import Chip from '@mui/material/Chip' import { Button, Stack, Typography } from '@mui/material' import FlowListMenu from '../button/FlowListMenu' -import Chip from '@mui/material/Chip' const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { From 0b4bf0193123e955fc990a8180139528ff7f6cd5 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Nov 2023 12:44:16 +0000 Subject: [PATCH 12/15] unhide columns when xs mode --- .../ui/src/ui-component/table/FlowListTable.js | 14 ++++++-------- packages/ui/src/views/chatflows/index.js | 7 +------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/ui-component/table/FlowListTable.js b/packages/ui/src/ui-component/table/FlowListTable.js index c879d82d5..e3baa2e28 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.js +++ b/packages/ui/src/ui-component/table/FlowListTable.js @@ -51,13 +51,13 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) Category - + Nodes - + Last Modified Date - + Actions
@@ -90,7 +90,7 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) ))}
- + {images[row.id] && (
)} - - {moment(row.updatedDate).format('MMMM Do, YYYY')} - - + {moment(row.updatedDate).format('MMMM Do, YYYY')} + diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 34c6523b7..7f288a952 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -152,12 +152,7 @@ const Chatflows = () => { /> - + Date: Mon, 20 Nov 2023 00:55:58 +0000 Subject: [PATCH 13/15] add file annotations, sync and delete assistant --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 125 +++++++++++-- packages/server/src/Interface.ts | 1 + .../src/database/entities/ChatMessage.ts | 3 + ...1021237-AddFileAnnotationsToChatMessage.ts | 12 ++ .../src/database/migrations/mysql/index.ts | 4 +- ...1699481607341-AddUsedToolsToChatMessage.ts | 2 +- ...1021237-AddFileAnnotationsToChatMessage.ts | 11 ++ .../src/database/migrations/postgres/index.ts | 4 +- ...1021237-AddFileAnnotationsToChatMessage.ts | 20 +++ .../src/database/migrations/sqlite/index.ts | 4 +- packages/server/src/index.ts | 16 +- packages/ui/src/api/assistants.js | 3 +- .../ui-component/dialog/ViewMessagesDialog.js | 50 +++++- .../src/views/assistants/AssistantDialog.js | 165 ++++++++++++------ .../views/assistants/DeleteConfirmDialog.js | 47 +++++ .../ui/src/views/chatmessage/ChatMessage.js | 51 +++++- 16 files changed, 436 insertions(+), 82 deletions(-) create mode 100644 packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts create mode 100644 packages/ui/src/views/assistants/DeleteConfirmDialog.js diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 56e1b290e..ed7baf7de 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -111,7 +111,7 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) options.logger.info(`Clearing OpenAI Thread ${sessionId}`) - await openai.beta.threads.del(sessionId) + if (sessionId) await openai.beta.threads.del(sessionId) options.logger.info(`Successfully cleared OpenAI Thread ${sessionId}`) } @@ -135,16 +135,25 @@ class OpenAIAssistant_Agents implements INode { const openai = new OpenAI({ apiKey: openAIApiKey }) - // Retrieve assistant try { const assistantDetails = JSON.parse(assistant.details) const openAIAssistantId = assistantDetails.id + + // Retrieve assistant const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId) if (formattedTools.length) { - let filteredTools = uniqWith([...retrievedAssistant.tools, ...formattedTools], isEqual) + let filteredTools = [] + for (const tool of retrievedAssistant.tools) { + if (tool.type === 'code_interpreter' || tool.type === 'retrieval') filteredTools.push(tool) + } + filteredTools = uniqWith([...filteredTools, ...formattedTools], isEqual) + // filter out tool with empty function filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function)) await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools }) + } else { + let filteredTools = retrievedAssistant.tools.filter((tool) => tool.type !== 'function') + await openai.beta.assistants.update(openAIAssistantId, { tools: filteredTools }) } const chatmessage = await appDataSource.getRepository(databaseEntities['ChatMessage']).findOneBy({ @@ -152,14 +161,45 @@ class OpenAIAssistant_Agents implements INode { }) let threadId = '' + let isNewThread = false if (!chatmessage) { const thread = await openai.beta.threads.create({}) threadId = thread.id + isNewThread = true } else { const thread = await openai.beta.threads.retrieve(chatmessage.sessionId) threadId = thread.id } + // List all runs + if (!isNewThread) { + const promise = (threadId: string) => { + return new Promise((resolve) => { + const timeout = setInterval(async () => { + const allRuns = await openai.beta.threads.runs.list(threadId) + if (allRuns.data && allRuns.data.length) { + const firstRunId = allRuns.data[0].id + const runStatus = allRuns.data.find((run) => run.id === firstRunId)?.status + if ( + runStatus && + (runStatus === 'cancelled' || + runStatus === 'completed' || + runStatus === 'expired' || + runStatus === 'failed') + ) { + clearInterval(timeout) + resolve() + } + } else { + clearInterval(timeout) + resolve() + } + }, 500) + }) + } + await promise(threadId) + } + // Add message to thread await openai.beta.threads.messages.create(threadId, { role: 'user', @@ -217,27 +257,41 @@ class OpenAIAssistant_Agents implements INode { }) resolve(state) } else { - reject( - new Error( - `Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}. submit_tool_outputs.tool_calls are empty` - ) - ) + await openai.beta.threads.runs.cancel(threadId, runId) + resolve('requires_action_retry') } } } else if (state === 'cancelled' || state === 'expired' || state === 'failed') { clearInterval(timeout) - reject(new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}`)) + reject( + new Error(`Error processing thread: ${state}, Thread ID: ${threadId}, Run ID: ${runId}, Status: ${state}`) + ) } }, 500) }) } // Polling run status + let runThreadId = runThread.id let state = await promise(threadId, runThread.id) while (state === 'requires_action') { state = await promise(threadId, runThread.id) } + let retries = 3 + while (state === 'requires_action_retry') { + if (retries > 0) { + retries -= 1 + const newRunThread = await openai.beta.threads.runs.create(threadId, { + assistant_id: retrievedAssistant.id + }) + runThreadId = newRunThread.id + state = await promise(threadId, newRunThread.id) + } else { + throw new Error(`Error processing thread: ${state}, Thread ID: ${threadId}`) + } + } + // List messages const messages = await openai.beta.threads.messages.list(threadId) const messageData = messages.data ?? [] @@ -245,12 +299,58 @@ class OpenAIAssistant_Agents implements INode { if (!assistantMessages.length) return '' let returnVal = '' + const fileAnnotations = [] for (let i = 0; i < assistantMessages[0].content.length; i += 1) { if (assistantMessages[0].content[i].type === 'text') { const content = assistantMessages[0].content[i] as MessageContentText - returnVal += content.text.value - //TODO: handle annotations + if (content.text.annotations) { + const message_content = content.text + const annotations = message_content.annotations + + const dirPath = path.join(getUserHome(), '.flowise', 'openai-assistant') + + // Iterate over the annotations and add footnotes + for (let index = 0; index < annotations.length; index++) { + const annotation = annotations[index] + let filePath = '' + + // Gather citations based on annotation attributes + const file_citation = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FileCitation) + .file_citation + if (file_citation) { + const cited_file = await openai.files.retrieve(file_citation.file_id) + // eslint-disable-next-line no-useless-escape + const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename + filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName) + await downloadFile(cited_file, filePath, dirPath, openAIApiKey) + fileAnnotations.push({ + filePath, + fileName + }) + } else { + const file_path = (annotation as OpenAI.Beta.Threads.Messages.MessageContentText.Text.FilePath).file_path + if (file_path) { + const cited_file = await openai.files.retrieve(file_path.file_id) + // eslint-disable-next-line no-useless-escape + const fileName = cited_file.filename.split(/[\/\\]/).pop() ?? cited_file.filename + filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', fileName) + await downloadFile(cited_file, filePath, dirPath, openAIApiKey) + fileAnnotations.push({ + filePath, + fileName + }) + } + } + + // Replace the text with a footnote + message_content.value = message_content.value.replace(`${annotation.text}`, `${filePath}`) + } + + returnVal += message_content.value + } else { + returnVal += content.text.value + } } else { const content = assistantMessages[0].content[i] as MessageContentImageFile const fileId = content.image_file.file_id @@ -271,7 +371,8 @@ class OpenAIAssistant_Agents implements INode { return { text: returnVal, usedTools, - assistant: { assistantId: openAIAssistantId, threadId, runId: runThread.id, messages: messageData } + fileAnnotations, + assistant: { assistantId: openAIAssistantId, threadId, runId: runThreadId, messages: messageData } } } catch (error) { throw new Error(error) diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 8d0965f48..1d2724642 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -30,6 +30,7 @@ export interface IChatMessage { chatflowid: string sourceDocuments?: string usedTools?: string + fileAnnotations?: string chatType: string chatId: string memoryType?: string diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index b51aa4340..4054a26dd 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -23,6 +23,9 @@ export class ChatMessage implements IChatMessage { @Column({ nullable: true, type: 'text' }) usedTools?: string + @Column({ nullable: true, type: 'text' }) + fileAnnotations?: string + @Column() chatType: string diff --git a/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..a352cde8c --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const columnExists = await queryRunner.hasColumn('chat_message', 'fileAnnotations') + if (!columnExists) queryRunner.query(`ALTER TABLE \`chat_message\` ADD COLUMN \`fileAnnotations\` TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`chat_message\` DROP COLUMN \`fileAnnotations\`;`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index 4b7b8a954..eff089cda 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658767766 } from './1694658767766-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const mysqlMigrations = [ Init1693840429259, @@ -19,5 +20,6 @@ export const mysqlMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658767766, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts index f9f893f82..ae34c8132 100644 --- a/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts +++ b/packages/server/src/database/migrations/postgres/1699481607341-AddUsedToolsToChatMessage.ts @@ -6,6 +6,6 @@ export class AddUsedToolsToChatMessage1699481607341 implements MigrationInterfac } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "chat_flow" DROP COLUMN "usedTools";`) + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "usedTools";`) } } diff --git a/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..8824f57d5 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" ADD COLUMN IF NOT EXISTS "fileAnnotations" TEXT;`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "fileAnnotations";`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 75562c0b5..93d02f3e0 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694658756136 } from './1694658756136-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const postgresMigrations = [ Init1693891895163, @@ -19,5 +20,6 @@ export const postgresMigrations = [ AddAnalytic1694432361423, AddChatHistory1694658756136, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts new file mode 100644 index 000000000..af29fba4b --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1700271021237-AddFileAnnotationsToChatMessage.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddFileAnnotationsToChatMessage1700271021237 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR);` + ) + await queryRunner.query( + `INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";` + ) + await queryRunner.query(`DROP TABLE "chat_message";`) + await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`) + await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`) + } + + public async down(queryRunner: QueryRunner): Promise { + 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..edba59305 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -8,6 +8,7 @@ import { AddAnalytic1694432361423 } from './1694432361423-AddAnalytic' import { AddChatHistory1694657778173 } from './1694657778173-AddChatHistory' import { AddAssistantEntity1699325775451 } from './1699325775451-AddAssistantEntity' import { AddUsedToolsToChatMessage1699481607341 } from './1699481607341-AddUsedToolsToChatMessage' +import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' export const sqliteMigrations = [ Init1693835579790, @@ -19,5 +20,6 @@ export const sqliteMigrations = [ AddAnalytic1694432361423, AddChatHistory1694657778173, AddAssistantEntity1699325775451, - AddUsedToolsToChatMessage1699481607341 + AddUsedToolsToChatMessage1699481607341, + AddFileAnnotationsToChatMessage1700271021237 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ba6c3ce0e..57245f2cd 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) => { @@ -782,8 +783,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 +953,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 +962,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 // ---------------------------------------- @@ -1499,6 +1508,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/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/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index 2e52d5963..29a64155c 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -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 { @@ -28,7 +29,7 @@ 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 { IconFileExport, IconEraser, IconX, IconDownload } from '@tabler/icons' // Project import import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown' @@ -48,6 +49,7 @@ import useConfirm from 'hooks/useConfirm' // Utils 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' @@ -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,6 +672,30 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { {message.message}
+ {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
+ )} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { diff --git a/packages/ui/src/views/assistants/AssistantDialog.js b/packages/ui/src/views/assistants/AssistantDialog.js index e841fc4fa..30087baed 100644 --- a/packages/ui/src/views/assistants/AssistantDialog.js +++ b/packages/ui/src/views/assistants/AssistantDialog.js @@ -9,12 +9,12 @@ import { Box, Typography, Button, IconButton, Dialog, DialogActions, DialogConte 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 DeleteConfirmDialog from './DeleteConfirmDialog' // Icons import { IconX } from '@tabler/icons' @@ -23,7 +23,6 @@ import { IconX } from '@tabler/icons' import assistantsApi from 'api/assistants' // Hooks -import useConfirm from 'hooks/useConfirm' import useApi from 'hooks/useApi' // utils @@ -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) => ( + ) + } + }) + setLoading(false) + } + } + + const onDeleteClick = () => { + setDeleteDialogProps({ + title: `Delete Assistant`, + description: `Delete Assistant ${assistantName}?`, + cancelButtonName: 'Cancel' + }) + setDeleteDialogOpen(true) + } + + const deleteAssistant = async (isDeleteBoth) => { + setDeleteDialogOpen(false) + try { + const delResp = await assistantsApi.deleteAssistant(assistantId, isDeleteBoth) + 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}` + enqueueSnackbar({ + message: `Failed to delete Assistant: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + onCancel() } } @@ -578,7 +616,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.type === 'EDIT' && ( - deleteAssistant()}> + onSyncClick()}> + Sync + + )} + {dialogProps.type === 'EDIT' && ( + onDeleteClick()}> Delete )} @@ -590,7 +633,13 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.confirmButtonName} - + setDeleteDialogOpen(false)} + onDelete={() => deleteAssistant()} + onDeleteBoth={() => deleteAssistant(true)} + /> {loading && } ) : null diff --git a/packages/ui/src/views/assistants/DeleteConfirmDialog.js b/packages/ui/src/views/assistants/DeleteConfirmDialog.js new file mode 100644 index 000000000..f4453631b --- /dev/null +++ b/packages/ui/src/views/assistants/DeleteConfirmDialog.js @@ -0,0 +1,47 @@ +import { createPortal } from 'react-dom' +import PropTypes from 'prop-types' +import { Button, Dialog, DialogContent, DialogTitle } from '@mui/material' +import { StyledButton } from 'ui-component/button/StyledButton' + +const DeleteConfirmDialog = ({ show, dialogProps, onCancel, onDelete, onDeleteBoth }) => { + const portalElement = document.getElementById('portal') + + const component = show ? ( + + + {dialogProps.title} + + + {dialogProps.description} +
+ + Delete only from Flowise + + + Delete from both OpenAI and Flowise + + +
+
+
+ ) : null + + return createPortal(component, portalElement) +} + +DeleteConfirmDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onDeleteBoth: PropTypes.func, + onDelete: PropTypes.func, + onCancel: PropTypes.func +} + +export default DeleteConfirmDialog diff --git a/packages/ui/src/views/chatmessage/ChatMessage.js b/packages/ui/src/views/chatmessage/ChatMessage.js index 0cf5695be..7cfd04740 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.js +++ b/packages/ui/src/views/chatmessage/ChatMessage.js @@ -7,10 +7,11 @@ import rehypeMathjax from 'rehype-mathjax' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' +import axios from 'axios' -import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip } from '@mui/material' +import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconSend } from '@tabler/icons' +import { IconSend, IconDownload } from '@tabler/icons' // project import import { CodeBlock } from 'ui-component/markdown/CodeBlock' @@ -139,7 +140,13 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { setMessages((prevMessages) => [ ...prevMessages, - { message: text, sourceDocuments: data?.sourceDocuments, usedTools: data?.usedTools, type: 'apiMessage' } + { + message: text, + sourceDocuments: data?.sourceDocuments, + usedTools: data?.usedTools, + fileAnnotations: data?.fileAnnotations, + type: 'apiMessage' + } ]) } @@ -170,6 +177,26 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } } + 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) + } + } + // Get chatmessages successful useEffect(() => { if (getChatmessageApi.data?.length) { @@ -183,6 +210,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { } if (message.sourceDocuments) obj.sourceDocuments = JSON.parse(message.sourceDocuments) if (message.usedTools) obj.usedTools = JSON.parse(message.usedTools) + if (message.fileAnnotations) obj.fileAnnotations = JSON.parse(message.fileAnnotations) return obj }) setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) @@ -331,6 +359,23 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => { {message.message}
+ {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ) + })} +
+ )} {message.sourceDocuments && (
{removeDuplicateURL(message).map((source, index) => { From c7bf75e2597809fbdb56c04282059ece22f73f66 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Mon, 20 Nov 2023 18:19:25 +0530 Subject: [PATCH 14/15] UX Changes: persist user display choice in localStorage --- packages/ui/src/views/chatflows/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index 7f288a952..3c4b89728 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -43,9 +43,10 @@ const Chatflows = () => { const [loginDialogProps, setLoginDialogProps] = useState({}) const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows) - const [view, setView] = React.useState('card') + const [view, setView] = React.useState(localStorage.getItem('flowDisplayStyle') || 'card') const handleChange = (event, nextView) => { + localStorage.setItem('flowDisplayStyle', nextView) setView(nextView) } From 9a3be5f4bf35706adc8cc65466b65c50b85d1f22 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Nov 2023 14:38:33 +0000 Subject: [PATCH 15/15] fix sessionid undefined --- packages/server/src/utils/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 239773a9a..6eb979f62 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -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 }