Fix merge conflicts

This commit is contained in:
Ilango
2024-03-06 16:03:12 +05:30
636 changed files with 29474 additions and 5308 deletions
+92 -8
View File
@@ -22,7 +22,9 @@ import {
Popper,
Stack,
Typography,
Chip
Chip,
Tab,
Tabs
} from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
@@ -36,12 +38,20 @@ import { StyledFab } from '@/ui-component/button/StyledFab'
// icons
import { IconPlus, IconSearch, IconMinus, IconX } from '@tabler/icons'
import LlamaindexPNG from 'assets/images/llamaindex.png'
import LangChainPNG from 'assets/images/langchain.png'
// const
import { baseURL } from '@/store/constant'
import { SET_COMPONENT_NODES } from '@/store/actions'
// ==============================|| ADD NODES||============================== //
function a11yProps(index) {
return {
id: `attachment-tab-${index}`,
'aria-controls': `attachment-tabpanel-${index}`
}
}
const AddNodes = ({ nodesData, node }) => {
const theme = useTheme()
@@ -52,6 +62,7 @@ const AddNodes = ({ nodesData, node }) => {
const [nodes, setNodes] = useState({})
const [open, setOpen] = useState(false)
const [categoryExpanded, setCategoryExpanded] = useState({})
const [tabValue, setTabValue] = useState(0)
const anchorRef = useRef(null)
const prevOpen = useRef(open)
@@ -86,6 +97,11 @@ const AddNodes = ({ nodesData, node }) => {
}
}
const handleTabChange = (event, newValue) => {
setTabValue(newValue)
filterSearch(searchValue, newValue)
}
const getSearchedNodes = (value) => {
const passed = nodesData.filter((nd) => {
const passesQuery = nd.name.toLowerCase().includes(value.toLowerCase())
@@ -95,23 +111,34 @@ const AddNodes = ({ nodesData, node }) => {
return passed
}
const filterSearch = (value) => {
const filterSearch = (value, newTabValue) => {
setSearchValue(value)
setTimeout(() => {
if (value) {
const returnData = getSearchedNodes(value)
groupByCategory(returnData, true)
groupByCategory(returnData, newTabValue ?? tabValue, true)
scrollTop()
} else if (value === '') {
groupByCategory(nodesData)
groupByCategory(nodesData, newTabValue ?? tabValue)
scrollTop()
}
}, 500)
}
const groupByCategory = (nodes, isFilter) => {
const groupByTags = (nodes, newTabValue = 0) => {
const langchainNodes = nodes.filter((nd) => !nd.tags)
const llmaindexNodes = nodes.filter((nd) => nd.tags && nd.tags.includes('LlamaIndex'))
if (newTabValue === 0) {
return langchainNodes
} else {
return llmaindexNodes
}
}
const groupByCategory = (nodes, newTabValue, isFilter) => {
const taggedNodes = groupByTags(nodes, newTabValue)
const accordianCategories = {}
const result = nodes.reduce(function (r, a) {
const result = taggedNodes.reduce(function (r, a) {
r[a.category] = r[a.category] || []
r[a.category].push(a)
accordianCategories[a.category] = isFilter ? true : false
@@ -244,15 +271,72 @@ const AddNodes = ({ nodesData, node }) => {
'aria-label': 'weight'
}}
/>
<Tabs
sx={{ position: 'relative', minHeight: '50px', height: '50px' }}
variant='fullWidth'
value={tabValue}
onChange={handleTabChange}
aria-label='tabs'
>
{['LangChain', 'LlamaIndex'].map((item, index) => (
<Tab
icon={
<div
style={{
borderRadius: '50%'
}}
>
<img
style={{
width: '25px',
height: '25px',
borderRadius: '50%',
objectFit: 'contain'
}}
src={index === 0 ? LangChainPNG : LlamaindexPNG}
alt={item}
/>
</div>
}
iconPosition='start'
sx={{ minHeight: '50px', height: '50px' }}
key={index}
label={item}
{...a11yProps(index)}
></Tab>
))}
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
background: 'rgb(254,252,191)',
paddingLeft: 6,
paddingRight: 6,
paddingTop: 1,
paddingBottom: 1,
width: 'max-content',
position: 'absolute',
top: 0,
right: 0,
fontSize: '0.65rem',
fontWeight: 700
}}
>
<span style={{ color: 'rgb(116,66,16)' }}>BETA</span>
</div>
</Tabs>
<Divider />
</Box>
<PerfectScrollbar
containerRef={(el) => {
ps.current = el
}}
style={{ height: '100%', maxHeight: 'calc(100vh - 320px)', overflowX: 'hidden' }}
style={{ height: '100%', maxHeight: 'calc(100vh - 380px)', overflowX: 'hidden' }}
>
<Box sx={{ p: 2 }}>
<Box sx={{ p: 2, pt: 0 }}>
<List
sx={{
width: '100%',
@@ -16,6 +16,8 @@ import SaveChatflowDialog from '@/ui-component/dialog/SaveChatflowDialog'
import APICodeDialog from '@/views/chatflows/APICodeDialog'
import AnalyseFlowDialog from '@/ui-component/dialog/AnalyseFlowDialog'
import ViewMessagesDialog from '@/ui-component/dialog/ViewMessagesDialog'
import StarterPromptsDialog from '@/ui-component/dialog/StarterPromptsDialog'
import SpeechToTextDialog from '@/ui-component/dialog/SpeechToTextDialog'
// API
import chatflowsApi from '@/api/chatflows'
@@ -45,6 +47,10 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
const [apiDialogProps, setAPIDialogProps] = useState({})
const [analyseDialogOpen, setAnalyseDialogOpen] = useState(false)
const [analyseDialogProps, setAnalyseDialogProps] = useState({})
const [speechToAudioDialogOpen, setSpeechToAudioDialogOpen] = useState(false)
const [speechToAudioDialogProps, setSpeechToAudioialogProps] = useState({})
const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false)
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false)
const [viewMessagesDialogProps, setViewMessagesDialogProps] = useState({})
@@ -56,12 +62,24 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
if (setting === 'deleteChatflow') {
handleDeleteFlow()
} else if (setting === 'conversationStarters') {
setConversationStartersDialogProps({
title: 'Starter Prompts - ' + chatflow.name,
chatflow: chatflow
})
setConversationStartersDialogOpen(true)
} else if (setting === 'analyseChatflow') {
setAnalyseDialogProps({
title: 'Analyse Chatflow',
chatflow: chatflow
})
setAnalyseDialogOpen(true)
} else if (setting === 'enableSpeechToText') {
setSpeechToAudioialogProps({
title: 'Speech to Text',
chatflow: chatflow
})
setSpeechToAudioDialogOpen(true)
} else if (setting === 'viewMessages') {
setViewMessagesDialogProps({
title: 'View Messages',
@@ -376,6 +394,17 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
/>
<APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} />
<AnalyseFlowDialog show={analyseDialogOpen} dialogProps={analyseDialogProps} onCancel={() => setAnalyseDialogOpen(false)} />
<SpeechToTextDialog
show={speechToAudioDialogOpen}
dialogProps={speechToAudioDialogProps}
onCancel={() => setSpeechToAudioDialogOpen(false)}
/>
<StarterPromptsDialog
show={conversationStartersDialogOpen}
dialogProps={conversationStartersDialogProps}
onConfirm={() => setConversationStartersDialogOpen(false)}
onCancel={() => setConversationStartersDialogOpen(false)}
/>
<ViewMessagesDialog
show={viewMessagesDialogOpen}
dialogProps={viewMessagesDialogProps}
+26 -31
View File
@@ -3,12 +3,13 @@ import { useContext, useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { useTheme } from '@mui/material/styles'
import { IconButton, Box, Typography, Divider, Button } from '@mui/material'
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip'
import Tooltip from '@mui/material/Tooltip'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
import NodeCardWrapper from '@/ui-component/cards/NodeCardWrapper'
import NodeTooltip from '@/ui-component/tooltip/NodeTooltip'
import NodeInputHandler from './NodeInputHandler'
import NodeOutputHandler from './NodeOutputHandler'
import AdditionalParamsDialog from '@/ui-component/dialog/AdditionalParamsDialog'
@@ -18,28 +19,7 @@ import NodeInfoDialog from '@/ui-component/dialog/NodeInfoDialog'
import { baseURL } from '@/store/constant'
import { IconTrash, IconCopy, IconInfoCircle, IconAlertTriangle } from '@tabler/icons'
import { flowContext } from '@/store/context/ReactFlowContext'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
border: 'solid 1px',
borderColor: theme.palette.primary[200] + 75,
width: '300px',
height: 'auto',
padding: '10px',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
'&:hover': {
borderColor: theme.palette.primary.main
}
}))
const LightTooltip = styled(({ className, ...props }) => <Tooltip {...props} classes={{ popper: className }} />)(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.nodeToolTip.background,
color: theme.palette.nodeToolTip.color,
boxShadow: theme.shadows[1]
}
}))
import LlamaindexPNG from '@/assets/images/llamaindex.png'
// ===========================|| CANVAS NODE ||=========================== //
@@ -93,7 +73,7 @@ const CanvasNode = ({ data }) => {
return (
<>
<CardWrapper
<NodeCardWrapper
content={false}
sx={{
padding: 0,
@@ -101,7 +81,7 @@ const CanvasNode = ({ data }) => {
}}
border={false}
>
<LightTooltip
<NodeTooltip
open={!canvas.canvasDialogShow && open}
onClose={handleClose}
onOpen={handleOpen}
@@ -179,9 +159,25 @@ const CanvasNode = ({ data }) => {
{data.label}
</Typography>
</Box>
<div style={{ flexGrow: 1 }}></div>
{data.tags && data.tags.includes('LlamaIndex') && (
<>
<div
style={{
borderRadius: '50%',
padding: 15
}}
>
<img
style={{ width: '25px', height: '25px', borderRadius: '50%', objectFit: 'contain' }}
src={LlamaindexPNG}
alt='LlamaIndex'
/>
</div>
</>
)}
{warningMessage && (
<>
<div style={{ flexGrow: 1 }}></div>
<Tooltip title={<span style={{ whiteSpace: 'pre-line' }}>{warningMessage}</span>} placement='top'>
<IconButton sx={{ height: 35, width: 35 }}>
<IconAlertTriangle size={35} color='orange' />
@@ -242,13 +238,12 @@ const CanvasNode = ({ data }) => {
</Typography>
</Box>
<Divider />
{data.outputAnchors.map((outputAnchor, index) => (
<NodeOutputHandler key={index} outputAnchor={outputAnchor} data={data} />
))}
</Box>
</LightTooltip>
</CardWrapper>
</NodeTooltip>
</NodeCardWrapper>
<AdditionalParamsDialog
show={showDialog}
dialogProps={dialogProps}
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types'
import { useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
// material-ui
import { IconButton } from '@mui/material'
@@ -88,6 +88,10 @@ const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false }
setShowSpecificCredentialDialog(true)
}
useEffect(() => {
setCredentialId(data?.credential ?? '')
}, [data])
return (
<div ref={ref}>
{inputParam && (
+137 -12
View File
@@ -6,6 +6,7 @@ import { useSelector } from 'react-redux'
// material-ui
import { useTheme, styled } from '@mui/material/styles'
import { Box, Typography, Tooltip, IconButton, Button } from '@mui/material'
import IconAutoFixHigh from '@mui/icons-material/AutoFixHigh'
import { tooltipClasses } from '@mui/material/Tooltip'
import { IconArrowsMaximize, IconEdit, IconAlertTriangle } from '@tabler/icons'
@@ -21,9 +22,13 @@ import { flowContext } from '@/store/context/ReactFlowContext'
import { isValidConnection } from '@/utils/genericHelper'
import { JsonEditorInput } from '@/ui-component/json/JsonEditor'
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
import ToolDialog from '@/views/tools/ToolDialog'
import AssistantDialog from '@/views/assistants/AssistantDialog'
import FormatPromptValuesDialog from '@/ui-component/dialog/FormatPromptValuesDialog'
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
import PromptLangsmithHubDialog from '@/ui-component/dialog/PromptLangsmithHubDialog'
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
import CredentialInputHandler from './CredentialInputHandler'
// utils
@@ -56,20 +61,55 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString())
const [showFormatPromptValuesDialog, setShowFormatPromptValuesDialog] = useState(false)
const [formatPromptValuesDialogProps, setFormatPromptValuesDialogProps] = useState({})
const [showPromptHubDialog, setShowPromptHubDialog] = useState(false)
const [showManageScrapedLinksDialog, setShowManageScrapedLinksDialog] = useState(false)
const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({})
const onExpandDialogClicked = (value, inputParam) => {
const dialogProp = {
const dialogProps = {
value,
inputParam,
disabled,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}
setExpandDialogProps(dialogProp)
setExpandDialogProps(dialogProps)
setShowExpandDialog(true)
}
const onFormatPromptValuesClicked = (value, inputParam) => {
const onShowPromptHubButtonClicked = () => {
setShowPromptHubDialog(true)
}
const onShowPromptHubButtonSubmit = (templates) => {
setShowPromptHubDialog(false)
for (const t of templates) {
if (Object.prototype.hasOwnProperty.call(data.inputs, t.type)) {
data.inputs[t.type] = t.template
}
}
}
const onManageLinksDialogClicked = (url, selectedLinks, relativeLinksMethod, limit) => {
const dialogProps = {
url,
relativeLinksMethod,
limit,
selectedLinks,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}
setManageScrapedLinksDialogProps(dialogProps)
setShowManageScrapedLinksDialog(true)
}
const onManageLinksDialogSave = (url, links) => {
setShowManageScrapedLinksDialog(false)
data.inputs.url = url
data.inputs.selectedLinks = links
}
const onEditJSONClicked = (value, inputParam) => {
// Preset values if the field is format prompt values
let inputValue = value
if (inputParam.name === 'promptValues' && !value) {
@@ -209,6 +249,31 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
</CustomWidthTooltip>
)}
<Box sx={{ p: 2 }}>
{(data.name === 'promptTemplate' || data.name === 'chatPromptTemplate') &&
(inputParam.name === 'template' || inputParam.name === 'systemMessagePrompt') && (
<>
<Button
style={{
display: 'flex',
flexDirection: 'row',
width: '100%'
}}
disabled={disabled}
sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 0 }}
variant='outlined'
onClick={() => onShowPromptHubButtonClicked()}
endIcon={<IconAutoFixHigh />}
>
Langchain Hub
</Button>
<PromptLangsmithHubDialog
promptType={inputParam.name}
show={showPromptHubDialog}
onCancel={() => setShowPromptHubDialog(false)}
onSubmit={onShowPromptHubButtonSubmit}
></PromptLangsmithHubDialog>
</>
)}
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
{inputParam.label}
@@ -216,7 +281,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
{inputParam.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />}
</Typography>
<div style={{ flexGrow: 1 }}></div>
{inputParam.type === 'string' && inputParam.rows && (
{((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && (
<IconButton
size='small'
sx={{
@@ -238,6 +303,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
background: 'rgb(254,252,191)',
padding: 10,
@@ -245,7 +311,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
marginBottom: 10
}}
>
<IconAlertTriangle size={36} color='orange' />
<IconAlertTriangle size={30} color='orange' />
<span style={{ color: 'rgb(116,66,16)', marginLeft: 10 }}>{inputParam.warning}</span>
</div>
)}
@@ -260,6 +326,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
}}
/>
)}
{inputParam.type === 'file' && (
<File
disabled={disabled}
@@ -284,6 +351,23 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
/>
)}
{inputParam.type === 'code' && (
<>
<div style={{ height: '5px' }}></div>
<div style={{ height: inputParam.rows ? '100px' : '200px' }}>
<CodeEditor
disabled={disabled}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
height={inputParam.rows ? '100px' : '200px'}
theme={customization.isDarkMode ? 'dark' : 'light'}
lang={'js'}
placeholder={inputParam.placeholder}
onValueChange={(code) => (data.inputs[inputParam.name] = code)}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
</div>
</>
)}
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
<Input
key={data.inputs[inputParam.name]}
@@ -294,10 +378,6 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
nodes={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getNodes() : []}
edges={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getEdges() : []}
nodeId={data.id}
showDialog={showExpandDialog}
dialogProps={expandDialogProps}
onDialogCancel={() => setShowExpandDialog(false)}
onDialogConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
/>
)}
{inputParam.type === 'json' && (
@@ -313,11 +393,17 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
{inputParam?.acceptVariable && (
<>
<Button
sx={{ borderRadius: 25, width: '100%', mb: 2, mt: 2 }}
sx={{
borderRadius: 25,
width: '100%',
mb: 0,
mt: 2
}}
variant='outlined'
onClick={() => onFormatPromptValuesClicked(data.inputs[inputParam.name] ?? '', inputParam)}
disabled={disabled}
onClick={() => onEditJSONClicked(data.inputs[inputParam.name] ?? '', inputParam)}
>
Format Prompt Values
{inputParam.label}
</Button>
<FormatPromptValuesDialog
show={showFormatPromptValuesDialog}
@@ -373,6 +459,39 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
</div>
</>
)}
{(data.name === 'cheerioWebScraper' ||
data.name === 'puppeteerWebScraper' ||
data.name === 'playwrightWebScraper') &&
inputParam.name === 'url' && (
<>
<Button
style={{
display: 'flex',
flexDirection: 'row',
width: '100%'
}}
disabled={disabled}
sx={{ borderRadius: '12px', width: '100%', mt: 1 }}
variant='outlined'
onClick={() =>
onManageLinksDialogClicked(
data.inputs[inputParam.name] ?? inputParam.default ?? '',
data.inputs.selectedLinks,
data.inputs['relativeLinksMethod'] ?? 'webCrawl',
parseInt(data.inputs['limit']) ?? 0
)
}
>
Manage Links
</Button>
<ManageScrapedLinksDialog
show={showManageScrapedLinksDialog}
dialogProps={manageScrapedLinksDialogProps}
onCancel={() => setShowManageScrapedLinksDialog(false)}
onSave={onManageLinksDialogSave}
/>
</>
)}
</Box>
</>
)}
@@ -388,6 +507,12 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
onCancel={() => setAsyncOptionEditDialog('')}
onConfirm={onConfirmAsyncOption}
></AssistantDialog>
<ExpandTextDialog
show={showExpandDialog}
dialogProps={expandDialogProps}
onCancel={() => setShowExpandDialog(false)}
onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
></ExpandTextDialog>
</div>
)
}
@@ -73,42 +73,104 @@ const NodeOutputHandler = ({ outputAnchor, data, disabled = false }) => {
</Box>
</>
)}
{outputAnchor.type === 'options' && outputAnchor.options && outputAnchor.options.length > 0 && (
<>
<CustomWidthTooltip
placement='right'
title={
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ?? outputAnchor.type
}
>
<Handle
type='source'
position={Position.Right}
id={outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.id ?? ''}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position
}}
/>
</CustomWidthTooltip>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Dropdown
disabled={disabled}
disableClearable={true}
name={outputAnchor.name}
options={outputAnchor.options}
onSelect={(newValue) => {
setDropdownValue(newValue)
data.outputs[outputAnchor.name] = newValue
}}
value={data.outputs[outputAnchor.name] ?? outputAnchor.default ?? 'choose an option'}
/>
</Box>
</>
{data.name === 'ifElseFunction' && outputAnchor.type === 'options' && outputAnchor.options && (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<CustomWidthTooltip
placement='right'
title={
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ??
outputAnchor.type
}
>
<Handle
type='source'
position={Position.Right}
key={outputAnchor.options.find((opt) => opt.name === 'returnTrue')?.id ?? ''}
id={outputAnchor.options.find((opt) => opt.name === 'returnTrue')?.id ?? ''}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position - 25
}}
/>
</CustomWidthTooltip>
<div style={{ flex: 1 }}></div>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Typography>True</Typography>
</Box>
</div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<CustomWidthTooltip
placement='right'
title={
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ??
outputAnchor.type
}
>
<Handle
type='source'
position={Position.Right}
key={outputAnchor.options.find((opt) => opt.name === 'returnFalse')?.id ?? ''}
id={outputAnchor.options.find((opt) => opt.name === 'returnFalse')?.id ?? ''}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position + 25
}}
/>
</CustomWidthTooltip>
<div style={{ flex: 1 }}></div>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Typography>False</Typography>
</Box>
</div>
</div>
)}
{data.name !== 'ifElseFunction' &&
outputAnchor.type === 'options' &&
outputAnchor.options &&
outputAnchor.options.length > 0 && (
<>
<CustomWidthTooltip
placement='right'
title={
outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.type ??
outputAnchor.type
}
>
<Handle
type='source'
position={Position.Right}
id={outputAnchor.options.find((opt) => opt.name === data.outputs?.[outputAnchor.name])?.id ?? ''}
isValidConnection={(connection) => isValidConnection(connection, reactFlowInstance)}
style={{
height: 10,
width: 10,
backgroundColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
top: position
}}
/>
</CustomWidthTooltip>
<Box sx={{ p: 2, textAlign: 'end' }}>
<Dropdown
disabled={disabled}
disableClearable={true}
name={outputAnchor.name}
options={outputAnchor.options}
onSelect={(newValue) => {
setDropdownValue(newValue)
data.outputs[outputAnchor.name] = newValue
}}
value={data.outputs[outputAnchor.name] ?? outputAnchor.default ?? 'choose an option'}
/>
</Box>
</>
)}
</div>
)
}
+103
View File
@@ -0,0 +1,103 @@
import PropTypes from 'prop-types'
import { useContext, useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
// project imports
import NodeCardWrapper from '../../ui-component/cards/NodeCardWrapper'
import NodeTooltip from '../../ui-component/tooltip/NodeTooltip'
import { IconButton, Box } from '@mui/material'
import { IconCopy, IconTrash } from '@tabler/icons'
import { Input } from 'ui-component/input/Input'
// const
import { flowContext } from '../../store/context/ReactFlowContext'
const StickyNote = ({ data }) => {
const theme = useTheme()
const canvas = useSelector((state) => state.canvas)
const { deleteNode, duplicateNode } = useContext(flowContext)
const [inputParam] = data.inputParams
const [open, setOpen] = useState(false)
const handleClose = () => {
setOpen(false)
}
const handleOpen = () => {
setOpen(true)
}
return (
<>
<NodeCardWrapper
content={false}
sx={{
padding: 0,
borderColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary,
backgroundColor: data.selected ? '#FFDC00' : '#FFE770'
}}
border={false}
>
<NodeTooltip
open={!canvas.canvasDialogShow && open}
onClose={handleClose}
onOpen={handleOpen}
disableFocusListener={true}
title={
<div
style={{
background: 'transparent',
display: 'flex',
flexDirection: 'column'
}}
>
<IconButton
title='Duplicate'
onClick={() => {
duplicateNode(data.id)
}}
sx={{ height: '35px', width: '35px', '&:hover': { color: theme?.palette.primary.main } }}
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
>
<IconCopy />
</IconButton>
<IconButton
title='Delete'
onClick={() => {
deleteNode(data.id)
}}
sx={{ height: '35px', width: '35px', '&:hover': { color: 'red' } }}
color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'}
>
<IconTrash />
</IconButton>
</div>
}
placement='right-start'
>
<Box>
<Input
key={data.id}
inputParam={inputParam}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
nodes={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getNodes() : []}
edges={inputParam?.acceptVariable && reactFlowInstance ? reactFlowInstance.getEdges() : []}
nodeId={data.id}
/>
</Box>
</NodeTooltip>
</NodeCardWrapper>
</>
)
}
StickyNote.propTypes = {
data: PropTypes.object
}
export default StickyNote
+5 -5
View File
@@ -21,6 +21,7 @@ import { useTheme } from '@mui/material/styles'
// project imports
import CanvasNode from './CanvasNode'
import ButtonEdge from './ButtonEdge'
import StickyNote from './StickyNote'
import CanvasHeader from './CanvasHeader'
import AddNodes from './AddNodes'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
@@ -40,13 +41,13 @@ import useConfirm from '@/hooks/useConfirm'
import { IconX } from '@tabler/icons'
// utils
import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering, getUpsertDetails } from '@/utils/genericHelper'
import { getUniqueNodeId, initNode, rearrangeToolsOrdering, getUpsertDetails } from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier'
// const
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
const nodeTypes = { customNode: CanvasNode }
const nodeTypes = { customNode: CanvasNode, stickyNote: StickyNote }
const edgeTypes = { buttonedge: ButtonEdge }
// ==============================|| CANVAS ||============================== //
@@ -100,8 +101,7 @@ const Canvas = () => {
const newEdge = {
...params,
type: 'buttonedge',
id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}`,
data: { label: getEdgeLabelName(params.sourceHandle) }
id: `${params.source}-${params.sourceHandle}-${params.target}-${params.targetHandle}`
}
const targetNodeId = params.targetHandle.split('-')[0]
@@ -277,7 +277,7 @@ const Canvas = () => {
const newNode = {
id: newNodeId,
position,
type: 'customNode',
type: nodeData.type !== 'StickyNote' ? 'customNode' : 'stickyNote',
data: initNode(nodeData, newNodeId)
}
@@ -135,6 +135,8 @@ const ShareChatbot = ({ isSessionMemory }) => {
if (isSessionMemory) obj.overrideConfig.generateNewSession = generateNewSession
if (chatbotConfig?.starterPrompts) obj.starterPrompts = chatbotConfig.starterPrompts
return obj
}
+1 -1
View File
@@ -47,6 +47,7 @@ const Chatflows = () => {
const [view, setView] = React.useState(localStorage.getItem('flowDisplayStyle') || 'card')
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('flowDisplayStyle', nextView)
setView(nextView)
}
@@ -161,7 +162,6 @@ const Chatflows = () => {
variant='contained'
value='card'
title='Card View'
selectedColor='#00abc0'
>
<IconLayoutGrid />
</ToggleButton>
@@ -7,7 +7,7 @@ import { ChatMessage } from './ChatMessage'
import { StyledButton } from '@/ui-component/button/StyledButton'
import { IconEraser } from '@tabler/icons'
const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => {
const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel, previews, setPreviews }) => {
const portalElement = document.getElementById('portal')
const customization = useSelector((state) => state.customization)
@@ -21,7 +21,7 @@ const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => {
aria-describedby='alert-dialog-description'
sx={{ overflow: 'visible' }}
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<DialogTitle sx={{ fontSize: '1rem', p: 1.5 }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{dialogProps.title}
<div style={{ flex: 1 }}></div>
@@ -43,8 +43,17 @@ const ChatExpandDialog = ({ show, dialogProps, onClear, onCancel }) => {
)}
</div>
</DialogTitle>
<DialogContent sx={{ display: 'flex', justifyContent: 'flex-end', flexDirection: 'column' }}>
<ChatMessage isDialog={true} open={dialogProps.open} chatflowid={dialogProps.chatflowid} />
<DialogContent
className='cloud-dialog-wrapper'
sx={{ display: 'flex', justifyContent: 'flex-end', flexDirection: 'column', p: 0 }}
>
<ChatMessage
isDialog={true}
open={dialogProps.open}
chatflowid={dialogProps.chatflowid}
previews={previews}
setPreviews={setPreviews}
/>
</DialogContent>
</Dialog>
) : null
@@ -56,7 +65,9 @@ ChatExpandDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onClear: PropTypes.func,
onCancel: PropTypes.func
onCancel: PropTypes.func,
previews: PropTypes.array,
setPreviews: PropTypes.func
}
export default ChatExpandDialog
@@ -1,8 +1,6 @@
.messagelist {
width: 100%;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
height: auto;
border-radius: 0.5rem;
}
@@ -108,31 +106,57 @@
}
.center {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
flex-direction: column;
padding: 10px;
padding: 12px;
}
.cloud {
.cloud-wrapper {
width: 400px;
height: calc(100vh - 260px);
border-radius: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
height: calc(100vh - 180px);
}
.cloud-dialog-wrapper {
width: 100%;
height: calc(100vh - 120px);
}
.cloud-wrapper > div,
.cloud-dialog-wrapper > div {
width: 100%;
height: 100%;
display: flex;
align-items: flex-start;
justify-content: flex-start;
flex-direction: column;
position: relative;
}
.image-dropzone {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 2001; /* Ensure it's above other content */
}
.cloud,
.cloud-dialog {
width: 100%;
height: 100vh;
height: auto;
max-height: calc(100% - 54px);
overflow-y: scroll;
border-radius: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
align-items: flex-start;
flex-grow: 1;
}
.cloud-message {
@@ -144,3 +168,38 @@
justify-content: center;
align-items: center;
}
.preview {
position: absolute;
bottom: 0;
z-index: 1000;
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */
scrollbar-width: none; /* For Firefox */
}
.file-drop-field {
position: relative; /* Needed to position the icon correctly */
/* Other styling for the field */
}
.drop-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(137, 134, 134, 0.83); /* Semi-transparent white */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 2000; /* Ensure it's above other content */
border: 2px dashed #0094ff; /* Example style */
}
.center audio {
height: 100%;
border-radius: 0;
}
+695 -151
View File
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { useState, useRef, useEffect, useCallback, Fragment } from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import socketIOClient from 'socket.io-client'
@@ -9,15 +9,34 @@ import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import axios from 'axios'
import { CircularProgress, OutlinedInput, Divider, InputAdornment, IconButton, Box, Chip, Button } from '@mui/material'
import {
Box,
Button,
Card,
CardMedia,
Chip,
CircularProgress,
Divider,
IconButton,
InputAdornment,
OutlinedInput,
Typography
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconSend, IconDownload } from '@tabler/icons'
import { IconCircleDot, IconDownload, IconSend, IconMicrophone, IconPhotoPlus, IconTrash, IconX } from '@tabler/icons'
import robotPNG from '@/assets/images/robot.png'
import userPNG from '@/assets/images/account.png'
import audioUploadSVG from '@/assets/images/wave-sound.jpg'
// project import
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'
import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'
import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'
import './ChatMessage.css'
import './audio-recording.css'
// api
import chatmessageApi from '@/api/chatmessage'
@@ -30,11 +49,16 @@ import useApi from '@/hooks/useApi'
// Const
import { baseURL, maxScroll } from '@/store/constant'
import robotPNG from '@/assets/images/robot.png'
import userPNG from '@/assets/images/account.png'
// Utils
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from '@/utils/genericHelper'
export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const messageImageStyle = {
width: '128px',
height: '128px',
objectFit: 'cover'
}
export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
@@ -50,6 +74,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
])
const [socketIOClientId, setSocketIOClientId] = useState('')
const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false)
const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false)
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
const [sourceDialogProps, setSourceDialogProps] = useState({})
const [chatId, setChatId] = useState(undefined)
@@ -57,6 +82,219 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const inputRef = useRef(null)
const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow)
const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming)
const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads)
const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow)
const [starterPrompts, setStarterPrompts] = useState([])
// drag & drop and file input
const fileUploadRef = useRef(null)
const [isChatFlowAvailableForUploads, setIsChatFlowAvailableForUploads] = useState(false)
const [isDragActive, setIsDragActive] = useState(false)
// recording
const [isRecording, setIsRecording] = useState(false)
const [recordingNotSupported, setRecordingNotSupported] = useState(false)
const [isLoadingRecording, setIsLoadingRecording] = useState(false)
const isFileAllowedForUpload = (file) => {
const constraints = getAllowChatFlowUploads.data
/**
* {isImageUploadAllowed: boolean, imgUploadSizeAndTypes: Array<{ fileTypes: string[], maxUploadSize: number }>}
*/
let acceptFile = false
if (constraints.isImageUploadAllowed) {
const fileType = file.type
const sizeInMB = file.size / 1024 / 1024
constraints.imgUploadSizeAndTypes.map((allowed) => {
if (allowed.fileTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) {
acceptFile = true
}
})
}
if (!acceptFile) {
alert(`Cannot upload file. Kindly check the allowed file types and maximum allowed size.`)
}
return acceptFile
}
const handleDrop = async (e) => {
if (!isChatFlowAvailableForUploads) {
return
}
e.preventDefault()
setIsDragActive(false)
let files = []
if (e.dataTransfer.files.length > 0) {
for (const file of e.dataTransfer.files) {
if (isFileAllowedForUpload(file) === false) {
return
}
const reader = new FileReader()
const { name } = file
files.push(
new Promise((resolve) => {
reader.onload = (evt) => {
if (!evt?.target?.result) {
return
}
const { result } = evt.target
let previewUrl
if (file.type.startsWith('audio/')) {
previewUrl = audioUploadSVG
} else if (file.type.startsWith('image/')) {
previewUrl = URL.createObjectURL(file)
}
resolve({
data: result,
preview: previewUrl,
type: 'file',
name: name,
mime: file.type
})
}
reader.readAsDataURL(file)
})
)
}
const newFiles = await Promise.all(files)
setPreviews((prevPreviews) => [...prevPreviews, ...newFiles])
}
if (e.dataTransfer.items) {
for (const item of e.dataTransfer.items) {
if (item.kind === 'string' && item.type.match('^text/uri-list')) {
item.getAsString((s) => {
let upload = {
data: s,
preview: s,
type: 'url',
name: s.substring(s.lastIndexOf('/') + 1)
}
setPreviews((prevPreviews) => [...prevPreviews, upload])
})
} else if (item.kind === 'string' && item.type.match('^text/html')) {
item.getAsString((s) => {
if (s.indexOf('href') === -1) return
//extract href
let start = s.substring(s.indexOf('href') + 6)
let hrefStr = start.substring(0, start.indexOf('"'))
let upload = {
data: hrefStr,
preview: hrefStr,
type: 'url',
name: hrefStr.substring(hrefStr.lastIndexOf('/') + 1)
}
setPreviews((prevPreviews) => [...prevPreviews, upload])
})
}
}
}
}
const handleFileChange = async (event) => {
const fileObj = event.target.files && event.target.files[0]
if (!fileObj) {
return
}
let files = []
for (const file of event.target.files) {
if (isFileAllowedForUpload(file) === false) {
return
}
const reader = new FileReader()
const { name } = file
files.push(
new Promise((resolve) => {
reader.onload = (evt) => {
if (!evt?.target?.result) {
return
}
const { result } = evt.target
resolve({
data: result,
preview: URL.createObjectURL(file),
type: 'file',
name: name,
mime: file.type
})
}
reader.readAsDataURL(file)
})
)
}
const newFiles = await Promise.all(files)
setPreviews((prevPreviews) => [...prevPreviews, ...newFiles])
// 👇 reset file input
event.target.value = null
}
const addRecordingToPreviews = (blob) => {
const mimeType = blob.type.substring(0, blob.type.indexOf(';'))
// read blob and add to previews
const reader = new FileReader()
reader.readAsDataURL(blob)
reader.onloadend = () => {
const base64data = reader.result
const upload = {
data: base64data,
preview: audioUploadSVG,
type: 'audio',
name: 'audio.wav',
mime: mimeType
}
setPreviews((prevPreviews) => [...prevPreviews, upload])
}
}
const handleDrag = (e) => {
if (isChatFlowAvailableForUploads) {
e.preventDefault()
e.stopPropagation()
if (e.type === 'dragenter' || e.type === 'dragover') {
setIsDragActive(true)
} else if (e.type === 'dragleave') {
setIsDragActive(false)
}
}
}
const handleDeletePreview = (itemToDelete) => {
if (itemToDelete.type === 'file') {
URL.revokeObjectURL(itemToDelete.preview) // Clean up for file
}
setPreviews(previews.filter((item) => item !== itemToDelete))
}
const handleUploadClick = () => {
// 👇 open file input box on click of another element
fileUploadRef.current.click()
}
const clearPreviews = () => {
// Revoke the data uris to avoid memory leaks
previews.forEach((file) => URL.revokeObjectURL(file.preview))
setPreviews([])
}
const onMicrophonePressed = () => {
setIsRecording(true)
startAudioRecording(setIsRecording, setRecordingNotSupported)
}
const onRecordingCancelled = () => {
if (!recordingNotSupported) cancelAudioRecording()
setIsRecording(false)
setRecordingNotSupported(false)
}
const onRecordingStopped = async () => {
setIsLoadingRecording(true)
stopAudioRecording(addRecordingToPreviews)
}
const onSourceDialogClick = (data, title) => {
setSourceDialogProps({ data, title })
@@ -104,24 +342,46 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
}, 100)
}
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault()
const handlePromptClick = async (promptStarterInput) => {
setUserInput(promptStarterInput)
handleSubmit(undefined, promptStarterInput)
}
if (userInput.trim() === '') {
return
// Handle form submission
const handleSubmit = async (e, promptStarterInput) => {
if (e) e.preventDefault()
if (!promptStarterInput && userInput.trim() === '') {
const containsAudio = previews.filter((item) => item.type === 'audio').length > 0
if (!(previews.length >= 1 && containsAudio)) {
return
}
}
let input = userInput
if (promptStarterInput !== undefined && promptStarterInput.trim() !== '') input = promptStarterInput
setLoading(true)
setMessages((prevMessages) => [...prevMessages, { message: userInput, type: 'userMessage' }])
const urls = previews.map((item) => {
return {
data: item.data,
type: item.type,
name: item.name,
mime: item.mime
}
})
clearPreviews()
setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: urls }])
// Send user question and history to API
try {
const params = {
question: userInput,
question: input,
history: messages.filter((msg) => msg.message !== 'Hi there! How can I help?'),
chatId
}
if (urls && urls.length > 0) params.uploads = urls
if (isChatFlowAvailableToStream) params.socketIOClientId = socketIOClientId
const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params)
@@ -131,6 +391,17 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (!chatId) setChatId(data.chatId)
if (input === '' && data.question) {
// the response contains the question even if it was in an audio format
// so if input is empty but the response contains the question, update the user message to show the question
setMessages((prevMessages) => {
let allMessages = [...cloneDeep(prevMessages)]
if (allMessages[allMessages.length - 2].type === 'apiMessage') return allMessages
allMessages[allMessages.length - 2].message = data.question
return allMessages
})
}
if (!isChatFlowAvailableToStream) {
let text = ''
if (data.text) text = data.text
@@ -209,6 +480,14 @@ 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)
if (message.fileUploads) {
obj.fileUploads = JSON.parse(message.fileUploads)
obj.fileUploads.forEach((file) => {
if (file.type === 'stored-file') {
file.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${file.name}`
}
})
}
return obj
})
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
@@ -223,10 +502,36 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (getIsChatflowStreamingApi.data) {
setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getIsChatflowStreamingApi.data])
// Get chatflow uploads capability
useEffect(() => {
if (getAllowChatFlowUploads.data) {
setIsChatFlowAvailableForUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false)
setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllowChatFlowUploads.data])
useEffect(() => {
if (getChatflowConfig.data) {
if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) {
let config = JSON.parse(getChatflowConfig.data?.chatbotConfig)
if (config.starterPrompts) {
let inputFields = []
Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => {
if (config.starterPrompts[key]) {
inputFields.push(config.starterPrompts[key])
}
})
setStarterPrompts(inputFields)
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChatflowConfig.data])
// Auto scroll chat to bottom
useEffect(() => {
scrollToBottom()
@@ -243,10 +548,18 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
useEffect(() => {
let socket
if (open && chatflowid) {
// API request
getChatmessageApi.request(chatflowid)
getIsChatflowStreamingApi.request(chatflowid)
getAllowChatFlowUploads.request(chatflowid)
getChatflowConfig.request(chatflowid)
// Scroll to bottom
scrollToBottom()
setIsRecording(false)
// SocketIO
socket = socketIOClient(baseURL)
socket.on('connect', () => {
@@ -280,141 +593,330 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, chatflowid])
useEffect(() => {
// wait for audio recording to load and then send
const containsAudio = previews.filter((item) => item.type === 'audio').length > 0
if (previews.length >= 1 && containsAudio) {
setIsRecording(false)
setRecordingNotSupported(false)
handlePromptClick('')
}
// eslint-disable-next-line
}, [previews])
return (
<>
<div className={isDialog ? 'cloud-dialog' : 'cloud'}>
<div ref={ps} className='messagelist'>
<div onDragEnter={handleDrag}>
{isDragActive && (
<div
className='image-dropzone'
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragEnd={handleDrag}
onDrop={handleDrop}
/>
)}
{isDragActive && getAllowChatFlowUploads.data?.isImageUploadAllowed && (
<Box className='drop-overlay'>
<Typography variant='h2'>Drop here to upload</Typography>
{getAllowChatFlowUploads.data.imgUploadSizeAndTypes.map((allowed) => {
return (
<>
<Typography variant='subtitle1'>{allowed.fileTypes?.join(', ')}</Typography>
<Typography variant='subtitle1'>Max Allowed Size: {allowed.maxUploadSize} MB</Typography>
</>
)
})}
</Box>
)}
<div ref={ps} className={`${isDialog ? 'cloud-dialog' : 'cloud'}`}>
<div id='messagelist' className={'messagelist'}>
{messages &&
messages.map((message, index) => {
return (
// The latest message sent by the user will be animated while waiting for a response
<>
<Box
sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
}}
key={index}
style={{ display: 'flex' }}
className={
message.type === 'userMessage' && loading && index === messages.length - 1
? customization.isDarkMode
? 'usermessagewaiting-dark'
: 'usermessagewaiting-light'
: message.type === 'usermessagewaiting'
? 'apimessage'
: 'usermessage'
}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img src={robotPNG} alt='AI' width='30' height='30' className='boticon' />
) : (
<img src={userPNG} alt='Me' width='30' height='30' className='usericon' />
)}
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
{message.usedTools && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{message.usedTools.map((tool, index) => {
return (
<Chip
size='small'
key={index}
label={tool.tool}
component='a'
sx={{ mr: 1, mt: 1 }}
variant='outlined'
clickable
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
/>
)
})}
</div>
)}
<div className='markdownanswer'>
{/* Messages are being rendered in Markdown format */}
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax, rehypeRaw]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
chatflowid={chatflowid}
isDialog={isDialog}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{message.message}
</MemoizedReactMarkdown>
<Box
sx={{
background: message.type === 'apiMessage' ? theme.palette.asyncSelect.main : ''
}}
key={index}
style={{ display: 'flex' }}
className={
message.type === 'userMessage' && loading && index === messages.length - 1
? customization.isDarkMode
? 'usermessagewaiting-dark'
: 'usermessagewaiting-light'
: message.type === 'usermessagewaiting'
? 'apimessage'
: 'usermessage'
}
>
{/* Display the correct icon depending on the message type */}
{message.type === 'apiMessage' ? (
<img src={robotPNG} alt='AI' width='30' height='30' className='boticon' />
) : (
<img src={userPNG} alt='Me' width='30' height='30' className='usericon' />
)}
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
{message.usedTools && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{message.usedTools.map((tool, index) => {
return (
<Chip
size='small'
key={index}
label={tool.tool}
component='a'
sx={{ mr: 1, mt: 1 }}
variant='outlined'
clickable
onClick={() => onSourceDialogClick(tool, 'Used Tools')}
/>
)
})}
</div>
{message.fileAnnotations && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{message.fileAnnotations.map((fileAnnotation, index) => {
return (
<Button
sx={{ fontSize: '0.85rem', textTransform: 'none', mb: 1 }}
key={index}
variant='outlined'
onClick={() => downloadFile(fileAnnotation)}
endIcon={<IconDownload color={theme.palette.primary.main} />}
>
{fileAnnotation.fileName}
</Button>
)
})}
</div>
)}
{message.sourceDocuments && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{removeDuplicateURL(message).map((source, index) => {
const URL =
source.metadata && source.metadata.source
? isValidURL(source.metadata.source)
: undefined
return (
<Chip
size='small'
key={index}
label={
URL
? URL.pathname.substring(0, 15) === '/'
? URL.host
: `${URL.pathname.substring(0, 15)}...`
: `${source.pageContent.substring(0, 15)}...`
}
component='a'
sx={{ mr: 1, mb: 1 }}
variant='outlined'
clickable
onClick={() =>
URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source)
}
)}
{message.fileUploads && message.fileUploads.length > 0 && (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'column',
width: '100%',
gap: '8px'
}}
>
{message.fileUploads.map((item, index) => {
return (
<>
{item.mime.startsWith('image/') ? (
<Card
key={index}
sx={{
p: 0,
m: 0,
maxWidth: 128,
marginRight: '10px',
flex: '0 0 auto'
}}
>
<CardMedia
component='img'
image={item.data}
sx={{ height: 64 }}
alt={'preview'}
style={messageImageStyle}
/>
</Card>
) : (
// eslint-disable-next-line jsx-a11y/media-has-caption
<audio controls='controls'>
Your browser does not support the &lt;audio&gt; tag.
<source src={item.data} type={item.mime} />
</audio>
)}
</>
)
})}
</div>
)}
<div className='markdownanswer'>
{/* Messages are being rendered in Markdown format */}
<MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax, rehypeRaw]}
components={{
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
chatflowid={chatflowid}
isDialog={isDialog}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
})}
</div>
)}
}
}}
>
{message.message}
</MemoizedReactMarkdown>
</div>
</Box>
</>
{message.fileAnnotations && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{message.fileAnnotations.map((fileAnnotation, index) => {
return (
<Button
sx={{ fontSize: '0.85rem', textTransform: 'none', mb: 1 }}
key={index}
variant='outlined'
onClick={() => downloadFile(fileAnnotation)}
endIcon={<IconDownload color={theme.palette.primary.main} />}
>
{fileAnnotation.fileName}
</Button>
)
})}
</div>
)}
{message.sourceDocuments && (
<div style={{ display: 'block', flexDirection: 'row', width: '100%' }}>
{removeDuplicateURL(message).map((source, index) => {
const URL =
source.metadata && source.metadata.source
? isValidURL(source.metadata.source)
: undefined
return (
<Chip
size='small'
key={index}
label={
URL
? URL.pathname.substring(0, 15) === '/'
? URL.host
: `${URL.pathname.substring(0, 15)}...`
: `${source.pageContent.substring(0, 15)}...`
}
component='a'
sx={{ mr: 1, mb: 1 }}
variant='outlined'
clickable
onClick={() =>
URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source)
}
/>
)
})}
</div>
)}
</div>
</Box>
)
})}
</div>
</div>
<Divider />
{messages && messages.length === 1 && starterPrompts.length > 0 && (
<div style={{ position: 'relative' }}>
<StarterPromptsCard
sx={{ bottom: previews && previews.length > 0 ? 70 : 0 }}
starterPrompts={starterPrompts || []}
onPromptClick={handlePromptClick}
isGrid={isDialog}
/>
</div>
)}
<Divider sx={{ width: '100%' }} />
<div className='center'>
<div style={{ width: '100%' }}>
{previews && previews.length > 0 && (
<Box sx={{ width: '100%', mb: 1.5, display: 'flex', alignItems: 'center' }}>
{previews.map((item, index) => (
<Fragment key={index}>
{item.mime.startsWith('image/') ? (
<ImageButton
focusRipple
style={{
width: '48px',
height: '48px',
marginRight: '10px',
flex: '0 0 auto'
}}
onClick={() => handleDeletePreview(item)}
>
<ImageSrc style={{ backgroundImage: `url(${item.data})` }} />
<ImageBackdrop className='MuiImageBackdrop-root' />
<ImageMarked className='MuiImageMarked-root'>
<IconTrash size={20} color='white' />
</ImageMarked>
</ImageButton>
) : (
<Card
sx={{
display: 'inline-flex',
alignItems: 'center',
height: '48px',
width: isDialog ? ps?.current?.offsetWidth / 4 : ps?.current?.offsetWidth / 2,
p: 0.5,
mr: 1,
backgroundColor: theme.palette.grey[500],
flex: '0 0 auto'
}}
variant='outlined'
>
<CardMedia component='audio' sx={{ color: 'transparent' }} controls src={item.data} />
<IconButton onClick={() => handleDeletePreview(item)} size='small'>
<IconTrash size={20} color='white' />
</IconButton>
</Card>
)}
</Fragment>
))}
</Box>
)}
{isRecording ? (
<>
{recordingNotSupported ? (
<div className='overlay'>
<div className='browser-not-supporting-audio-recording-box'>
<Typography variant='body1'>
To record audio, use modern browsers like Chrome or Firefox that support audio recording.
</Typography>
<Button
variant='contained'
color='error'
size='small'
type='button'
onClick={() => onRecordingCancelled()}
>
Okay
</Button>
</div>
</div>
) : (
<Box
sx={{
width: '100%',
height: '54px',
px: 2,
border: '1px solid',
borderRadius: 3,
backgroundColor: customization.isDarkMode ? '#32353b' : '#fafafa',
borderColor: 'rgba(0, 0, 0, 0.23)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<div className='recording-elapsed-time'>
<span className='red-recording-dot'>
<IconCircleDot />
</span>
<Typography id='elapsed-time'>00:00</Typography>
{isLoadingRecording && <Typography ml={1.5}>Sending...</Typography>}
</div>
<div className='recording-control-buttons-container'>
<IconButton onClick={onRecordingCancelled} size='small'>
<IconX
color={loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
<IconButton onClick={onRecordingStopped} size='small'>
<IconSend
color={loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
</IconButton>
</div>
</Box>
)}
</>
) : (
<form style={{ width: '100%' }} onSubmit={handleSubmit}>
<OutlinedInput
inputRef={inputRef}
@@ -430,33 +932,75 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
onChange={onChange}
multiline={true}
maxRows={isDialog ? 7 : 2}
endAdornment={
<InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
{loading ? (
<div>
<CircularProgress color='inherit' size={20} />
</div>
) : (
// Send icon SVG in input field
<IconSend
startAdornment={
isChatFlowAvailableForUploads && (
<InputAdornment position='start' sx={{ pl: 2 }}>
<IconButton
onClick={handleUploadClick}
type='button'
disabled={loading || !chatflowid}
edge='start'
>
<IconPhotoPlus
color={loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'}
/>
)}
</IconButton>
</InputAdornment>
</IconButton>
</InputAdornment>
)
}
endAdornment={
<>
{isChatFlowAvailableForSpeech && (
<InputAdornment position='end'>
<IconButton
onClick={() => onMicrophonePressed()}
type='button'
disabled={loading || !chatflowid}
edge='end'
>
<IconMicrophone
className={'start-recording-button'}
color={
loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'
}
/>
</IconButton>
</InputAdornment>
)}
<InputAdornment position='end' sx={{ padding: '15px' }}>
<IconButton type='submit' disabled={loading || !chatflowid} edge='end'>
{loading ? (
<div>
<CircularProgress color='inherit' size={20} />
</div>
) : (
// Send icon SVG in input field
<IconSend
color={
loading || !chatflowid ? '#9e9e9e' : customization.isDarkMode ? 'white' : '#1e88e5'
}
/>
)}
</IconButton>
</InputAdornment>
</>
}
/>
{isChatFlowAvailableForUploads && (
<input style={{ display: 'none' }} multiple ref={fileUploadRef} type='file' onChange={handleFileChange} />
)}
</form>
</div>
)}
</div>
<SourceDocDialog show={sourceDialogOpen} dialogProps={sourceDialogProps} onCancel={() => setSourceDialogOpen(false)} />
</>
</div>
)
}
ChatMessage.propTypes = {
open: PropTypes.bool,
chatflowid: PropTypes.string,
isDialog: PropTypes.bool
isDialog: PropTypes.bool,
previews: PropTypes.array,
setPreviews: PropTypes.func
}
@@ -35,6 +35,7 @@ export const ChatPopUp = ({ chatflowid }) => {
const [open, setOpen] = useState(false)
const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({})
const [previews, setPreviews] = useState([])
const anchorRef = useRef(null)
const prevOpen = useRef(open)
@@ -191,8 +192,15 @@ export const ChatPopUp = ({ chatflowid }) => {
<Transitions in={open} {...TransitionProps}>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
<ChatMessage chatflowid={chatflowid} open={open} />
<MainCard
border={false}
className='cloud-wrapper'
elevation={16}
content={false}
boxShadow
shadow={theme.shadows[16]}
>
<ChatMessage chatflowid={chatflowid} open={open} previews={previews} setPreviews={setPreviews} />
</MainCard>
</ClickAwayListener>
</Paper>
@@ -204,6 +212,8 @@ export const ChatPopUp = ({ chatflowid }) => {
dialogProps={expandDialogProps}
onClear={clearChat}
onCancel={() => setShowExpandDialog(false)}
previews={previews}
setPreviews={setPreviews}
></ChatExpandDialog>
</>
)
@@ -0,0 +1,249 @@
/* style.css*/
/* Media Queries */
/* Small Devices*/
@media (min-width: 0px) {
* {
box-sizing: border-box;
}
.start-recording-button {
font-size: 70px;
color: #435f7a;
cursor: pointer;
}
.start-recording-button:hover {
opacity: 1;
}
.recording-control-buttons-container {
/*targeting Chrome & Safari*/
display: -webkit-flex;
/*targeting IE10*/
display: -ms-flex;
display: flex;
justify-content: center;
/*horizontal centering*/
align-items: center;
gap: 12px;
}
.recording-elapsed-time {
font-size: 16px;
/*targeting Chrome & Safari*/
display: -webkit-flex;
/*targeting IE10*/
display: -ms-flex;
display: flex;
justify-content: center;
/*horizontal centering*/
align-items: center;
}
.recording-elapsed-time #elapsed-time {
margin: 0;
}
.recording-indicator-wrapper {
position: relative;
display: flex;
width: 16px;
height: 16px;
}
.red-recording-dot {
font-size: 25px;
color: red;
margin-right: 12px;
/*transitions with Firefox, IE and Opera Support browser support*/
animation-name: flashing-recording-dot;
-webkit-animation-name: flashing-recording-dot;
-moz-animation-name: flashing-recording-dot;
-o-animation-name: flashing-recording-dot;
animation-duration: 2s;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-o-animation-duration: 2s;
animation-iteration-count: infinite;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-o-animation-iteration-count: infinite;
}
/* The animation code */
@keyframes flashing-recording-dot {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-webkit-keyframes flashing-recording-dot {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-moz-keyframes flashing-recording-dot {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-o-keyframes flashing-recording-dot {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.recording-control-buttons-container.hide {
display: none;
}
.overlay {
width: 100%;
height: '54px';
/*targeting Chrome & Safari*/
display: -webkit-flex;
/*targeting IE10*/
display: -ms-flex;
display: flex;
justify-content: center;
/*horizontal centering*/
align-items: center;
}
.overlay.hide {
display: none;
}
.browser-not-supporting-audio-recording-box {
/*targeting Chrome & Safari*/
display: -webkit-flex;
/*targeting IE10*/
display: -ms-flex;
display: flex;
justify-content: space-between;
/*horizontal centering*/
align-items: center;
width: 100%;
font-size: 16px;
gap: 12px;
}
.browser-not-supporting-audio-recording-box > p {
margin: 0;
}
.close-browser-not-supported-box {
cursor: pointer;
background-color: #abc1c05c;
border-radius: 10px;
font-size: 16px;
border: none;
}
.close-browser-not-supported-box:hover {
background-color: #92a5a45c;
}
.close-browser-not-supported-box:focus {
outline: none;
border: none;
}
.audio-element.hide {
display: none;
}
.text-indication-of-audio-playing-container {
height: 20px;
}
.text-indication-of-audio-playing {
font-size: 20px;
}
.text-indication-of-audio-playing.hide {
display: none;
}
/* 3 Dots animation*/
.text-indication-of-audio-playing span {
/*transitions with Firefox, IE and Opera Support browser support*/
animation-name: blinking-dot;
-webkit-animation-name: blinking-dot;
-moz-animation-name: blinking-dot;
-o-animation-name: blinking-dot;
animation-duration: 2s;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-o-animation-duration: 2s;
animation-iteration-count: infinite;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-o-animation-iteration-count: infinite;
}
.text-indication-of-audio-playing span:nth-child(2) {
animation-delay: 0.4s;
-webkit-animation-delay: 0.4s;
-moz-animation-delay: 0.4s;
-o-animation-delay: 0.4s;
}
.text-indication-of-audio-playing span:nth-child(3) {
animation-delay: 0.8s;
-webkit-animation-delay: 0.8s;
-moz-animation-delay: 0.8s;
-o-animation-delay: 0.8s;
}
/* The animation code */
@keyframes blinking-dot {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* The animation code */
@-webkit-keyframes blinking-dot {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* The animation code */
@-moz-keyframes blinking-dot {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* The animation code */
@-o-keyframes blinking-dot {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
@@ -0,0 +1,337 @@
/**
* @fileoverview This file contains the API to handle audio recording.
* Originally from 'https://ralzohairi.medium.com/audio-recording-in-javascript-96eed45b75ee'
*/
// audio-recording.js ---------------
let microphoneButton, elapsedTimeTag
/** Initialize controls */
function initializeControls() {
microphoneButton = document.getElementsByClassName('start-recording-button')[0]
}
/** Displays recording control buttons */
function handleDisplayingRecordingControlButtons() {
//Hide the microphone button that starts audio recording
microphoneButton.style.display = 'none'
//Handle the displaying of the elapsed recording time
handleElapsedRecordingTime()
}
/** Hide the displayed recording control buttons */
function handleHidingRecordingControlButtons() {
//Display the microphone button that starts audio recording
microphoneButton.style.display = 'block'
//stop interval that handles both time elapsed and the red dot
clearInterval(elapsedTimeTimer)
}
/** Stores the actual start time when an audio recording begins to take place to ensure elapsed time start time is accurate*/
let audioRecordStartTime
/** Stores the maximum recording time in hours to stop recording once maximum recording hour has been reached */
let maximumRecordingTimeInHours = 1
/** Stores the reference of the setInterval function that controls the timer in audio recording*/
let elapsedTimeTimer
/** Starts the audio recording*/
export function startAudioRecording(onRecordingStart, onUnsupportedBrowser) {
initializeControls()
//start recording using the audio recording API
audioRecorder
.start()
.then(() => {
//on success show the controls to stop and cancel the recording
if (onRecordingStart) {
onRecordingStart(true)
}
//store the recording start time to display the elapsed time according to it
audioRecordStartTime = new Date()
//display control buttons to offer the functionality of stop and cancel
handleDisplayingRecordingControlButtons()
})
.catch((error) => {
//on error
//No Browser Support Error
if (error.message.includes('mediaDevices API or getUserMedia method is not supported in this browser.')) {
if (onUnsupportedBrowser) {
onUnsupportedBrowser(true)
}
}
//Error handling structure
switch (error.name) {
case 'AbortError': //error from navigator.mediaDevices.getUserMedia
// eslint-disable-next-line no-console
console.log('An AbortError has occurred.')
break
case 'NotAllowedError': //error from navigator.mediaDevices.getUserMedia
// eslint-disable-next-line no-console
console.log('A NotAllowedError has occurred. User might have denied permission.')
break
case 'NotFoundError': //error from navigator.mediaDevices.getUserMedia
// eslint-disable-next-line no-console
console.log('A NotFoundError has occurred.')
break
case 'NotReadableError': //error from navigator.mediaDevices.getUserMedia
// eslint-disable-next-line no-console
console.log('A NotReadableError has occurred.')
break
case 'SecurityError': //error from navigator.mediaDevices.getUserMedia or from the MediaRecorder.start
// eslint-disable-next-line no-console
console.log('A SecurityError has occurred.')
break
case 'TypeError': //error from navigator.mediaDevices.getUserMedia
// eslint-disable-next-line no-console
console.log('A TypeError has occurred.')
break
case 'InvalidStateError': //error from the MediaRecorder.start
// eslint-disable-next-line no-console
console.log('An InvalidStateError has occurred.')
break
case 'UnknownError': //error from the MediaRecorder.start
// eslint-disable-next-line no-console
console.log('An UnknownError has occurred.')
break
default:
// eslint-disable-next-line no-console
console.log('An error occurred with the error name ' + error.name)
}
})
}
/** Stop the currently started audio recording & sends it
*/
export function stopAudioRecording(addRecordingToPreviews) {
//stop the recording using the audio recording API
audioRecorder
.stop()
.then((audioBlob) => {
//hide recording control button & return record icon
handleHidingRecordingControlButtons()
if (addRecordingToPreviews) {
addRecordingToPreviews(audioBlob)
}
})
.catch((error) => {
//Error handling structure
switch (error.name) {
case 'InvalidStateError': //error from the MediaRecorder.stop
// eslint-disable-next-line no-console
console.log('An InvalidStateError has occurred.')
break
default:
// eslint-disable-next-line no-console
console.log('An error occurred with the error name ' + error.name)
}
})
}
/** Cancel the currently started audio recording */
export function cancelAudioRecording() {
//cancel the recording using the audio recording API
audioRecorder.cancel()
//hide recording control button & return record icon
handleHidingRecordingControlButtons()
}
/** Computes the elapsed recording time since the moment the function is called in the format h:m:s*/
function handleElapsedRecordingTime() {
elapsedTimeTag = document.getElementById('elapsed-time')
//display initial time when recording begins
displayElapsedTimeDuringAudioRecording('00:00')
//create an interval that compute & displays elapsed time, as well as, animate red dot - every second
elapsedTimeTimer = setInterval(() => {
//compute the elapsed time every second
let elapsedTime = computeElapsedTime(audioRecordStartTime) //pass the actual record start time
//display the elapsed time
displayElapsedTimeDuringAudioRecording(elapsedTime)
}, 1000) //every second
}
/** Display elapsed time during audio recording
* @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss
*/
function displayElapsedTimeDuringAudioRecording(elapsedTime) {
//1. display the passed elapsed time as the elapsed time in the elapsedTime HTML element
elapsedTimeTag.innerHTML = elapsedTime
//2. Stop the recording when the max number of hours is reached
if (elapsedTimeReachedMaximumNumberOfHours(elapsedTime)) {
stopAudioRecording()
}
}
/**
* @param {String} elapsedTime - elapsed time in the format mm:ss or hh:mm:ss
* @returns {Boolean} whether the elapsed time reached the maximum number of hours or not
*/
function elapsedTimeReachedMaximumNumberOfHours(elapsedTime) {
//Split the elapsed time by the symbol that separates the hours, minutes and seconds :
let elapsedTimeSplit = elapsedTime.split(':')
//Turn the maximum recording time in hours to a string and pad it with zero if less than 10
let maximumRecordingTimeInHoursAsString =
maximumRecordingTimeInHours < 10 ? '0' + maximumRecordingTimeInHours : maximumRecordingTimeInHours.toString()
//if the elapsed time reach hours and also reach the maximum recording time in hours return true
return elapsedTimeSplit.length === 3 && elapsedTimeSplit[0] === maximumRecordingTimeInHoursAsString
}
/** Computes the elapsedTime since the moment the function is called in the format mm:ss or hh:mm:ss
* @param {String} startTime - start time to compute the elapsed time since
* @returns {String} elapsed time in mm:ss format or hh:mm:ss format, if elapsed hours are 0.
*/
function computeElapsedTime(startTime) {
//record end time
let endTime = new Date()
//time difference in ms
let timeDiff = endTime - startTime
//convert time difference from ms to seconds
timeDiff = timeDiff / 1000
//extract integer seconds that don't form a minute using %
let seconds = Math.floor(timeDiff % 60) //ignoring incomplete seconds (floor)
//pad seconds with a zero if necessary
seconds = seconds < 10 ? '0' + seconds : seconds
//convert time difference from seconds to minutes using %
timeDiff = Math.floor(timeDiff / 60)
//extract integer minutes that don't form an hour using %
let minutes = timeDiff % 60 //no need to floor possible incomplete minutes, because they've been handled as seconds
minutes = minutes < 10 ? '0' + minutes : minutes
//convert time difference from minutes to hours
timeDiff = Math.floor(timeDiff / 60)
//extract integer hours that don't form a day using %
let hours = timeDiff % 24 //no need to floor possible incomplete hours, because they've been handled as seconds
//convert time difference from hours to days
timeDiff = Math.floor(timeDiff / 24)
// the rest of timeDiff is number of days
let days = timeDiff //add days to hours
let totalHours = hours + days * 24
totalHours = totalHours < 10 ? '0' + totalHours : totalHours
if (totalHours === '00') {
return minutes + ':' + seconds
} else {
return totalHours + ':' + minutes + ':' + seconds
}
}
//API to handle audio recording
export const audioRecorder = {
/** Stores the recorded audio as Blob objects of audio data as the recording continues*/
audioBlobs: [] /*of type Blob[]*/,
/** Stores the reference of the MediaRecorder instance that handles the MediaStream when recording starts*/
mediaRecorder: null /*of type MediaRecorder*/,
/** Stores the reference to the stream currently capturing the audio*/
streamBeingCaptured: null /*of type MediaStream*/,
/** Start recording the audio
* @returns {Promise} - returns a promise that resolves if audio recording successfully started
*/
start: function () {
//Feature Detection
if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
//Feature is not supported in browser
//return a custom error
return Promise.reject(new Error('mediaDevices API or getUserMedia method is not supported in this browser.'))
} else {
//Feature is supported in browser
//create an audio stream
return (
navigator.mediaDevices
.getUserMedia({ audio: true } /*of type MediaStreamConstraints*/)
//returns a promise that resolves to the audio stream
.then((stream) /*of type MediaStream*/ => {
//save the reference of the stream to be able to stop it when necessary
audioRecorder.streamBeingCaptured = stream
//create a media recorder instance by passing that stream into the MediaRecorder constructor
audioRecorder.mediaRecorder = new MediaRecorder(stream)
/*the MediaRecorder interface of the MediaStream Recording API provides functionality to easily record media*/
//clear previously saved audio Blobs, if any
audioRecorder.audioBlobs = []
//add a dataavailable event listener in order to store the audio data Blobs when recording
audioRecorder.mediaRecorder.addEventListener('dataavailable', (event) => {
//store audio Blob object
audioRecorder.audioBlobs.push(event.data)
})
//start the recording by calling the start method on the media recorder
audioRecorder.mediaRecorder.start()
})
)
/* errors are not handled in the API because if its handled and the promise is chained, the .then after the catch will be executed*/
}
},
/** Stop the started audio recording
* @returns {Promise} - returns a promise that resolves to the audio as a blob file
*/
stop: function () {
//return a promise that would return the blob or URL of the recording
return new Promise((resolve) => {
//save audio type to pass to set the Blob type
let mimeType = audioRecorder.mediaRecorder.mimeType
//listen to the stop event in order to create & return a single Blob object
audioRecorder.mediaRecorder.addEventListener('stop', () => {
//create a single blob object, as we might have gathered a few Blob objects that needs to be joined as one
let audioBlob = new Blob(audioRecorder.audioBlobs, { type: mimeType })
//resolve promise with the single audio blob representing the recorded audio
resolve(audioBlob)
})
audioRecorder.cancel()
})
},
/** Cancel audio recording*/
cancel: function () {
//stop the recording feature
audioRecorder.mediaRecorder.stop()
//stop all the tracks on the active stream in order to stop the stream
audioRecorder.stopStream()
//reset API properties for next recording
audioRecorder.resetRecordingProperties()
},
/** Stop all the tracks on the active stream in order to stop the stream and remove
* the red flashing dot showing in the tab
*/
stopStream: function () {
//stopping the capturing request by stopping all the tracks on the active stream
audioRecorder.streamBeingCaptured
.getTracks() //get all tracks from the stream
.forEach((track) /*of type MediaStreamTrack*/ => track.stop()) //stop each one
},
/** Reset all the recording properties including the media recorder and stream being captured*/
resetRecordingProperties: function () {
audioRecorder.mediaRecorder = null
audioRecorder.streamBeingCaptured = null
/*No need to remove event listeners attached to mediaRecorder as
If a DOM element which is removed is reference-free (no references pointing to it), the element itself is picked
up by the garbage collector as well as any event handlers/listeners associated with it.
getEventListeners(audioRecorder.mediaRecorder) will return an empty array of events.*/
}
}
@@ -11,10 +11,10 @@ import { useTheme } from '@mui/material/styles'
// project imports
import MarketplaceCanvasNode from './MarketplaceCanvasNode'
import MarketplaceCanvasHeader from './MarketplaceCanvasHeader'
import StickyNote from '../canvas/StickyNote'
const nodeTypes = { customNode: MarketplaceCanvasNode }
const nodeTypes = { customNode: MarketplaceCanvasNode, stickyNote: StickyNote }
const edgeTypes = { buttonedge: '' }
// ==============================|| CANVAS ||============================== //
@@ -13,6 +13,7 @@ import AdditionalParamsDialog from '@/ui-component/dialog/AdditionalParamsDialog
// const
import { baseURL } from '@/store/constant'
import LlamaindexPNG from '@/assets/images/llamaindex.png'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
@@ -87,6 +88,23 @@ const MarketplaceCanvasNode = ({ data }) => {
{data.label}
</Typography>
</Box>
<div style={{ flexGrow: 1 }}></div>
{data.tags && data.tags.includes('LlamaIndex') && (
<>
<div
style={{
borderRadius: '50%',
padding: 15
}}
>
<img
style={{ width: '25px', height: '25px', borderRadius: '50%', objectFit: 'contain' }}
src={LlamaindexPNG}
alt='LlamaIndex'
/>
</div>
</>
)}
</div>
{(data.inputAnchors.length > 0 || data.inputParams.length > 0) && (
<>
+324 -99
View File
@@ -1,12 +1,30 @@
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
// material-ui
import { Grid, Box, Stack, Tabs, Tab, Badge } from '@mui/material'
import {
Grid,
Box,
Stack,
Badge,
Toolbar,
TextField,
InputAdornment,
ButtonGroup,
ToggleButton,
InputLabel,
FormControl,
Select,
OutlinedInput,
Checkbox,
ListItemText,
Button
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconHierarchy, IconTool } from '@tabler/icons'
import { IconChevronsDown, IconChevronsUp, IconLayoutGrid, IconList, IconSearch } from '@tabler/icons'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
@@ -23,6 +41,9 @@ import useApi from '@/hooks/useApi'
// const
import { baseURL } from '@/store/constant'
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
import { MarketplaceTable } from '@/ui-component/table/MarketplaceTable'
import MenuItem from '@mui/material/MenuItem'
function TabPanel(props) {
const { children, value, index, ...other } = props
@@ -45,6 +66,19 @@ TabPanel.propTypes = {
value: PropTypes.number.isRequired
}
const ITEM_HEIGHT = 48
const ITEM_PADDING_TOP = 8
const badges = ['POPULAR', 'NEW']
const types = ['Chatflow', 'Tool']
const framework = ['Langchain', 'LlamaIndex']
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
}
}
// ==============================|| Marketplace ||============================== //
const Marketplace = () => {
@@ -53,16 +87,78 @@ const Marketplace = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [isChatflowsLoading, setChatflowsLoading] = useState(true)
const [isToolsLoading, setToolsLoading] = useState(true)
const [isLoading, setLoading] = useState(true)
const [images, setImages] = useState({})
const tabItems = ['Chatflows', 'Tools']
const [value, setValue] = useState(0)
const [showToolDialog, setShowToolDialog] = useState(false)
const [toolDialogProps, setToolDialogProps] = useState({})
const getAllChatflowsMarketplacesApi = useApi(marketplacesApi.getAllChatflowsMarketplaces)
const getAllToolsMarketplacesApi = useApi(marketplacesApi.getAllToolsMarketplaces)
const getAllTemplatesMarketplacesApi = useApi(marketplacesApi.getAllTemplatesFromMarketplaces)
const [view, setView] = React.useState(localStorage.getItem('mpDisplayStyle') || 'card')
const [search, setSearch] = useState('')
const [badgeFilter, setBadgeFilter] = useState([])
const [typeFilter, setTypeFilter] = useState([])
const [frameworkFilter, setFrameworkFilter] = useState([])
const [open, setOpen] = useState(false)
const handleBadgeFilterChange = (event) => {
const {
target: { value }
} = event
setBadgeFilter(
// On autofill we get a stringified value.
typeof value === 'string' ? value.split(',') : value
)
}
const handleTypeFilterChange = (event) => {
const {
target: { value }
} = event
setTypeFilter(
// On autofill we get a stringified value.
typeof value === 'string' ? value.split(',') : value
)
}
const handleFrameworkFilterChange = (event) => {
const {
target: { value }
} = event
setFrameworkFilter(
// On autofill we get a stringified value.
typeof value === 'string' ? value.split(',') : value
)
}
const handleViewChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('mpDisplayStyle', nextView)
setView(nextView)
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
function filterFlows(data) {
return (
data.categories?.toLowerCase().indexOf(search.toLowerCase()) > -1 ||
data.templateName.toLowerCase().indexOf(search.toLowerCase()) > -1 ||
(data.description && data.description.toLowerCase().indexOf(search.toLowerCase()) > -1)
)
}
function filterByBadge(data) {
return badgeFilter.length > 0 ? badgeFilter.includes(data.badge) : true
}
function filterByType(data) {
return typeFilter.length > 0 ? typeFilter.includes(data.type) : true
}
function filterByFramework(data) {
return frameworkFilter.length > 0 ? frameworkFilter.includes(data.framework) : true
}
const onUseTemplate = (selectedTool) => {
const dialogProp = {
@@ -90,39 +186,33 @@ const Marketplace = () => {
navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow })
}
const handleChange = (event, newValue) => {
setValue(newValue)
}
useEffect(() => {
getAllChatflowsMarketplacesApi.request()
getAllToolsMarketplacesApi.request()
getAllTemplatesMarketplacesApi.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setChatflowsLoading(getAllChatflowsMarketplacesApi.loading)
}, [getAllChatflowsMarketplacesApi.loading])
setLoading(getAllTemplatesMarketplacesApi.loading)
}, [getAllTemplatesMarketplacesApi.loading])
useEffect(() => {
setToolsLoading(getAllToolsMarketplacesApi.loading)
}, [getAllToolsMarketplacesApi.loading])
useEffect(() => {
if (getAllChatflowsMarketplacesApi.data) {
if (getAllTemplatesMarketplacesApi.data) {
try {
const chatflows = getAllChatflowsMarketplacesApi.data
const flows = getAllTemplatesMarketplacesApi.data
const images = {}
for (let i = 0; i < chatflows.length; i += 1) {
const flowDataStr = chatflows[i].flowData
const flowData = JSON.parse(flowDataStr)
const nodes = flowData.nodes || []
images[chatflows[i].id] = []
for (let j = 0; j < nodes.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}`
if (!images[chatflows[i].id].includes(imageSrc)) {
images[chatflows[i].id].push(imageSrc)
for (let i = 0; i < flows.length; i += 1) {
if (flows[i].flowData) {
const flowDataStr = flows[i].flowData
const flowData = JSON.parse(flowDataStr)
const nodes = flowData.nodes || []
images[flows[i].id] = []
for (let j = 0; j < nodes.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}`
if (!images[flows[i].id].includes(imageSrc)) {
images[flows[i].id].push(imageSrc)
}
}
}
}
@@ -131,80 +221,215 @@ const Marketplace = () => {
console.error(e)
}
}
}, [getAllChatflowsMarketplacesApi.data])
}, [getAllTemplatesMarketplacesApi.data])
return (
<>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<h1>Marketplace</h1>
</Stack>
<Tabs sx={{ mb: 2 }} variant='fullWidth' value={value} onChange={handleChange} aria-label='tabs'>
{tabItems.map((item, index) => (
<Tab
key={index}
icon={index === 0 ? <IconHierarchy /> : <IconTool />}
iconPosition='start'
label={<span style={{ fontSize: '1.1rem' }}>{item}</span>}
<Box sx={{ flexGrow: 1 }}>
<Toolbar
disableGutters={true}
style={{
margin: 1,
padding: 1,
paddingBottom: 10,
display: 'flex',
justifyContent: 'space-between',
width: '100%'
}}
>
<h1>Marketplace</h1>
<TextField
size='small'
id='search-filter-textbox'
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
variant='outlined'
fullWidth='true'
placeholder='Search name or description or node name'
onChange={onSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<IconSearch />
</InputAdornment>
)
}}
/>
))}
</Tabs>
{tabItems.map((item, index) => (
<TabPanel key={index} value={value} index={index}>
{item === 'Chatflows' && (
<Grid container spacing={gridSpacing}>
{!isChatflowsLoading &&
getAllChatflowsMarketplacesApi.data &&
getAllChatflowsMarketplacesApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
{data.badge && (
<Badge
sx={{
'& .MuiBadge-badge': {
right: 20
}
}}
badgeContent={data.badge}
color={data.badge === 'POPULAR' ? 'primary' : 'error'}
>
<Button
sx={{ width: '220px', ml: 3, mr: 5 }}
variant='outlined'
onClick={() => setOpen(!open)}
startIcon={open ? <IconChevronsUp /> : <IconChevronsDown />}
>
{open ? 'Hide Filters' : 'Show Filters'}
</Button>
<Box sx={{ flexGrow: 1 }} />
<ButtonGroup sx={{ maxHeight: 40 }} disableElevation variant='contained' aria-label='outlined primary button group'>
<ButtonGroup disableElevation variant='contained' aria-label='outlined primary button group'>
<ToggleButtonGroup
sx={{ maxHeight: 40 }}
value={view}
color='primary'
exclusive
onChange={handleViewChange}
>
<ToggleButton
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }}
variant='contained'
value='card'
title='Card View'
>
<IconLayoutGrid />
</ToggleButton>
<ToggleButton
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }}
variant='contained'
value='list'
title='List View'
>
<IconList />
</ToggleButton>
</ToggleButtonGroup>
</ButtonGroup>
</ButtonGroup>
</Toolbar>
</Box>
{open && (
<Box sx={{ flexGrow: 1, mb: 2 }}>
<Toolbar
disableGutters={true}
style={{
margin: 1,
padding: 1,
paddingBottom: 10,
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
borderBottom: '1px solid'
}}
>
<FormControl sx={{ m: 1, width: 250 }}>
<InputLabel size='small' id='filter-badge-label'>
Tag
</InputLabel>
<Select
labelId='filter-badge-label'
id='filter-badge-checkbox'
size='small'
multiple
value={badgeFilter}
onChange={handleBadgeFilterChange}
input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{badges.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox checked={badgeFilter.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ m: 1, width: 250 }}>
<InputLabel size='small' id='type-badge-label'>
Type
</InputLabel>
<Select
size='small'
labelId='type-badge-label'
id='type-badge-checkbox'
multiple
value={typeFilter}
onChange={handleTypeFilterChange}
input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{types.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox checked={typeFilter.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl sx={{ m: 1, width: 250 }}>
<InputLabel size='small' id='type-fw-label'>
Framework
</InputLabel>
<Select
size='small'
labelId='type-fw-label'
id='type-fw-checkbox'
multiple
value={frameworkFilter}
onChange={handleFrameworkFilterChange}
input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{framework.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox checked={frameworkFilter.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</Toolbar>
</Box>
)}
{!isLoading && (!view || view === 'card') && getAllTemplatesMarketplacesApi.data && (
<>
<Grid container spacing={gridSpacing}>
{getAllTemplatesMarketplacesApi.data
.filter(filterByBadge)
.filter(filterByType)
.filter(filterFlows)
.filter(filterByFramework)
.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
{data.badge && (
<Badge
sx={{
'& .MuiBadge-badge': {
right: 20
}
}}
badgeContent={data.badge}
color={data.badge === 'POPULAR' ? 'primary' : 'error'}
>
{data.type === 'Chatflow' && (
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
</Badge>
)}
{!data.badge && (
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
)}
</Grid>
))}
</Grid>
)}
{item === 'Tools' && (
<Grid container spacing={gridSpacing}>
{!isToolsLoading &&
getAllToolsMarketplacesApi.data &&
getAllToolsMarketplacesApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
{data.badge && (
<Badge
sx={{
'& .MuiBadge-badge': {
right: 20
}
}}
badgeContent={data.badge}
color={data.badge === 'POPULAR' ? 'primary' : 'error'}
>
<ItemCard data={data} onClick={() => goToTool(data)} />
</Badge>
)}
{!data.badge && <ItemCard data={data} onClick={() => goToTool(data)} />}
</Grid>
))}
</Grid>
)}
</TabPanel>
))}
{((!isChatflowsLoading && (!getAllChatflowsMarketplacesApi.data || getAllChatflowsMarketplacesApi.data.length === 0)) ||
(!isToolsLoading && (!getAllToolsMarketplacesApi.data || getAllToolsMarketplacesApi.data.length === 0))) && (
)}
{data.type === 'Tool' && <ItemCard data={data} onClick={() => goToTool(data)} />}
</Badge>
)}
{!data.badge && data.type === 'Chatflow' && (
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
)}
{!data.badge && data.type === 'Tool' && <ItemCard data={data} onClick={() => goToTool(data)} />}
</Grid>
))}
</Grid>
</>
)}
{!isLoading && view === 'list' && getAllTemplatesMarketplacesApi.data && (
<MarketplaceTable
sx={{ mt: 20 }}
data={getAllTemplatesMarketplacesApi.data}
filterFunction={filterFlows}
filterByType={filterByType}
filterByBadge={filterByBadge}
filterByFramework={filterByFramework}
goToTool={goToTool}
goToCanvas={goToCanvas}
/>
)}
{!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
+62 -13
View File
@@ -1,9 +1,11 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, List, Paper, Popper, ClickAwayListener } from '@mui/material'
import { ListItemButton, ListItemIcon, ListItemText, Typography, Box, List, Paper, Popper, ClickAwayListener } from '@mui/material'
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
// third-party
import PerfectScrollbar from 'react-perfect-scrollbar'
@@ -11,8 +13,6 @@ import PerfectScrollbar from 'react-perfect-scrollbar'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
import Transitions from '@/ui-component/extended/Transitions'
import NavItem from '@/layout/MainLayout/Sidebar/MenuList/NavItem'
import settings from '@/menu-items/settings'
// ==============================|| SETTINGS ||============================== //
@@ -20,9 +20,26 @@ import settings from '@/menu-items/settings'
const Settings = ({ chatflow, isSettingsOpen, anchorEl, onSettingsItemClick, onUploadFile, onClose }) => {
const theme = useTheme()
const [settingsMenu, setSettingsMenu] = useState([])
const customization = useSelector((state) => state.customization)
const inputFile = useRef(null)
const [open, setOpen] = useState(false)
const handleFileUpload = (e) => {
if (!e.target.files) return
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = (evt) => {
if (!evt?.target?.result) {
return
}
const { result } = evt.target
onUploadFile(result)
}
reader.readAsText(file)
}
useEffect(() => {
if (chatflow && !chatflow.id) {
const settingsMenu = settings.children.filter((menu) => menu.id === 'loadChatflow')
@@ -39,16 +56,40 @@ const Settings = ({ chatflow, isSettingsOpen, anchorEl, onSettingsItemClick, onU
// settings list items
const items = settingsMenu.map((menu) => {
return (
<NavItem
key={menu.id}
item={menu}
level={1}
navType='SETTINGS'
onClick={(id) => onSettingsItemClick(id)}
onUploadFile={onUploadFile}
const Icon = menu.icon
const itemIcon = menu?.icon ? (
<Icon stroke={1.5} size='1.3rem' />
) : (
<FiberManualRecordIcon
sx={{
width: customization.isOpen.findIndex((id) => id === menu?.id) > -1 ? 8 : 6,
height: customization.isOpen.findIndex((id) => id === menu?.id) > -1 ? 8 : 6
}}
fontSize={level > 0 ? 'inherit' : 'medium'}
/>
)
return (
<ListItemButton
key={menu.id}
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
py: 1.25,
pl: `24px`
}}
onClick={() => {
if (menu.id === 'loadChatflow' && inputFile) {
inputFile?.current.click()
} else {
onSettingsItemClick(menu.id)
}
}}
>
<ListItemIcon sx={{ my: 'auto', minWidth: !menu?.icon ? 18 : 36 }}>{itemIcon}</ListItemIcon>
<ListItemText primary={<Typography color='inherit'>{menu.title}</Typography>} />
</ListItemButton>
)
})
return (
@@ -82,6 +123,14 @@ const Settings = ({ chatflow, isSettingsOpen, anchorEl, onSettingsItemClick, onU
<List>{items}</List>
</Box>
</PerfectScrollbar>
<input
type='file'
hidden
accept='.json'
ref={inputFile}
style={{ display: 'none' }}
onChange={(e) => handleFileUpload(e)}
/>
</MainCard>
</ClickAwayListener>
</Paper>
@@ -0,0 +1,68 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
const HowToUseFunctionDialog = ({ show, onCancel }) => {
const portalElement = document.getElementById('portal')
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
How To Use Function
</DialogTitle>
<DialogContent>
<ul>
<li style={{ marginTop: 10 }}>You can use any libraries imported in Flowise</li>
<li style={{ marginTop: 10 }}>
You can use properties specified in Output Schema as variables with prefix $:
<ul style={{ marginTop: 10 }}>
<li>
Property = <code>userid</code>
</li>
<li>
Variable = <code>$userid</code>
</li>
</ul>
</li>
<li style={{ marginTop: 10 }}>
You can get default flow config:
<ul style={{ marginTop: 10 }}>
<li>
<code>$flow.sessionId</code>
</li>
<li>
<code>$flow.chatId</code>
</li>
<li>
<code>$flow.chatflowId</code>
</li>
<li>
<code>$flow.input</code>
</li>
</ul>
</li>
<li style={{ marginTop: 10 }}>
You can get custom variables:&nbsp;<code>{`$vars.<variable-name>`}</code>
</li>
<li style={{ marginTop: 10 }}>Must return a string value at the end of function</li>
</ul>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
HowToUseFunctionDialog.propTypes = {
show: PropTypes.bool,
onCancel: PropTypes.func
}
export default HowToUseFunctionDialog
+22 -30
View File
@@ -12,9 +12,8 @@ import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
import { GridActionsCellItem } from '@mui/x-data-grid'
import DeleteIcon from '@mui/icons-material/Delete'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import { DarkCodeEditor } from '@/ui-component/editor/DarkCodeEditor'
import { LightCodeEditor } from '@/ui-component/editor/LightCodeEditor'
import { useTheme } from '@mui/material/styles'
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
import HowToUseFunctionDialog from './HowToUseFunctionDialog'
// Icons
import { IconX, IconFileExport } from '@tabler/icons'
@@ -34,6 +33,8 @@ import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
const exampleAPIFunc = `/*
* You can use any libraries imported in Flowise
* You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid
* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input
* You can get custom variables: $vars.<variable-name>
* Must return a string value at the end of function
*/
@@ -56,7 +57,6 @@ try {
const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
@@ -77,6 +77,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
const [toolIcon, setToolIcon] = useState('')
const [toolSchema, setToolSchema] = useState([])
const [toolFunc, setToolFunc] = useState('')
const [showHowToDialog, setShowHowToDialog] = useState(false)
const deleteItem = useCallback(
(id) => () => {
@@ -485,37 +486,27 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
/>
</Typography>
</Stack>
<Button
style={{ marginBottom: 10, marginRight: 10 }}
color='secondary'
variant='outlined'
onClick={() => setShowHowToDialog(true)}
>
How to use Function
</Button>
{dialogProps.type !== 'TEMPLATE' && (
<Button style={{ marginBottom: 10 }} variant='outlined' onClick={() => setToolFunc(exampleAPIFunc)}>
See Example
</Button>
)}
{customization.isDarkMode ? (
<DarkCodeEditor
value={toolFunc}
disabled={dialogProps.type === 'TEMPLATE'}
onValueChange={(code) => setToolFunc(code)}
style={{
fontSize: '0.875rem',
minHeight: 'calc(100vh - 220px)',
width: '100%',
borderRadius: 5
}}
/>
) : (
<LightCodeEditor
value={toolFunc}
disabled={dialogProps.type === 'TEMPLATE'}
onValueChange={(code) => setToolFunc(code)}
style={{
fontSize: '0.875rem',
minHeight: 'calc(100vh - 220px)',
width: '100%',
border: `1px solid ${theme.palette.grey[300]}`,
borderRadius: 5
}}
/>
)}
<CodeEditor
disabled={dialogProps.type === 'TEMPLATE'}
value={toolFunc}
height='calc(100vh - 220px)'
theme={customization.isDarkMode ? 'dark' : 'light'}
lang={'js'}
onValueChange={(code) => setToolFunc(code)}
/>
</Box>
</DialogContent>
<DialogActions>
@@ -540,6 +531,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
)}
</DialogActions>
<ConfirmDialog />
<HowToUseFunctionDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)} />
</Dialog>
) : null
@@ -0,0 +1,285 @@
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'
// Material
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, OutlinedInput } from '@mui/material'
// Project imports
import { StyledButton } from 'ui-component/button/StyledButton'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
// Icons
import { IconX, IconVariable } from '@tabler/icons'
// API
import variablesApi from 'api/variables'
// Hooks
// utils
import useNotifier from 'utils/useNotifier'
// const
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
import { Dropdown } from '../../ui-component/dropdown/Dropdown'
const variableTypes = [
{
label: 'Static',
name: 'static',
description: 'Variable value will be read from the value entered below'
},
{
label: 'Runtime',
name: 'runtime',
description: 'Variable value will be read from .env file'
}
]
const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [variableName, setVariableName] = useState('')
const [variableValue, setVariableValue] = useState('')
const [variableType, setVariableType] = useState('static')
const [dialogType, setDialogType] = useState('ADD')
const [variable, setVariable] = useState({})
useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) {
setVariableName(dialogProps.data.name)
setVariableValue(dialogProps.data.value)
setVariableType(dialogProps.data.type)
setDialogType('EDIT')
setVariable(dialogProps.data)
} else if (dialogProps.type === 'ADD') {
setVariableName('')
setVariableValue('')
setVariableType('static')
setDialogType('ADD')
setVariable({})
}
return () => {
setVariableName('')
setVariableValue('')
setVariableType('static')
setDialogType('ADD')
setVariable({})
}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const addNewVariable = async () => {
try {
const obj = {
name: variableName,
value: variableValue,
type: variableType
}
const createResp = await variablesApi.createVariable(obj)
if (createResp.data) {
enqueueSnackbar({
message: 'New Variable added',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(createResp.data.id)
}
} catch (err) {
const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response?.status}: ${err.response?.statusText}`
enqueueSnackbar({
message: `Failed to add new Variable: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const saveVariable = async () => {
try {
const saveObj = {
name: variableName,
value: variableValue,
type: variableType
}
const saveResp = await variablesApi.updateVariable(variable.id, saveObj)
if (saveResp.data) {
enqueueSnackbar({
message: 'Variable saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(saveResp.data.id)
}
} catch (error) {
const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}`
enqueueSnackbar({
message: `Failed to save Variable: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div
style={{
width: 50,
height: 50,
marginRight: 10,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
{dialogProps.type === 'ADD' ? 'Add Variable' : 'Edit Variable'}
</div>
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Variable Name<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
size='small'
sx={{ mt: 1 }}
type='string'
fullWidth
key='variableName'
onChange={(e) => setVariableName(e.target.value)}
value={variableName ?? ''}
/>
</Box>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Type<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<Dropdown
key={variableType}
name='variableType'
options={variableTypes}
onSelect={(newValue) => setVariableType(newValue)}
value={variableType ?? 'choose an option'}
/>
</Box>
{variableType === 'static' && (
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Value<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
size='small'
sx={{ mt: 1 }}
type='string'
fullWidth
key='variableValue'
onChange={(e) => setVariableValue(e.target.value)}
value={variableValue ?? ''}
/>
</Box>
)}
</DialogContent>
<DialogActions>
<StyledButton
disabled={!variableName || !variableType || (variableType === 'static' && !variableValue)}
variant='contained'
onClick={() => (dialogType === 'ADD' ? addNewVariable() : saveVariable())}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
AddEditVariableDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default AddEditVariableDialog
@@ -0,0 +1,72 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
import { CodeEditor } from 'ui-component/editor/CodeEditor'
const overrideConfig = `{
overrideConfig: {
vars: {
var1: 'abc'
}
}
}`
const HowToUseVariablesDialog = ({ show, onCancel }) => {
const portalElement = document.getElementById('portal')
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
How To Use Variables
</DialogTitle>
<DialogContent>
<p style={{ marginBottom: '10px' }}>Variables can be used in Custom Tool Function with the $ prefix.</p>
<CodeEditor
disabled={true}
value={`$vars.<variable-name>`}
height={'50px'}
theme={'dark'}
lang={'js'}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
<p style={{ marginBottom: '10px' }}>
If variable type is Static, the value will be retrieved as it is. If variable type is Runtime, the value will be
retrieved from .env file.
</p>
<p style={{ marginBottom: '10px' }}>
You can also override variable values in API overrideConfig using <b>vars</b>:
</p>
<CodeEditor
disabled={true}
value={overrideConfig}
height={'170px'}
theme={'dark'}
lang={'js'}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
<p>
Read more from{' '}
<a target='_blank' rel='noreferrer' href='https://docs.flowiseai.com/using-flowise/variables'>
docs
</a>
</p>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
HowToUseVariablesDialog.propTypes = {
show: PropTypes.bool,
onCancel: PropTypes.func
}
export default HowToUseVariablesDialog
+314
View File
@@ -0,0 +1,314 @@
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import moment from 'moment'
// material-ui
import {
Button,
Box,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Toolbar,
TextField,
InputAdornment,
ButtonGroup,
Chip
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import { StyledButton } from 'ui-component/button/StyledButton'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
// API
import variablesApi from 'api/variables'
// Hooks
import useApi from 'hooks/useApi'
import useConfirm from 'hooks/useConfirm'
// utils
import useNotifier from 'utils/useNotifier'
// Icons
import { IconTrash, IconEdit, IconX, IconPlus, IconSearch, IconVariable } from '@tabler/icons'
import VariablesEmptySVG from 'assets/images/variables_empty.svg'
// const
import AddEditVariableDialog from './AddEditVariableDialog'
import HowToUseVariablesDialog from './HowToUseVariablesDialog'
// ==============================|| Credentials ||============================== //
const Variables = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [showVariableDialog, setShowVariableDialog] = useState(false)
const [variableDialogProps, setVariableDialogProps] = useState({})
const [variables, setVariables] = useState([])
const [showHowToDialog, setShowHowToDialog] = useState(false)
const { confirm } = useConfirm()
const getAllVariables = useApi(variablesApi.getAllVariables)
const [search, setSearch] = useState('')
const onSearchChange = (event) => {
setSearch(event.target.value)
}
function filterVariables(data) {
return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1
}
const addNew = () => {
const dialogProp = {
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
data: {}
}
setVariableDialogProps(dialogProp)
setShowVariableDialog(true)
}
const edit = (variable) => {
const dialogProp = {
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
data: variable
}
setVariableDialogProps(dialogProp)
setShowVariableDialog(true)
}
const deleteVariable = async (variable) => {
const confirmPayload = {
title: `Delete`,
description: `Delete variable ${variable.name}?`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
const deleteResp = await variablesApi.deleteVariable(variable.id)
if (deleteResp.data) {
enqueueSnackbar({
message: 'Variable deleted',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm()
}
} catch (error) {
const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}`
enqueueSnackbar({
message: `Failed to delete Variable: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const onConfirm = () => {
setShowVariableDialog(false)
getAllVariables.request()
}
useEffect(() => {
getAllVariables.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllVariables.data) {
setVariables(getAllVariables.data)
}
}, [getAllVariables.data])
return (
<>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<Box sx={{ flexGrow: 1 }}>
<Toolbar
disableGutters={true}
style={{
margin: 1,
padding: 1,
paddingBottom: 10,
display: 'flex',
justifyContent: 'space-between',
width: '100%'
}}
>
<h1>Variables&nbsp;</h1>
<TextField
size='small'
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
variant='outlined'
placeholder='Search variable name'
onChange={onSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<IconSearch />
</InputAdornment>
)
}}
/>
<Box sx={{ flexGrow: 1 }} />
<Button variant='outlined' sx={{ mr: 2 }} onClick={() => setShowHowToDialog(true)}>
How To Use
</Button>
<ButtonGroup
sx={{ maxHeight: 40 }}
disableElevation
variant='contained'
aria-label='outlined primary button group'
>
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton
variant='contained'
sx={{ color: 'white', mr: 1, height: 37 }}
onClick={addNew}
startIcon={<IconPlus />}
>
Add Variable
</StyledButton>
</ButtonGroup>
</ButtonGroup>
</Toolbar>
</Box>
</Stack>
{variables.length === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '30vh', width: 'auto' }}
src={VariablesEmptySVG}
alt='VariablesEmptySVG'
/>
</Box>
<div>No Variables Yet</div>
</Stack>
)}
{variables.length > 0 && (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Value</TableCell>
<TableCell>Type</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell>Created</TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{variables.filter(filterVariables).map((variable, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component='th' scope='row'>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<div
style={{
width: 25,
height: 25,
marginRight: 10,
borderRadius: '50%'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
{variable.name}
</div>
</TableCell>
<TableCell>{variable.value}</TableCell>
<TableCell>
<Chip
color={variable.type === 'static' ? 'info' : 'secondary'}
size='small'
label={variable.type}
/>
</TableCell>
<TableCell>{moment(variable.updatedDate).format('DD-MMM-YY')}</TableCell>
<TableCell>{moment(variable.createdDate).format('DD-MMM-YY')}</TableCell>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit />
</IconButton>
</TableCell>
<TableCell>
<IconButton title='Delete' color='error' onClick={() => deleteVariable(variable)}>
<IconTrash />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</MainCard>
<AddEditVariableDialog
show={showVariableDialog}
dialogProps={variableDialogProps}
onCancel={() => setShowVariableDialog(false)}
onConfirm={onConfirm}
></AddEditVariableDialog>
<HowToUseVariablesDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)}></HowToUseVariablesDialog>
<ConfirmDialog />
</>
)
}
export default Variables