mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 19:00:59 +03:00
Fix merge conflicts
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <audio> 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) && (
|
||||
<>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: <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
|
||||
@@ -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' }}> *</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' }}> *</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' }}> *</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
|
||||
@@ -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 </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
|
||||
Reference in New Issue
Block a user