Fix merge conflicts
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
webpack: {
|
||||
configure: {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
ignoreWarnings: [/Failed to parse source map/] // Ignore warnings about source maps
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flowise-ui",
|
||||
"version": "1.4.1",
|
||||
"version": "1.6.0",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://flowiseai.com",
|
||||
"author": {
|
||||
@@ -8,14 +8,20 @@
|
||||
"email": "henryheng@flowiseai.com"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/view": "^6.22.3",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.0.3",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@mui/x-data-grid": "^6.8.0",
|
||||
"@mui/icons-material": "5.0.3",
|
||||
"@mui/lab": "5.0.0-alpha.156",
|
||||
"@mui/material": "5.15.0",
|
||||
"@mui/x-data-grid": "6.8.0",
|
||||
"@tabler/icons": "^1.39.1",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"@uiw/codemirror-theme-sublime": "^4.21.21",
|
||||
"@uiw/codemirror-theme-vscode": "^4.21.21",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"axios": "^0.27.2",
|
||||
"clsx": "^1.1.1",
|
||||
"dotenv": "^16.0.0",
|
||||
@@ -29,8 +35,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.3",
|
||||
"notistack": "^2.0.4",
|
||||
"openai": "^4.20.0",
|
||||
"prismjs": "^1.28.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^18.2.0",
|
||||
"react-code-blocks": "^0.0.9-0",
|
||||
@@ -43,7 +47,6 @@
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router": "~6.3.0",
|
||||
"react-router-dom": "~6.3.0",
|
||||
"react-simple-code-editor": "^0.11.2",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"reactflow": "^11.5.6",
|
||||
"redux": "^4.0.5",
|
||||
@@ -53,9 +56,6 @@
|
||||
"remark-math": "^5.1.1",
|
||||
"socket.io-client": "^4.6.1",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.0.2",
|
||||
"vite-plugin-pwa": "^0.17.0",
|
||||
"vite-plugin-react-js-support": "^1.0.7",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -87,10 +87,14 @@
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"sass": "^1.42.1",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^5.0.2",
|
||||
"vite-plugin-pwa": "^0.17.0",
|
||||
"vite-plugin-react-js-support": "^1.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,19 @@
|
||||
<meta property="og:site_name" content="flowiseai.com" />
|
||||
<meta property="article:publisher" content="https://www.facebook.com/codedthemes" />
|
||||
<meta property="og:title" content="Flowise - Low-code LLM apps builder" />
|
||||
<meta property="og:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Build customized LLM orchestration flow & agents, enable quick iterations from testing to production"
|
||||
/>
|
||||
<meta property="og:image" content="https://flowiseai.com/og-image/og-facebook.png" />
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://flowiseai.com" />
|
||||
<meta property="twitter:title" content="Flowise - Low-code LLM apps builder" />
|
||||
<meta property="twitter:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Build customized LLM orchestration flow & agents, enable quick iterations from testing to production"
|
||||
/>
|
||||
<meta property="twitter:image" content="https://flowiseai.com/og-image/og-twitter.png" />
|
||||
<meta name="twitter:creator" content="@codedthemes" />
|
||||
<!--
|
||||
|
||||
@@ -14,6 +14,8 @@ const deleteChatflow = (id) => client.delete(`/chatflows/${id}`)
|
||||
|
||||
const getIsChatflowStreaming = (id) => client.get(`/chatflows-streaming/${id}`)
|
||||
|
||||
const getAllowChatflowUploads = (id) => client.get(`/chatflows-uploads/${id}`)
|
||||
|
||||
export default {
|
||||
getAllChatflows,
|
||||
getSpecificChatflow,
|
||||
@@ -21,5 +23,6 @@ export default {
|
||||
createNewChatflow,
|
||||
updateChatflow,
|
||||
deleteChatflow,
|
||||
getIsChatflowStreaming
|
||||
getIsChatflowStreaming,
|
||||
getAllowChatflowUploads
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ const getInternalChatmessageFromChatflow = (id) => client.get(`/internal-chatmes
|
||||
const getAllChatmessageFromChatflow = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'DESC', ...params } })
|
||||
const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', ...params } })
|
||||
const deleteChatmessage = (id, params = {}) => client.delete(`/chatmessage/${id}`, { params: { ...params } })
|
||||
const getStoragePath = () => client.get(`/get-upload-path`)
|
||||
|
||||
export default {
|
||||
getInternalChatmessageFromChatflow,
|
||||
getAllChatmessageFromChatflow,
|
||||
getChatmessageFromPK,
|
||||
deleteChatmessage
|
||||
deleteChatmessage,
|
||||
getStoragePath
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import client from './client'
|
||||
|
||||
const getAllChatflowsMarketplaces = () => client.get('/marketplaces/chatflows')
|
||||
const getAllToolsMarketplaces = () => client.get('/marketplaces/tools')
|
||||
const getAllTemplatesFromMarketplaces = () => client.get('/marketplaces/templates')
|
||||
|
||||
export default {
|
||||
getAllChatflowsMarketplaces,
|
||||
getAllToolsMarketplaces
|
||||
getAllToolsMarketplaces,
|
||||
getAllTemplatesFromMarketplaces
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ const getAllNodes = () => client.get('/nodes')
|
||||
|
||||
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
|
||||
|
||||
const executeCustomFunctionNode = (body) => client.post(`/node-custom-function`, body)
|
||||
|
||||
export default {
|
||||
getAllNodes,
|
||||
getSpecificNode
|
||||
getSpecificNode,
|
||||
executeCustomFunctionNode
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import client from './client'
|
||||
|
||||
const getAvailablePrompts = (body) => client.post(`/prompts-list`, body)
|
||||
const getPrompt = (body) => client.post(`/load-prompt`, body)
|
||||
|
||||
export default {
|
||||
getAvailablePrompts,
|
||||
getPrompt
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import client from './client'
|
||||
|
||||
const fetchLinks = (url, relativeLinksMethod, relativeLinksLimit) =>
|
||||
client.get(`/fetch-links?url=${encodeURIComponent(url)}&relativeLinksMethod=${relativeLinksMethod}&limit=${relativeLinksLimit}`)
|
||||
|
||||
export default {
|
||||
fetchLinks
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllVariables = () => client.get('/variables')
|
||||
|
||||
const createVariable = (body) => client.post(`/variables`, body)
|
||||
|
||||
const updateVariable = (id, body) => client.put(`/variables/${id}`, body)
|
||||
|
||||
const deleteVariable = (id) => client.delete(`/variables/${id}`)
|
||||
|
||||
export default {
|
||||
getAllVariables,
|
||||
createVariable,
|
||||
updateVariable,
|
||||
deleteVariable
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 13.2044C23.0012 13.3219 21.9869 13.2745 21 13.0618C19.7752 12.7978 18.5927 12.2794 17.5344 11.506L15.8458 10.2721C14.0933 8.99141 11.9531 8.63334 10 9.10241C9.30052 9.27041 8.62505 9.54449 8 9.92028M24 18.7958C23.0012 18.6783 21.9869 18.7258 21 18.9385C19.7752 19.2024 18.5927 19.7208 17.5344 20.4942L15.8458 21.7282C14.0933 23.0088 11.9531 23.3669 10 22.8978C9.30052 22.7298 8.62505 22.4557 8 22.08" stroke="#E91212" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 21.0001L3.58441 20.5618C6.8019 18.1487 11.2184 18.1213 14.4656 20.4942L16.1542 21.7282C19.3633 24.0733 23.8728 23.3248 26.1521 20.0686C27.862 17.6258 27.862 14.3745 26.1521 11.9317C23.8728 8.67548 19.3633 7.92693 16.1542 10.2721L14.4656 11.506C11.2184 13.879 6.8019 13.8515 3.58441 11.4384L3 11.0001" stroke="#1363BB" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M29 21L28.4156 20.5617C27.0917 19.5688 25.5649 18.9798 24 18.7957C23.0012 18.6782 21.9869 18.7257 21 18.9383M29 11L28.4156 11.4383C27.0917 12.4312 25.5649 13.0202 24 13.2043C23.0012 13.3218 21.9869 13.2743 21 13.0617M10 9.10229C9.30052 9.27029 8.62505 9.54437 8 9.92017C7.17438 10.4165 6.43672 11.0904 5.84789 11.9316C4.13794 14.3743 4.13794 17.6257 5.84789 20.0684C6.43672 20.9096 7.17438 21.5835 8 22.0798C8.62505 22.4556 9.30052 22.7297 10 22.8977" stroke="#E91212" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="15.9562" cy="16.0002" rx="4.3478" ry="4.34782" stroke="black" stroke-width="2"/>
|
||||
<path d="M23.5651 23.6086C21.603 25.621 18.8688 26.8695 15.8445 26.8695C10.2386 26.8695 5.62933 22.5797 5.08691 17.0869" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M8.34766 8.23751C10.314 6.3155 13.0074 5.13037 15.9785 5.13037C21.6311 5.13037 26.2789 9.42024 26.8258 14.913" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M9.5217 9.47815C9.5217 10.7268 8.50948 11.739 7.26085 11.739C6.01222 11.739 5 10.7268 5 9.47815C5 8.22951 6.01222 7.21729 7.26085 7.21729C8.50948 7.21729 9.5217 8.22951 9.5217 9.47815Z" fill="black" stroke="black" stroke-width="2"/>
|
||||
<path d="M28.0002 21.4347C28.0002 22.6833 26.988 23.6956 25.7394 23.6956C24.4907 23.6956 23.4785 22.6833 23.4785 21.4347C23.4785 20.186 24.4907 19.1738 25.7394 19.1738C26.988 19.1738 28.0002 20.186 28.0002 21.4347Z" fill="black" stroke="black" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.9814 25.2665C15.353 26.2672 16.0645 27.1054 16.9914 27.6347C17.9183 28.164 19.0018 28.3507 20.0524 28.1622C21.103 27.9737 22.054 27.422 22.7391 26.6034C23.4242 25.7849 23.7998 24.7517 23.8004 23.6842V17.5533C23.8004 17.1909 23.6043 16.8569 23.2879 16.6802L15.9995 12.6108" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.17701 19.5848C6.49568 20.4069 6.12505 21.4424 6.12993 22.5101C6.13481 23.5779 6.51489 24.6099 7.2037 25.4258C7.89252 26.2416 8.84622 26.7893 9.89802 26.9732C10.9498 27.157 12.0328 26.9653 12.9575 26.4314L18.1044 23.4263C18.4114 23.247 18.6002 22.9182 18.6002 22.5627V14.106" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.19877 9.98459C6.39026 9.67775 4.57524 10.4982 3.60403 12.1806C3.00524 13.2178 2.84295 14.4504 3.15284 15.6073C3.46273 16.7642 4.21943 17.7507 5.25652 18.3498L10.3049 21.3269C10.6109 21.5074 10.9898 21.5119 11.3001 21.3388L18.6 17.2655" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17.0172 6.06585C16.6456 5.06522 15.9342 4.227 15.0072 3.6977C14.0803 3.1684 12.9969 2.98168 11.9462 3.17018C10.8956 3.35869 9.94464 3.91042 9.25954 4.72895C8.57444 5.54747 8.19879 6.58074 8.19824 7.64814V13.6575C8.19824 14.0154 8.38951 14.346 8.69977 14.5244L15.9992 18.7215" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.8216 11.7476C25.5029 10.9255 25.8735 9.89004 25.8687 8.8223C25.8638 7.75457 25.4837 6.72253 24.7949 5.90667C24.1061 5.09082 23.1524 4.54308 22.1006 4.35924C21.0488 4.17541 19.9658 4.36718 19.0411 4.90101L13.8942 7.90613C13.5872 8.08539 13.3984 8.41418 13.3984 8.76971V17.2265" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M23.7997 21.2595C25.6082 21.5663 27.4232 20.7459 28.3944 19.0635C28.9932 18.0263 29.1555 16.7937 28.8456 15.6368C28.5357 14.4799 27.779 13.4934 26.7419 12.8943L21.6409 9.91752C21.3316 9.73703 20.9494 9.7357 20.6388 9.91405L17.2696 11.849L13.3984 14.0723" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 323 KiB |
@@ -1,8 +1,8 @@
|
||||
// assets
|
||||
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } from '@tabler/icons'
|
||||
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable } from '@tabler/icons'
|
||||
|
||||
// constant
|
||||
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot }
|
||||
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable }
|
||||
|
||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||
|
||||
@@ -51,6 +51,14 @@ const dashboard = {
|
||||
icon: icons.IconLock,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'variables',
|
||||
title: 'Variables',
|
||||
type: 'item',
|
||||
url: '/variables',
|
||||
icon: icons.IconVariable,
|
||||
breadcrumbs: true
|
||||
},
|
||||
{
|
||||
id: 'apikey',
|
||||
title: 'API Keys',
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
// assets
|
||||
import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage } from '@tabler/icons'
|
||||
import {
|
||||
IconTrash,
|
||||
IconFileUpload,
|
||||
IconFileExport,
|
||||
IconCopy,
|
||||
IconSearch,
|
||||
IconMessage,
|
||||
IconPictureInPictureOff,
|
||||
IconMicrophone
|
||||
} from '@tabler/icons'
|
||||
|
||||
// constant
|
||||
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage }
|
||||
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff, IconMicrophone }
|
||||
|
||||
// ==============================|| SETTINGS MENU ITEMS ||============================== //
|
||||
|
||||
@@ -11,6 +20,13 @@ const settings = {
|
||||
title: '',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
id: 'conversationStarters',
|
||||
title: 'Starter Prompts',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconPictureInPictureOff
|
||||
},
|
||||
{
|
||||
id: 'viewMessages',
|
||||
title: 'View Messages',
|
||||
@@ -18,6 +34,13 @@ const settings = {
|
||||
url: '',
|
||||
icon: icons.IconMessage
|
||||
},
|
||||
{
|
||||
id: 'enableSpeechToText',
|
||||
title: 'Speech to Text',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconMicrophone
|
||||
},
|
||||
{
|
||||
id: 'duplicateChatflow',
|
||||
title: 'Duplicate Chatflow',
|
||||
|
||||
@@ -22,6 +22,9 @@ const Assistants = Loadable(lazy(() => import('@/views/assistants')))
|
||||
// credentials routing
|
||||
const Credentials = Loadable(lazy(() => import('@/views/credentials')))
|
||||
|
||||
// variables routing
|
||||
const Variables = Loadable(lazy(() => import('views/variables')))
|
||||
|
||||
// ==============================|| MAIN ROUTING ||============================== //
|
||||
|
||||
const MainRoutes = {
|
||||
@@ -55,6 +58,10 @@ const MainRoutes = {
|
||||
{
|
||||
path: '/credentials',
|
||||
element: <Credentials />
|
||||
},
|
||||
{
|
||||
path: '/variables',
|
||||
element: <Variables />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import FileCopyIcon from '@mui/icons-material/FileCopy'
|
||||
import FileDownloadIcon from '@mui/icons-material/Downloading'
|
||||
import FileDeleteIcon from '@mui/icons-material/Delete'
|
||||
import FileCategoryIcon from '@mui/icons-material/Category'
|
||||
import PictureInPictureAltIcon from '@mui/icons-material/PictureInPictureAlt'
|
||||
import Button from '@mui/material/Button'
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||
import { IconX } from '@tabler/icons'
|
||||
@@ -22,8 +23,9 @@ import useConfirm from '@/hooks/useConfirm'
|
||||
import { uiBaseURL } from '@/store/constant'
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
|
||||
|
||||
import SaveChatflowDialog from '../dialog/SaveChatflowDialog'
|
||||
import TagDialog from '../dialog/TagDialog'
|
||||
import SaveChatflowDialog from '@/dialog/SaveChatflowDialog'
|
||||
import TagDialog from '@/dialog/TagDialog'
|
||||
import StarterPromptsDialog from '@/dialog/StarterPromptsDialog'
|
||||
|
||||
import { generateExportFlowData } from '@/utils/genericHelper'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
@@ -78,6 +80,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
|
||||
const [categoryDialogProps, setCategoryDialogProps] = useState({})
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const open = Boolean(anchorEl)
|
||||
const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false)
|
||||
const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({})
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
@@ -92,6 +96,20 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
|
||||
setFlowDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleFlowStarterPrompts = () => {
|
||||
setAnchorEl(null)
|
||||
setConversationStartersDialogProps({
|
||||
title: 'Starter Prompts - ' + chatflow.name,
|
||||
chatflow: chatflow
|
||||
})
|
||||
setConversationStartersDialogOpen(true)
|
||||
}
|
||||
|
||||
const saveFlowStarterPrompts = async () => {
|
||||
setConversationStartersDialogOpen(false)
|
||||
await updateFlowsApi.request()
|
||||
}
|
||||
|
||||
const saveFlowRename = async (chatflowName) => {
|
||||
const updateBody = {
|
||||
name: chatflowName,
|
||||
@@ -253,6 +271,10 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
|
||||
Export
|
||||
</MenuItem>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<MenuItem onClick={handleFlowStarterPrompts} disableRipple>
|
||||
<PictureInPictureAltIcon />
|
||||
Starter Prompts
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleFlowCategory} disableRipple>
|
||||
<FileCategoryIcon />
|
||||
Update Category
|
||||
@@ -279,6 +301,12 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
|
||||
onClose={() => setCategoryDialogOpen(false)}
|
||||
onSubmit={saveFlowCategory}
|
||||
/>
|
||||
<StarterPromptsDialog
|
||||
show={conversationStartersDialogOpen}
|
||||
dialogProps={conversationStartersDialogProps}
|
||||
onConfirm={saveFlowStarterPrompts}
|
||||
onCancel={() => setConversationStartersDialogOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { styled } from '@mui/material/styles'
|
||||
import ButtonBase from '@mui/material/ButtonBase'
|
||||
|
||||
export const ImageButton = styled(ButtonBase)(({ theme }) => ({
|
||||
position: 'relative',
|
||||
height: 200,
|
||||
borderRadius: '10px',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
width: '100% !important', // Overrides inline-style
|
||||
height: 100
|
||||
},
|
||||
'&:hover, &.Mui-focusVisible': {
|
||||
zIndex: 1,
|
||||
'& .MuiImageBackdrop-root': {
|
||||
opacity: 0.4
|
||||
},
|
||||
'& .MuiImageMarked-root': {
|
||||
opacity: 1
|
||||
},
|
||||
'& .MuiTypography-root': {
|
||||
border: '4px solid currentColor'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
export const ImageSrc = styled('span')({
|
||||
position: 'absolute',
|
||||
borderRadius: '10px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center 40%'
|
||||
})
|
||||
|
||||
export const ImageBackdrop = styled('span')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
borderRadius: '10px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.common.black,
|
||||
opacity: 0.1,
|
||||
transition: theme.transitions.create('opacity')
|
||||
}))
|
||||
|
||||
export const ImageMarked = styled('span')(() => ({
|
||||
height: 25,
|
||||
width: 25,
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
top: 'auto',
|
||||
left: 'auto',
|
||||
opacity: 0
|
||||
}))
|
||||
@@ -0,0 +1,21 @@
|
||||
// material-ui
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import MainCard from './MainCard'
|
||||
|
||||
const NodeCardWrapper = 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
|
||||
}
|
||||
}))
|
||||
|
||||
export default NodeCardWrapper
|
||||
@@ -0,0 +1,10 @@
|
||||
.button-container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */
|
||||
scrollbar-width: none; /* For Firefox */
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Chip } from '@mui/material'
|
||||
import './StarterPromptsCard.css'
|
||||
|
||||
const StarterPromptsCard = ({ isGrid, starterPrompts, sx, onPromptClick }) => {
|
||||
return (
|
||||
<Box
|
||||
className={'button-container'}
|
||||
sx={{ width: '100%', maxWidth: isGrid ? 'inherit' : '400px', p: 1.5, display: 'flex', gap: 1, ...sx }}
|
||||
>
|
||||
{starterPrompts.map((sp, index) => (
|
||||
<Chip label={sp.prompt} className={'button'} key={index} onClick={(e) => onPromptClick(sp.prompt, e)} />
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
StarterPromptsCard.propTypes = {
|
||||
isGrid: PropTypes.bool,
|
||||
starterPrompts: PropTypes.array,
|
||||
sx: PropTypes.object,
|
||||
onPromptClick: PropTypes.func
|
||||
}
|
||||
|
||||
export default StarterPromptsCard
|
||||
@@ -30,8 +30,7 @@ import { SwitchInput } from '@/ui-component/switch/Switch'
|
||||
import { Input } from '@/ui-component/input/Input'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import langsmithPNG from '@/assets/images/langchain.png'
|
||||
import langfusePNG from '@/assets/images/langfuse.png'
|
||||
import llmonitorPNG from '@/assets/images/llmonitor.png'
|
||||
import lunarySVG from '@/assets/images/lunary.svg'
|
||||
|
||||
// store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
@@ -72,7 +71,7 @@ const analyticProviders = [
|
||||
{
|
||||
label: 'LangFuse',
|
||||
name: 'langFuse',
|
||||
icon: langfusePNG,
|
||||
icon: langfuseSVG,
|
||||
url: 'https://langfuse.com',
|
||||
inputs: [
|
||||
{
|
||||
@@ -97,16 +96,16 @@ const analyticProviders = [
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'LLMonitor',
|
||||
name: 'llmonitor',
|
||||
icon: llmonitorPNG,
|
||||
url: 'https://llmonitor.com',
|
||||
label: 'Lunary',
|
||||
name: 'lunary',
|
||||
icon: lunarySVG,
|
||||
url: 'https://lunary.ai',
|
||||
inputs: [
|
||||
{
|
||||
label: 'Connect Credential',
|
||||
name: 'credential',
|
||||
type: 'credential',
|
||||
credentialNames: ['llmonitorApi']
|
||||
credentialNames: ['lunaryApi']
|
||||
},
|
||||
{
|
||||
label: 'On/Off',
|
||||
|
||||
@@ -2,14 +2,24 @@ import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// MUI
|
||||
import { Button, Dialog, DialogActions, DialogContent, Typography } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import { LoadingButton } from '@mui/lab'
|
||||
|
||||
// Project Import
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import { DarkCodeEditor } from '@/ui-component/editor/DarkCodeEditor'
|
||||
import { LightCodeEditor } from '@/ui-component/editor/LightCodeEditor'
|
||||
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
|
||||
|
||||
// Store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
|
||||
// API
|
||||
import nodesApi from '@/api/nodes'
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
import './ExpandTextDialog.css'
|
||||
|
||||
const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
@@ -18,18 +28,30 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const languageType = 'json'
|
||||
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [inputParam, setInputParam] = useState(null)
|
||||
const [languageType, setLanguageType] = useState('json')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [codeExecutedResult, setCodeExecutedResult] = useState('')
|
||||
|
||||
const executeCustomFunctionNodeApi = useApi(nodesApi.executeCustomFunctionNode)
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.value) setInputValue(dialogProps.value)
|
||||
if (dialogProps.inputParam) setInputParam(dialogProps.inputParam)
|
||||
if (dialogProps.inputParam) {
|
||||
setInputParam(dialogProps.inputParam)
|
||||
if (dialogProps.inputParam.type === 'code') {
|
||||
setLanguageType('js')
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
setInputValue('')
|
||||
setLoading(false)
|
||||
setInputParam(null)
|
||||
setLanguageType('json')
|
||||
setCodeExecutedResult('')
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
@@ -39,11 +61,35 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(executeCustomFunctionNodeApi.loading)
|
||||
}, [executeCustomFunctionNodeApi.loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (executeCustomFunctionNodeApi.data) {
|
||||
if (typeof executeCustomFunctionNodeApi.data === 'object') {
|
||||
setCodeExecutedResult(JSON.stringify(executeCustomFunctionNodeApi.data, null, 2))
|
||||
} else {
|
||||
setCodeExecutedResult(executeCustomFunctionNodeApi.data)
|
||||
}
|
||||
}
|
||||
}, [executeCustomFunctionNodeApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (executeCustomFunctionNodeApi.error) {
|
||||
if (typeof executeCustomFunctionNodeApi.error === 'object' && executeCustomFunctionNodeApi.error?.response?.data) {
|
||||
setCodeExecutedResult(executeCustomFunctionNodeApi.error?.response?.data)
|
||||
} else if (typeof executeCustomFunctionNodeApi.error === 'string') {
|
||||
setCodeExecutedResult(executeCustomFunctionNodeApi.error)
|
||||
}
|
||||
}
|
||||
}, [executeCustomFunctionNodeApi.error])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog open={show} fullWidth maxWidth='md' aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
|
||||
<DialogContent>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
{inputParam && inputParam.type === 'string' && (
|
||||
{inputParam && (inputParam.type === 'string' || inputParam.type === 'code') && (
|
||||
<div style={{ flex: 70 }}>
|
||||
<Typography sx={{ mb: 2, ml: 1 }} variant='h4'>
|
||||
{inputParam.label}
|
||||
@@ -54,42 +100,66 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
borderColor: theme.palette.grey['500'],
|
||||
borderRadius: '12px',
|
||||
height: '100%',
|
||||
maxHeight: 'calc(100vh - 220px)',
|
||||
maxHeight: languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)',
|
||||
overflowX: 'hidden',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
{customization.isDarkMode ? (
|
||||
<DarkCodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
placeholder={inputParam.placeholder}
|
||||
type={languageType}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LightCodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
placeholder={inputParam.placeholder}
|
||||
type={languageType}
|
||||
style={{
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'calc(100vh - 220px)',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CodeEditor
|
||||
disabled={dialogProps.disabled}
|
||||
value={inputValue}
|
||||
height={languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)'}
|
||||
theme={customization.isDarkMode ? 'dark' : 'light'}
|
||||
lang={languageType}
|
||||
placeholder={inputParam.placeholder}
|
||||
basicSetup={
|
||||
languageType === 'json'
|
||||
? { lineNumbers: false, foldGutter: false, autocompletion: false, highlightActiveLine: false }
|
||||
: {}
|
||||
}
|
||||
onValueChange={(code) => setInputValue(code)}
|
||||
/>
|
||||
</PerfectScrollbar>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{languageType === 'js' && (
|
||||
<LoadingButton
|
||||
sx={{
|
||||
mt: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
backgroundImage: `linear-gradient(rgb(0 0 0/10%) 0 0)`
|
||||
},
|
||||
'&:disabled': {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
backgroundImage: `linear-gradient(rgb(0 0 0/50%) 0 0)`
|
||||
}
|
||||
}}
|
||||
loading={loading}
|
||||
variant='contained'
|
||||
fullWidth
|
||||
color='secondary'
|
||||
onClick={() => {
|
||||
setLoading(true)
|
||||
executeCustomFunctionNodeApi.request({ javascriptFunction: inputValue })
|
||||
}}
|
||||
>
|
||||
Execute
|
||||
</LoadingButton>
|
||||
)}
|
||||
{codeExecutedResult && (
|
||||
<div style={{ marginTop: '15px' }}>
|
||||
<CodeEditor
|
||||
disabled={true}
|
||||
value={codeExecutedResult}
|
||||
height='max-content'
|
||||
theme={customization.isDarkMode ? 'dark' : 'light'}
|
||||
lang={'js'}
|
||||
basicSetup={{ lineNumbers: false, foldGutter: false, autocompletion: false, highlightActiveLine: false }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
IconButton,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { IconEraser, IconTrash, IconX } from '@tabler/icons'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
|
||||
import scraperApi from 'api/scraper'
|
||||
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
import {
|
||||
HIDE_CANVAS_DIALOG,
|
||||
SHOW_CANVAS_DIALOG,
|
||||
enqueueSnackbar as enqueueSnackbarAction,
|
||||
closeSnackbar as closeSnackbarAction
|
||||
} from 'store/actions'
|
||||
|
||||
const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useNotifier()
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedLinks, setSelectedLinks] = useState([])
|
||||
const [url, setUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.url) setUrl(dialogProps.url)
|
||||
if (dialogProps.selectedLinks) setSelectedLinks(dialogProps.selectedLinks)
|
||||
|
||||
return () => {
|
||||
setLoading(false)
|
||||
setSelectedLinks([])
|
||||
setUrl('')
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const handleFetchLinks = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const fetchLinksResp = await scraperApi.fetchLinks(url, dialogProps.relativeLinksMethod, dialogProps.limit)
|
||||
if (fetchLinksResp.data) {
|
||||
setSelectedLinks(fetchLinksResp.data.links)
|
||||
enqueueSnackbar({
|
||||
message: 'Successfully fetched links',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: errorData,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleChangeLink = (index, event) => {
|
||||
const { value } = event.target
|
||||
const links = [...selectedLinks]
|
||||
links[index] = value
|
||||
setSelectedLinks(links)
|
||||
}
|
||||
|
||||
const handleRemoveLink = (index) => {
|
||||
const links = [...selectedLinks]
|
||||
links.splice(index, 1)
|
||||
setSelectedLinks(links)
|
||||
}
|
||||
|
||||
const handleRemoveAllLinks = () => {
|
||||
setSelectedLinks([])
|
||||
}
|
||||
|
||||
const handleSaveLinks = () => {
|
||||
onSave(url, selectedLinks)
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
aria-labelledby='manage-scraped-links-dialog-title'
|
||||
aria-describedby='manage-scraped-links-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='manage-scraped-links-dialog-title'>
|
||||
{dialogProps.title || `Manage Scraped Links - ${url}`}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Stack flexDirection='row' gap={1} sx={{ width: '100%' }}>
|
||||
<FormControl sx={{ mt: 1, width: '100%', display: 'flex', flexShrink: 1 }} size='small'>
|
||||
<OutlinedInput
|
||||
id='url'
|
||||
size='small'
|
||||
type='text'
|
||||
value={url}
|
||||
name='url'
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
disabled={!url}
|
||||
sx={{ borderRadius: '12px', mt: 1, display: 'flex', flexShrink: 0 }}
|
||||
size='small'
|
||||
variant='contained'
|
||||
onClick={handleFetchLinks}
|
||||
>
|
||||
Fetch Links
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
|
||||
<Typography sx={{ fontWeight: 500 }}>Scraped Links</Typography>
|
||||
{selectedLinks.length > 0 ? (
|
||||
<StyledButton
|
||||
sx={{ height: 'max-content', width: 'max-content' }}
|
||||
variant='outlined'
|
||||
color='error'
|
||||
title='Clear All Links'
|
||||
onClick={handleRemoveAllLinks}
|
||||
startIcon={<IconEraser />}
|
||||
>
|
||||
Clear All
|
||||
</StyledButton>
|
||||
) : null}
|
||||
</Box>
|
||||
<>
|
||||
{loading && <BackdropLoader open={loading} />}
|
||||
{selectedLinks.length > 0 ? (
|
||||
<PerfectScrollbar
|
||||
style={{
|
||||
height: '100%',
|
||||
maxHeight: '320px',
|
||||
overflowX: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
{selectedLinks.map((link, index) => (
|
||||
<div key={index} style={{ display: 'flex', width: '100%' }}>
|
||||
<Box sx={{ display: 'flex', width: '100%' }}>
|
||||
<OutlinedInput
|
||||
sx={{ width: '100%' }}
|
||||
key={index}
|
||||
type='text'
|
||||
onChange={(e) => handleChangeLink(index, e)}
|
||||
size='small'
|
||||
value={link}
|
||||
name={`link_${index}`}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ width: 'auto', flexGrow: 1 }}>
|
||||
<IconButton
|
||||
sx={{ height: 30, width: 30 }}
|
||||
size='small'
|
||||
color='error'
|
||||
onClick={() => handleRemoveLink(index)}
|
||||
edge='end'
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</div>
|
||||
))}
|
||||
</PerfectScrollbar>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography sx={{ my: 2 }}>Links scraped from the URL will appear here</Typography>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<StyledButton variant='contained' onClick={handleSaveLinks}>
|
||||
Save
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ManageScrapedLinksDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onSave: PropTypes.func
|
||||
}
|
||||
|
||||
export default ManageScrapedLinksDialog
|
||||
@@ -132,6 +132,35 @@ const NodeInfoDialog = ({ show, dialogProps, onCancel }) => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{dialogProps.data.tags &&
|
||||
dialogProps.data.tags.length &&
|
||||
dialogProps.data.tags.map((tag, index) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: 'max-content',
|
||||
borderRadius: 15,
|
||||
background: '#cae9ff',
|
||||
padding: 5,
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
marginTop: 5,
|
||||
marginLeft: 10,
|
||||
marginBottom: 5
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: '#023e7d',
|
||||
fontSize: '0.825rem'
|
||||
}}
|
||||
>
|
||||
{tag.toLowerCase()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import rehypeMathjax from 'rehype-mathjax'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
// MUI
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Chip,
|
||||
Grid,
|
||||
InputLabel,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
Typography,
|
||||
Stack,
|
||||
IconButton,
|
||||
FormControl,
|
||||
Checkbox,
|
||||
MenuItem
|
||||
} from '@mui/material'
|
||||
import MuiAccordion from '@mui/material/Accordion'
|
||||
import MuiAccordionSummary from '@mui/material/AccordionSummary'
|
||||
import MuiAccordionDetails from '@mui/material/AccordionDetails'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'
|
||||
import ClearIcon from '@mui/icons-material/Clear'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
//Project Import
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
|
||||
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
|
||||
import promptEmptySVG from 'assets/images/prompt_empty.svg'
|
||||
|
||||
import useApi from 'hooks/useApi'
|
||||
import promptApi from 'api/prompt'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
|
||||
const NewLineToBr = ({ children = '' }) => {
|
||||
return children.split('\n').reduce(function (arr, line) {
|
||||
return arr.concat(line, <br />)
|
||||
}, [])
|
||||
}
|
||||
|
||||
const Accordion = styled((props) => <MuiAccordion disableGutters elevation={0} square {...props} />)(({ theme }) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
'&:not(:last-child)': {
|
||||
borderBottom: 0
|
||||
},
|
||||
'&:before': {
|
||||
display: 'none'
|
||||
}
|
||||
}))
|
||||
|
||||
const AccordionSummary = styled((props) => (
|
||||
<MuiAccordionSummary expandIcon={<ArrowForwardIosSharpIcon sx={{ fontSize: '0.9rem' }} />} {...props} />
|
||||
))(({ theme }) => ({
|
||||
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, .05)' : 'rgba(0, 0, 0, .03)',
|
||||
flexDirection: 'row-reverse',
|
||||
'& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
|
||||
transform: 'rotate(180deg)'
|
||||
},
|
||||
'& .MuiAccordionSummary-content': {
|
||||
marginLeft: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
|
||||
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
borderTop: '1px solid rgba(0, 0, 0, .125)'
|
||||
}))
|
||||
|
||||
const PromptLangsmithHubDialog = ({ promptType, show, onCancel, onSubmit }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const getAvailablePromptsApi = useApi(promptApi.getAvailablePrompts)
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
} else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [show, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (promptType && show) {
|
||||
setLoading(true)
|
||||
getAvailablePromptsApi.request({ tags: promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&' })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [promptType, show])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAvailablePromptsApi.data && getAvailablePromptsApi.data.repos) {
|
||||
setAvailablePrompNameList(getAvailablePromptsApi.data.repos)
|
||||
if (getAvailablePromptsApi.data.repos?.length) handleListItemClick(0, getAvailablePromptsApi.data.repos)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAvailablePromptsApi.data])
|
||||
|
||||
const ITEM_HEIGHT = 48
|
||||
const ITEM_PADDING_TOP = 8
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 250
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const models = [
|
||||
{ id: 101, name: 'anthropic:claude-instant-1' },
|
||||
{ id: 102, name: 'anthropic:claude-instant-1.2' },
|
||||
{ id: 103, name: 'anthropic:claude-2' },
|
||||
{ id: 104, name: 'google:palm-2-chat-bison' },
|
||||
{ id: 105, name: 'google:palm-2-codechat-bison' },
|
||||
{ id: 106, name: 'google:palm-2-text-bison' },
|
||||
{ id: 107, name: 'meta:llama-2-13b-chat' },
|
||||
{ id: 108, name: 'meta:llama-2-70b-chat' },
|
||||
{ id: 109, name: 'openai:gpt-3.5-turbo' },
|
||||
{ id: 110, name: 'openai:gpt-4' },
|
||||
{ id: 111, name: 'openai:text-davinci-003' }
|
||||
]
|
||||
const [modelName, setModelName] = useState([])
|
||||
|
||||
const usecases = [
|
||||
{ id: 201, name: 'Agents' },
|
||||
{ id: 202, name: 'Agent Stimulation' },
|
||||
{ id: 203, name: 'Autonomous agents' },
|
||||
{ id: 204, name: 'Classification' },
|
||||
{ id: 205, name: 'Chatbots' },
|
||||
{ id: 206, name: 'Code understanding' },
|
||||
{ id: 207, name: 'Code writing' },
|
||||
{ id: 208, name: 'Evaluation' },
|
||||
{ id: 209, name: 'Extraction' },
|
||||
{ id: 210, name: 'Interacting with APIs' },
|
||||
{ id: 211, name: 'Multi-modal' },
|
||||
{ id: 212, name: 'QA over documents' },
|
||||
{ id: 213, name: 'Self-checking' },
|
||||
{ id: 214, name: 'SQL' },
|
||||
{ id: 215, name: 'Summarization' },
|
||||
{ id: 216, name: 'Tagging' }
|
||||
]
|
||||
const [usecase, setUsecase] = useState([])
|
||||
|
||||
const languages = [
|
||||
{ id: 301, name: 'Chinese' },
|
||||
{ id: 302, name: 'English' },
|
||||
{ id: 303, name: 'French' },
|
||||
{ id: 304, name: 'German' },
|
||||
{ id: 305, name: 'Russian' },
|
||||
{ id: 306, name: 'Spanish' }
|
||||
]
|
||||
const [language, setLanguage] = useState([])
|
||||
const [availablePrompNameList, setAvailablePrompNameList] = useState([])
|
||||
const [selectedPrompt, setSelectedPrompt] = useState({})
|
||||
|
||||
const [accordionExpanded, setAccordionExpanded] = useState(['prompt'])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleAccordionChange = (accordionName) => (event, isExpanded) => {
|
||||
const accordians = [...accordionExpanded]
|
||||
if (!isExpanded) setAccordionExpanded(accordians.filter((accr) => accr !== accordionName))
|
||||
else {
|
||||
accordians.push(accordionName)
|
||||
setAccordionExpanded(accordians)
|
||||
}
|
||||
}
|
||||
|
||||
const handleListItemClick = async (index, overridePromptNameList = []) => {
|
||||
const prompt = overridePromptNameList.length ? overridePromptNameList[index] : availablePrompNameList[index]
|
||||
|
||||
if (!prompt.detailed) {
|
||||
const createResp = await promptApi.getPrompt({
|
||||
promptName: prompt.full_name
|
||||
})
|
||||
if (createResp.data) {
|
||||
prompt.detailed = createResp.data.templates
|
||||
}
|
||||
}
|
||||
setSelectedPrompt(prompt)
|
||||
}
|
||||
|
||||
const fetchPrompts = async () => {
|
||||
let tags = promptType === 'template' ? 'StringPromptTemplate&' : 'ChatPromptTemplate&'
|
||||
modelName.forEach((item) => {
|
||||
tags += `tags=${item.name}&`
|
||||
})
|
||||
usecase.forEach((item) => {
|
||||
tags += `tags=${item.name}&`
|
||||
})
|
||||
language.forEach((item) => {
|
||||
tags += `tags=${item.name}&`
|
||||
})
|
||||
setLoading(true)
|
||||
getAvailablePromptsApi.request({ tags: tags })
|
||||
}
|
||||
|
||||
const removeDuplicates = (value) => {
|
||||
let duplicateRemoved = []
|
||||
|
||||
value.forEach((item) => {
|
||||
if (value.filter((o) => o.id === item.id).length === 1) {
|
||||
duplicateRemoved.push(item)
|
||||
}
|
||||
})
|
||||
return duplicateRemoved
|
||||
}
|
||||
|
||||
const handleModelChange = (event) => {
|
||||
const {
|
||||
target: { value }
|
||||
} = event
|
||||
|
||||
setModelName(removeDuplicates(value))
|
||||
}
|
||||
|
||||
const handleUsecaseChange = (event) => {
|
||||
const {
|
||||
target: { value }
|
||||
} = event
|
||||
|
||||
setUsecase(removeDuplicates(value))
|
||||
}
|
||||
const handleLanguageChange = (event) => {
|
||||
const {
|
||||
target: { value }
|
||||
} = event
|
||||
|
||||
setLanguage(removeDuplicates(value))
|
||||
}
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth={'lg'}
|
||||
aria-labelledby='prompt-dialog-title'
|
||||
aria-describedby='prompt-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='prompt-dialog-title'>
|
||||
Langchain Hub ({promptType === 'template' ? 'PromptTemplate' : 'ChatPromptTemplate'})
|
||||
</DialogTitle>
|
||||
<DialogContent dividers sx={{ p: 1 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', p: 2, pt: 1, alignItems: 'center' }}>
|
||||
<FormControl sx={{ mr: 1, width: '30%' }}>
|
||||
<InputLabel size='small' id='model-checkbox-label'>
|
||||
Model
|
||||
</InputLabel>
|
||||
<Select
|
||||
id='model-checkbox'
|
||||
labelId='model-checkbox-label'
|
||||
multiple
|
||||
size='small'
|
||||
value={modelName}
|
||||
onChange={handleModelChange}
|
||||
input={<OutlinedInput label='Model' />}
|
||||
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
|
||||
endAdornment={
|
||||
modelName.length ? (
|
||||
<IconButton sx={{ mr: 2 }} onClick={() => setModelName([])}>
|
||||
<ClearIcon style={{ width: 20, height: 20 }} />
|
||||
</IconButton>
|
||||
) : (
|
||||
false
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
'.MuiSvgIcon-root ': {
|
||||
fill: customization.isDarkMode ? 'white !important' : ''
|
||||
}
|
||||
}}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{models.map((variant) => (
|
||||
<MenuItem key={variant.id} value={variant}>
|
||||
<Checkbox id={variant.id} checked={modelName.findIndex((item) => item.id === variant.id) >= 0} />
|
||||
<ListItemText primary={variant.name} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl sx={{ mr: 1, width: '30%' }}>
|
||||
<InputLabel size='small' id='usecase-checkbox-label'>
|
||||
Usecase
|
||||
</InputLabel>
|
||||
<Select
|
||||
autoWidth={false}
|
||||
labelId='usecase-checkbox-label'
|
||||
id='usecase-checkbox'
|
||||
multiple
|
||||
size='small'
|
||||
value={usecase}
|
||||
onChange={handleUsecaseChange}
|
||||
input={<OutlinedInput label='Usecase' />}
|
||||
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
|
||||
endAdornment={
|
||||
usecase.length ? (
|
||||
<IconButton sx={{ mr: 2 }} onClick={() => setUsecase([])}>
|
||||
<ClearIcon style={{ width: 20, height: 20 }} />
|
||||
</IconButton>
|
||||
) : (
|
||||
false
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
'.MuiSvgIcon-root ': {
|
||||
fill: customization.isDarkMode ? 'white !important' : ''
|
||||
}
|
||||
}}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{usecases.map((variant) => (
|
||||
<MenuItem key={variant.id} value={variant}>
|
||||
<Checkbox id={variant.id} checked={usecase.findIndex((item) => item.id === variant.id) >= 0} />
|
||||
<ListItemText primary={variant.name} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl sx={{ mr: 1, width: '30%' }}>
|
||||
<InputLabel size='small' id='language-checkbox-label'>
|
||||
Language
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId='language-checkbox-label'
|
||||
id='language-checkbox'
|
||||
multiple
|
||||
size='small'
|
||||
value={language}
|
||||
onChange={handleLanguageChange}
|
||||
input={<OutlinedInput label='language' />}
|
||||
renderValue={(selected) => selected.map((x) => x.name).join(', ')}
|
||||
endAdornment={
|
||||
language.length ? (
|
||||
<IconButton sx={{ mr: 2 }} onClick={() => setLanguage([])}>
|
||||
<ClearIcon style={{ width: 20, height: 20 }} />
|
||||
</IconButton>
|
||||
) : (
|
||||
false
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
'.MuiSvgIcon-root ': {
|
||||
fill: customization.isDarkMode ? 'white !important' : ''
|
||||
}
|
||||
}}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{languages.map((variant) => (
|
||||
<MenuItem key={variant.id} value={variant}>
|
||||
<Checkbox id={variant.id} checked={language.findIndex((item) => item.id === variant.id) >= 0} />
|
||||
<ListItemText primary={variant.name} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl sx={{ width: '10%' }}>
|
||||
<Button disableElevation variant='outlined' onClick={fetchPrompts}>
|
||||
Search
|
||||
</Button>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{loading && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%', pb: 3 }} flexDirection='column'>
|
||||
<Box sx={{ p: 5, height: 'auto' }}>
|
||||
<img style={{ objectFit: 'cover', height: '20vh', width: 'auto' }} src={promptEmptySVG} alt='promptEmptySVG' />
|
||||
</Box>
|
||||
<div>Please wait....loading Prompts</div>
|
||||
</Stack>
|
||||
)}
|
||||
{!loading && availablePrompNameList && availablePrompNameList.length === 0 && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%', pb: 3 }} flexDirection='column'>
|
||||
<Box sx={{ p: 5, height: 'auto' }}>
|
||||
<img style={{ objectFit: 'cover', height: '20vh', width: 'auto' }} src={promptEmptySVG} alt='promptEmptySVG' />
|
||||
</Box>
|
||||
<div>No Available Prompts</div>
|
||||
</Stack>
|
||||
)}
|
||||
{!loading && availablePrompNameList && availablePrompNameList.length > 0 && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
|
||||
<Box sx={{ width: '100%', p: 2 }}>
|
||||
<Grid xs={12} container spacing={1} justifyContent='center' alignItems='center'>
|
||||
<Grid xs={4} item sx={{ textAlign: 'left' }}>
|
||||
<Box sx={{ width: '100%', maxWidth: 360 }}>
|
||||
<Card variant='outlined' sx={{ height: 470, overflow: 'auto', borderRadius: 0 }}>
|
||||
<CardContent sx={{ p: 1 }}>
|
||||
<Typography sx={{ fontSize: 10 }} color='text.secondary' gutterBottom>
|
||||
Available Prompts
|
||||
</Typography>
|
||||
<List component='nav' aria-label='secondary mailbox folder'>
|
||||
{availablePrompNameList.map((item, index) => (
|
||||
<ListItemButton
|
||||
key={item.id}
|
||||
selected={item.id === selectedPrompt?.id}
|
||||
onClick={() => handleListItemClick(index)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography sx={{ fontSize: 16, p: 1, fontWeight: 500 }}>
|
||||
{item.full_name}
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: 5
|
||||
}}
|
||||
>
|
||||
{item.tags.map((tag, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={tag}
|
||||
style={{ marginRight: 5, marginBottom: 5 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid xs={8} item sx={{ textAlign: 'left' }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Card sx={{ height: 470, overflow: 'auto' }}>
|
||||
<CardContent sx={{ p: 0.5 }}>
|
||||
<Accordion
|
||||
expanded={accordionExpanded.includes('prompt')}
|
||||
onChange={handleAccordionChange('prompt')}
|
||||
>
|
||||
<AccordionSummary
|
||||
aria-controls='panel2d-content'
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
id='panel2d-header'
|
||||
>
|
||||
<Typography>Prompt</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography sx={{ wordWrap: 'true' }} color='text.primary'>
|
||||
{selectedPrompt?.detailed?.map((item) => (
|
||||
<>
|
||||
<Typography sx={{ fontSize: 12 }} color='text.secondary' gutterBottom>
|
||||
{item.typeDisplay.toUpperCase()}
|
||||
</Typography>
|
||||
<Typography>
|
||||
<p
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap -moz-pre-wrap -pre-wrap -o-pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
fontFamily: 'inherit',
|
||||
wordSpacing: '0.1rem',
|
||||
lineHeight: '1.5rem'
|
||||
}}
|
||||
>
|
||||
<NewLineToBr>{item.template}</NewLineToBr>
|
||||
</p>
|
||||
</Typography>
|
||||
</>
|
||||
))}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
expanded={accordionExpanded.includes('description')}
|
||||
onChange={handleAccordionChange('description')}
|
||||
>
|
||||
<AccordionSummary
|
||||
aria-controls='panel1d-content'
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
id='panel1d-header'
|
||||
>
|
||||
<Typography>Description</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography
|
||||
sx={{ wordWrap: 'true', wordSpacing: '0.1rem', lineHeight: '1.5rem' }}
|
||||
color='text.primary'
|
||||
>
|
||||
{selectedPrompt?.description}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
expanded={accordionExpanded.includes('readme')}
|
||||
onChange={handleAccordionChange('readme')}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls='panel3d-content'
|
||||
id='panel3d-header'
|
||||
>
|
||||
<Typography>Readme</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<div
|
||||
style={{
|
||||
lineHeight: 1.75,
|
||||
'& a': {
|
||||
display: 'block',
|
||||
marginRight: '2.5rem',
|
||||
wordWrap: 'break-word',
|
||||
color: '#16bed7',
|
||||
fontWeight: 500
|
||||
},
|
||||
'& a:hover': { opacity: 0.8 },
|
||||
'& code': {
|
||||
color: '#0ab126',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'pre-wrap !important'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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()}
|
||||
isDialog={true}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedPrompt?.readme}
|
||||
</MemoizedReactMarkdown>
|
||||
</div>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</DialogContent>
|
||||
{availablePrompNameList && availablePrompNameList.length > 0 && (
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<StyledButton
|
||||
disabled={!selectedPrompt?.detailed}
|
||||
onClick={() => onSubmit(selectedPrompt.detailed)}
|
||||
variant='contained'
|
||||
>
|
||||
Load
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
PromptLangsmithHubDialog.propTypes = {
|
||||
promptType: PropTypes.string,
|
||||
show: PropTypes.bool,
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func
|
||||
}
|
||||
|
||||
export default PromptLangsmithHubDialog
|
||||
@@ -0,0 +1,348 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
FormControl,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Select
|
||||
} from '@mui/material'
|
||||
import { IconX } from '@tabler/icons'
|
||||
|
||||
// Project import
|
||||
import CredentialInputHandler from 'views/canvas/CredentialInputHandler'
|
||||
import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser'
|
||||
import { SwitchInput } from 'ui-component/switch/Switch'
|
||||
import { Input } from 'ui-component/input/Input'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
import { Dropdown } from 'ui-component/dropdown/Dropdown'
|
||||
import openAISVG from 'assets/images/openai.svg'
|
||||
import assemblyAIPng from 'assets/images/assemblyai.png'
|
||||
|
||||
// store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
// API
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
|
||||
const speechToTextProviders = {
|
||||
openAIWhisper: {
|
||||
label: 'OpenAI Whisper',
|
||||
name: 'openAIWhisper',
|
||||
icon: openAISVG,
|
||||
url: 'https://platform.openai.com/docs/guides/speech-to-text',
|
||||
inputs: [
|
||||
{
|
||||
label: 'Connect Credential',
|
||||
name: 'credential',
|
||||
type: 'credential',
|
||||
credentialNames: ['openAIApi']
|
||||
},
|
||||
{
|
||||
label: 'Language',
|
||||
name: 'language',
|
||||
type: 'string',
|
||||
description:
|
||||
'The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency.',
|
||||
placeholder: 'en',
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Prompt',
|
||||
name: 'prompt',
|
||||
type: 'string',
|
||||
rows: 4,
|
||||
description: `An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language.`,
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Temperature',
|
||||
name: 'temperature',
|
||||
type: 'number',
|
||||
step: 0.1,
|
||||
description: `The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.`,
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
},
|
||||
assemblyAiTranscribe: {
|
||||
label: 'Assembly AI',
|
||||
name: 'assemblyAiTranscribe',
|
||||
icon: assemblyAIPng,
|
||||
url: 'https://www.assemblyai.com/',
|
||||
inputs: [
|
||||
{
|
||||
label: 'Connect Credential',
|
||||
name: 'credential',
|
||||
type: 'credential',
|
||||
credentialNames: ['assemblyAIApi']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [speechToText, setSpeechToText] = useState({})
|
||||
const [selectedProvider, setSelectedProvider] = useState('none')
|
||||
|
||||
const onSave = async () => {
|
||||
const speechToText = setValue(true, selectedProvider, 'status')
|
||||
try {
|
||||
const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, {
|
||||
speechToText: JSON.stringify(speechToText)
|
||||
})
|
||||
if (saveResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Speech To Text Configuration Saved',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
|
||||
}
|
||||
onCancel()
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to save Speech To Text Configuration: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const setValue = (value, providerName, inputParamName) => {
|
||||
let newVal = {}
|
||||
if (!Object.prototype.hasOwnProperty.call(speechToText, providerName)) {
|
||||
newVal = { ...speechToText, [providerName]: {} }
|
||||
} else {
|
||||
newVal = { ...speechToText }
|
||||
}
|
||||
|
||||
newVal[providerName][inputParamName] = value
|
||||
if (inputParamName === 'status' && value === true) {
|
||||
// ensure that the others are turned off
|
||||
Object.keys(speechToTextProviders).forEach((key) => {
|
||||
const provider = speechToTextProviders[key]
|
||||
if (provider.name !== providerName) {
|
||||
newVal[provider.name] = { ...speechToText[provider.name], status: false }
|
||||
}
|
||||
})
|
||||
}
|
||||
setSpeechToText(newVal)
|
||||
return newVal
|
||||
}
|
||||
|
||||
const handleProviderChange = (event) => {
|
||||
setSelectedProvider(event.target.value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.chatflow && dialogProps.chatflow.speechToText) {
|
||||
try {
|
||||
const speechToText = JSON.parse(dialogProps.chatflow.speechToText)
|
||||
let selectedProvider = 'none'
|
||||
Object.keys(speechToTextProviders).forEach((key) => {
|
||||
const providerConfig = speechToText[key]
|
||||
if (providerConfig && providerConfig.status) {
|
||||
selectedProvider = key
|
||||
}
|
||||
})
|
||||
setSelectedProvider(selectedProvider)
|
||||
setSpeechToText(speechToText)
|
||||
} catch (e) {
|
||||
setSpeechToText({})
|
||||
setSelectedProvider('none')
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
setSpeechToText({})
|
||||
setSelectedProvider('none')
|
||||
}
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const component = (
|
||||
<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'>
|
||||
Speech To Text Configuration
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box fullWidth sx={{ my: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography>Speech To Text Providers</Typography>
|
||||
<FormControl fullWidth>
|
||||
<Select value={selectedProvider} onChange={handleProviderChange}>
|
||||
<MenuItem value='none'>None</MenuItem>
|
||||
<MenuItem value='openAIWhisper'>OpenAI Whisper</MenuItem>
|
||||
<MenuItem value='assemblyAiTranscribe'>Assembly AI</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{selectedProvider !== 'none' && (
|
||||
<>
|
||||
<ListItem style={{ padding: 0, margin: 0 }} alignItems='center'>
|
||||
<ListItemAvatar>
|
||||
<div
|
||||
style={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: 10,
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
alt='AI'
|
||||
src={speechToTextProviders[selectedProvider].icon}
|
||||
/>
|
||||
</div>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary={speechToTextProviders[selectedProvider].label}
|
||||
secondary={
|
||||
<a target='_blank' rel='noreferrer' href={speechToTextProviders[selectedProvider].url}>
|
||||
{speechToTextProviders[selectedProvider].url}
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
{speechToTextProviders[selectedProvider].inputs.map((inputParam, index) => (
|
||||
<Box key={index} sx={{ p: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Typography>
|
||||
{inputParam.label}
|
||||
{!inputParam.optional && <span style={{ color: 'red' }}> *</span>}
|
||||
{inputParam.description && (
|
||||
<TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
{inputParam.type === 'credential' && (
|
||||
<CredentialInputHandler
|
||||
key={speechToText[selectedProvider]?.credentialId}
|
||||
data={
|
||||
speechToText[selectedProvider]?.credentialId
|
||||
? { credential: speechToText[selectedProvider].credentialId }
|
||||
: {}
|
||||
}
|
||||
inputParam={inputParam}
|
||||
onSelect={(newValue) => setValue(newValue, selectedProvider, 'credentialId')}
|
||||
/>
|
||||
)}
|
||||
{inputParam.type === 'boolean' && (
|
||||
<SwitchInput
|
||||
onChange={(newValue) => setValue(newValue, selectedProvider, inputParam.name)}
|
||||
value={
|
||||
speechToText[selectedProvider]
|
||||
? speechToText[selectedProvider][inputParam.name]
|
||||
: inputParam.default ?? false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
|
||||
<Input
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => setValue(newValue, selectedProvider, inputParam.name)}
|
||||
value={
|
||||
speechToText[selectedProvider]
|
||||
? speechToText[selectedProvider][inputParam.name]
|
||||
: inputParam.default ?? ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputParam.type === 'options' && (
|
||||
<Dropdown
|
||||
name={inputParam.name}
|
||||
options={inputParam.options}
|
||||
onSelect={(newValue) => setValue(newValue, selectedProvider, inputParam.name)}
|
||||
value={
|
||||
speechToText[selectedProvider]
|
||||
? speechToText[selectedProvider][inputParam.name]
|
||||
: inputParam.default ?? 'choose an option'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<StyledButton
|
||||
disabled={selectedProvider !== 'none' && !speechToText[selectedProvider]?.credentialId}
|
||||
variant='contained'
|
||||
onClick={onSave}
|
||||
>
|
||||
Save
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
SpeechToTextDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func
|
||||
}
|
||||
|
||||
export default SpeechToTextDialog
|
||||
@@ -0,0 +1,252 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from 'store/actions'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
OutlinedInput,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
Box,
|
||||
List,
|
||||
InputAdornment
|
||||
} from '@mui/material'
|
||||
import { IconX, IconTrash, IconPlus, IconBulb } from '@tabler/icons'
|
||||
|
||||
// Project import
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
|
||||
// store
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
|
||||
import useNotifier from 'utils/useNotifier'
|
||||
|
||||
// API
|
||||
import chatflowsApi from 'api/chatflows'
|
||||
|
||||
const StarterPromptsDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useNotifier()
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const [inputFields, setInputFields] = useState([
|
||||
{
|
||||
prompt: ''
|
||||
}
|
||||
])
|
||||
|
||||
const [chatbotConfig, setChatbotConfig] = useState({})
|
||||
|
||||
const addInputField = () => {
|
||||
setInputFields([
|
||||
...inputFields,
|
||||
{
|
||||
prompt: ''
|
||||
}
|
||||
])
|
||||
}
|
||||
const removeInputFields = (index) => {
|
||||
const rows = [...inputFields]
|
||||
rows.splice(index, 1)
|
||||
setInputFields(rows)
|
||||
}
|
||||
|
||||
const handleChange = (index, evnt) => {
|
||||
const { name, value } = evnt.target
|
||||
const list = [...inputFields]
|
||||
list[index][name] = value
|
||||
setInputFields(list)
|
||||
}
|
||||
|
||||
const onSave = async () => {
|
||||
try {
|
||||
let value = {
|
||||
starterPrompts: {
|
||||
...inputFields
|
||||
}
|
||||
}
|
||||
chatbotConfig.starterPrompts = value.starterPrompts
|
||||
const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, {
|
||||
chatbotConfig: JSON.stringify(chatbotConfig)
|
||||
})
|
||||
if (saveResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Conversation Starter Prompts Saved',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
|
||||
}
|
||||
onConfirm()
|
||||
} catch (error) {
|
||||
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
|
||||
enqueueSnackbar({
|
||||
message: `Failed to save Conversation Starter Prompts: ${errorData}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) {
|
||||
try {
|
||||
let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig)
|
||||
setChatbotConfig(chatbotConfig || {})
|
||||
if (chatbotConfig.starterPrompts) {
|
||||
let inputFields = []
|
||||
Object.getOwnPropertyNames(chatbotConfig.starterPrompts).forEach((key) => {
|
||||
if (chatbotConfig.starterPrompts[key]) {
|
||||
inputFields.push(chatbotConfig.starterPrompts[key])
|
||||
}
|
||||
})
|
||||
setInputFields(inputFields)
|
||||
} else {
|
||||
setInputFields([
|
||||
{
|
||||
prompt: ''
|
||||
}
|
||||
])
|
||||
}
|
||||
} catch (e) {
|
||||
setInputFields([
|
||||
{
|
||||
prompt: ''
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return () => {}
|
||||
}, [dialogProps])
|
||||
|
||||
useEffect(() => {
|
||||
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
|
||||
else dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
|
||||
}, [show, dispatch])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{dialogProps.title || 'Conversation Starter Prompts'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 10,
|
||||
background: '#d8f3dc',
|
||||
padding: 10
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<IconBulb size={30} color='#2d6a4f' />
|
||||
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>
|
||||
Starter prompts will only be shown when there is no messages on the chat
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Box sx={{ '& > :not(style)': { m: 1 }, pt: 2 }}>
|
||||
<List>
|
||||
{inputFields.map((data, index) => {
|
||||
return (
|
||||
<div key={index} style={{ display: 'flex', width: '100%' }}>
|
||||
<Box sx={{ width: '95%', mb: 1 }}>
|
||||
<OutlinedInput
|
||||
sx={{ width: '100%' }}
|
||||
key={index}
|
||||
type='text'
|
||||
onChange={(e) => handleChange(index, e)}
|
||||
size='small'
|
||||
value={data.prompt}
|
||||
name='prompt'
|
||||
endAdornment={
|
||||
<InputAdornment position='end' sx={{ padding: '2px' }}>
|
||||
{inputFields.length > 1 && (
|
||||
<IconButton
|
||||
sx={{ height: 30, width: 30 }}
|
||||
size='small'
|
||||
color='error'
|
||||
disabled={inputFields.length === 1}
|
||||
onClick={() => removeInputFields(index)}
|
||||
edge='end'
|
||||
>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
)}
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ width: '5%', mb: 1 }}>
|
||||
{index === inputFields.length - 1 && (
|
||||
<IconButton color='primary' onClick={addInputField}>
|
||||
<IconPlus />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<StyledButton variant='contained' onClick={onSave}>
|
||||
Save
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
StarterPromptsDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default StarterPromptsDialog
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
DialogTitle,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Chip
|
||||
Chip,
|
||||
Card,
|
||||
CardMedia
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import DatePicker from 'react-datepicker'
|
||||
@@ -47,7 +49,7 @@ import useApi from '@/hooks/useApi'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
|
||||
// Utils
|
||||
import { isValidURL, removeDuplicateURL } from '@/utils/genericHelper'
|
||||
import { getOS, isValidURL, removeDuplicateURL } from '@/utils/genericHelper'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { baseURL } from '@/store/constant'
|
||||
|
||||
@@ -69,6 +71,12 @@ DatePickerCustomInput.propTypes = {
|
||||
onClick: PropTypes.func
|
||||
}
|
||||
|
||||
const messageImageStyle = {
|
||||
width: '128px',
|
||||
height: '128px',
|
||||
objectFit: 'cover'
|
||||
}
|
||||
|
||||
const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
@@ -92,6 +100,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
|
||||
const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow)
|
||||
const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK)
|
||||
const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath)
|
||||
let storagePath = ''
|
||||
|
||||
const onStartDateSelected = (date) => {
|
||||
setStartDate(date)
|
||||
@@ -120,16 +130,35 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const exportMessages = () => {
|
||||
const exportMessages = async () => {
|
||||
if (!storagePath && getStoragePathFromServer.data) {
|
||||
storagePath = getStoragePathFromServer.data.storagePath
|
||||
}
|
||||
const obj = {}
|
||||
let fileSeparator = '/'
|
||||
if ('windows' === getOS()) {
|
||||
fileSeparator = '\\'
|
||||
}
|
||||
for (let i = 0; i < allChatlogs.length; i += 1) {
|
||||
const chatmsg = allChatlogs[i]
|
||||
const chatPK = getChatPK(chatmsg)
|
||||
let filePaths = []
|
||||
if (chatmsg.fileUploads) {
|
||||
chatmsg.fileUploads = JSON.parse(chatmsg.fileUploads)
|
||||
chatmsg.fileUploads.forEach((file) => {
|
||||
if (file.type === 'stored-file') {
|
||||
filePaths.push(
|
||||
`${storagePath}${fileSeparator}${chatmsg.chatflowid}${fileSeparator}${chatmsg.chatId}${fileSeparator}${file.name}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
const msg = {
|
||||
content: chatmsg.content,
|
||||
role: chatmsg.role === 'apiMessage' ? 'bot' : 'user',
|
||||
time: chatmsg.createdDate
|
||||
}
|
||||
if (filePaths.length) msg.filePaths = filePaths
|
||||
if (chatmsg.sourceDocuments) msg.sourceDocuments = JSON.parse(chatmsg.sourceDocuments)
|
||||
if (chatmsg.usedTools) msg.usedTools = JSON.parse(chatmsg.usedTools)
|
||||
if (chatmsg.fileAnnotations) msg.fileAnnotations = JSON.parse(chatmsg.fileAnnotations)
|
||||
@@ -249,6 +278,14 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (chatmsg.fileUploads) {
|
||||
chatmsg.fileUploads = JSON.parse(chatmsg.fileUploads)
|
||||
chatmsg.fileUploads.forEach((file) => {
|
||||
if (file.type === 'stored-file') {
|
||||
file.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatmsg.chatflowid}&chatId=${chatmsg.chatId}&fileName=${file.name}`
|
||||
}
|
||||
})
|
||||
}
|
||||
const obj = {
|
||||
...chatmsg,
|
||||
message: chatmsg.content,
|
||||
@@ -357,6 +394,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (getChatmessageApi.data) {
|
||||
getStoragePathFromServer.request()
|
||||
|
||||
setAllChatLogs(getChatmessageApi.data)
|
||||
const chatPK = processChatLogs(getChatmessageApi.data)
|
||||
setSelectedMessageIndex(0)
|
||||
@@ -593,8 +632,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
sx={{
|
||||
background:
|
||||
message.type === 'apiMessage' ? theme.palette.asyncSelect.main : '',
|
||||
pl: 1,
|
||||
pr: 1
|
||||
py: '1rem',
|
||||
px: '1.5rem'
|
||||
}}
|
||||
key={index}
|
||||
style={{ display: 'flex', justifyContent: 'center', alignContent: 'center' }}
|
||||
@@ -644,6 +683,51 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{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
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import CodeMirror from '@uiw/react-codemirror'
|
||||
import { javascript } from '@codemirror/lang-javascript'
|
||||
import { json } from '@codemirror/lang-json'
|
||||
import { vscodeDark } from '@uiw/codemirror-theme-vscode'
|
||||
import { sublime } from '@uiw/codemirror-theme-sublime'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
|
||||
export const CodeEditor = ({ value, height, theme, lang, placeholder, disabled = false, basicSetup = {}, onValueChange }) => {
|
||||
const customStyle = EditorView.baseTheme({
|
||||
'&': {
|
||||
color: '#191b1f',
|
||||
padding: '10px'
|
||||
},
|
||||
'.cm-placeholder': {
|
||||
color: 'rgba(120, 120, 120, 0.5)'
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<CodeMirror
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
height={height ?? 'calc(100vh - 220px)'}
|
||||
theme={theme === 'dark' ? (lang === 'js' ? vscodeDark : sublime) : 'none'}
|
||||
extensions={
|
||||
lang === 'js'
|
||||
? [javascript({ jsx: true }), EditorView.lineWrapping, customStyle]
|
||||
: [json(), EditorView.lineWrapping, customStyle]
|
||||
}
|
||||
onChange={onValueChange}
|
||||
readOnly={disabled}
|
||||
editable={!disabled}
|
||||
basicSetup={basicSetup}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
CodeEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
theme: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
basicSetup: PropTypes.object,
|
||||
onValueChange: PropTypes.func
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import { highlight, languages } from 'prismjs/components/prism-core'
|
||||
import 'prismjs/components/prism-clike'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-json'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import './prism-dark.css'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Editor
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
|
||||
padding={10}
|
||||
onValueChange={onValueChange}
|
||||
onMouseUp={onMouseUp}
|
||||
onBlur={onBlur}
|
||||
tabSize={4}
|
||||
style={{
|
||||
...style,
|
||||
background: theme.palette.codeEditor.main
|
||||
}}
|
||||
textareaClassName='editor__textarea'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
DarkCodeEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
onValueChange: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onBlur: PropTypes.func
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import { highlight, languages } from 'prismjs/components/prism-core'
|
||||
import 'prismjs/components/prism-clike'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-json'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import './prism-light.css'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
export const LightCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Editor
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
highlight={(code) => highlight(code, type === 'json' ? languages.json : languages.js)}
|
||||
padding={10}
|
||||
onValueChange={onValueChange}
|
||||
onMouseUp={onMouseUp}
|
||||
onBlur={onBlur}
|
||||
tabSize={4}
|
||||
style={{
|
||||
...style,
|
||||
background: theme.palette.card.main
|
||||
}}
|
||||
textareaClassName='editor__textarea'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
LightCodeEditor.propTypes = {
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
onValueChange: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onBlur: PropTypes.func
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
pre[class*='language-'],
|
||||
code[class*='language-'] {
|
||||
color: #d4d4d4;
|
||||
font-size: 13px;
|
||||
text-shadow: none;
|
||||
font-family: Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono', 'Courier New', monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*='language-']::selection,
|
||||
code[class*='language-']::selection,
|
||||
pre[class*='language-'] *::selection,
|
||||
code[class*='language-'] *::selection {
|
||||
text-shadow: none;
|
||||
background: #264f78;
|
||||
}
|
||||
|
||||
@media print {
|
||||
pre[class*='language-'],
|
||||
code[class*='language-'] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.3em;
|
||||
color: #db4c69;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
/*********************************************************
|
||||
* Tokens
|
||||
*/
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.doctype .token.doctype-tag {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.doctype .token.name {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog {
|
||||
color: #6a9955;
|
||||
}
|
||||
|
||||
.token.punctuation,
|
||||
.language-html .language-css .token.punctuation,
|
||||
.language-html .language-javascript .token.punctuation {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.inserted,
|
||||
.token.unit {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.deleted {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.language-css .token.string.url {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.token.operator.arrow {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.atrule {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.token.atrule .token.rule {
|
||||
color: #c586c0;
|
||||
}
|
||||
|
||||
.token.atrule .token.url {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.token.atrule .token.url .token.function {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.token.atrule .token.url .token.punctuation {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.keyword.module,
|
||||
.token.keyword.control-flow {
|
||||
color: #c586c0;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.function .token.maybe-class-name {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
color: #d16969;
|
||||
}
|
||||
|
||||
.token.important {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.constant {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.token.class-name,
|
||||
.token.maybe-class-name {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
.token.console {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.token.parameter {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.token.interpolation {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.token.punctuation.interpolation-punctuation {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.variable,
|
||||
.token.imports .token.maybe-class-name,
|
||||
.token.exports .token.maybe-class-name {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.token.selector {
|
||||
color: #d7ba7d;
|
||||
}
|
||||
|
||||
.token.escape {
|
||||
color: #d7ba7d;
|
||||
}
|
||||
|
||||
.token.tag {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.tag .token.punctuation {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.token.cdata {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.token.attr-name {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.token.attr-value,
|
||||
.token.attr-value .token.punctuation {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.token.attr-value .token.punctuation.attr-equals {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
/*********************************************************
|
||||
* Language Specific
|
||||
*/
|
||||
|
||||
pre[class*='language-javascript'],
|
||||
code[class*='language-javascript'],
|
||||
pre[class*='language-jsx'],
|
||||
code[class*='language-jsx'],
|
||||
pre[class*='language-typescript'],
|
||||
code[class*='language-typescript'],
|
||||
pre[class*='language-tsx'],
|
||||
code[class*='language-tsx'] {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
pre[class*='language-css'],
|
||||
code[class*='language-css'] {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
pre[class*='language-html'],
|
||||
code[class*='language-html'] {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.language-regex .token.anchor {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.language-html .token.punctuation {
|
||||
color: #808080;
|
||||
}
|
||||
/*********************************************************
|
||||
* Line highlighting
|
||||
*/
|
||||
pre[class*='language-'] > code[class*='language-'] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.line-highlight.line-highlight {
|
||||
background: #f7ebc6;
|
||||
box-shadow: inset 5px 0 0 #f7d87c;
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
color: #90a4ae;
|
||||
background: #fafafa;
|
||||
font-family: Roboto Mono, monospace;
|
||||
font-size: 1em;
|
||||
line-height: 1.5em;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
code[class*='language-']::-moz-selection,
|
||||
pre[class*='language-']::-moz-selection,
|
||||
code[class*='language-'] ::-moz-selection,
|
||||
pre[class*='language-'] ::-moz-selection {
|
||||
background: #cceae7;
|
||||
color: #263238;
|
||||
}
|
||||
|
||||
code[class*='language-']::selection,
|
||||
pre[class*='language-']::selection,
|
||||
code[class*='language-'] ::selection,
|
||||
pre[class*='language-'] ::selection {
|
||||
background: #cceae7;
|
||||
color: #263238;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*='language-'] {
|
||||
white-space: normal;
|
||||
border-radius: 0.2em;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
pre[class*='language-'] {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
margin: 0.5em 0;
|
||||
padding: 1.25em 1em;
|
||||
}
|
||||
|
||||
.language-css > code,
|
||||
.language-sass > code,
|
||||
.language-scss > code {
|
||||
color: #f76d47;
|
||||
}
|
||||
|
||||
[class*='language-'] .namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.atrule {
|
||||
color: #7c4dff;
|
||||
}
|
||||
|
||||
.token.attr-name {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.attr-value {
|
||||
color: #f6a434;
|
||||
}
|
||||
|
||||
.token.attribute {
|
||||
color: #f6a434;
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: #7c4dff;
|
||||
}
|
||||
|
||||
.token.builtin {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.cdata {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.char {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.class {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.class-name {
|
||||
color: #6182b8;
|
||||
}
|
||||
|
||||
.token.comment {
|
||||
color: #aabfc9;
|
||||
}
|
||||
|
||||
.token.constant {
|
||||
color: #7c4dff;
|
||||
}
|
||||
|
||||
.token.deleted {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.token.doctype {
|
||||
color: #aabfc9;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #7c4dff;
|
||||
}
|
||||
|
||||
.token.hexcode {
|
||||
color: #f76d47;
|
||||
}
|
||||
|
||||
.token.id {
|
||||
color: #7c4dff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.important {
|
||||
color: #7c4dff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #7c4dff;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #f76d47;
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.prolog {
|
||||
color: #aabfc9;
|
||||
}
|
||||
|
||||
.token.property {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.pseudo-class {
|
||||
color: #f6a434;
|
||||
}
|
||||
|
||||
.token.pseudo-element {
|
||||
color: #f6a434;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #39adb5;
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
color: #6182b8;
|
||||
}
|
||||
|
||||
.token.selector {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.token.string {
|
||||
color: #f6a434;
|
||||
}
|
||||
|
||||
.token.symbol {
|
||||
color: #7c4dff;
|
||||
}
|
||||
|
||||
.token.tag {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.token.unit {
|
||||
color: #f76d47;
|
||||
}
|
||||
|
||||
.token.url {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.token.variable {
|
||||
color: #e53935;
|
||||
}
|
||||
@@ -1,23 +1,10 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { FormControl, OutlinedInput, Popover } from '@mui/material'
|
||||
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
|
||||
import { FormControl, OutlinedInput, InputBase, Popover } from '@mui/material'
|
||||
import SelectVariable from '@/ui-component/json/SelectVariable'
|
||||
import { getAvailableNodesForVariable } from '@/utils/genericHelper'
|
||||
|
||||
export const Input = ({
|
||||
inputParam,
|
||||
value,
|
||||
nodes,
|
||||
edges,
|
||||
nodeId,
|
||||
onChange,
|
||||
disabled = false,
|
||||
showDialog,
|
||||
dialogProps,
|
||||
onDialogCancel,
|
||||
onDialogConfirm
|
||||
}) => {
|
||||
export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => {
|
||||
const [myValue, setMyValue] = useState(value ?? '')
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [availableNodesForVariable, setAvailableNodesForVariable] = useState([])
|
||||
@@ -63,39 +50,66 @@ export const Input = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
|
||||
<OutlinedInput
|
||||
id={inputParam.name}
|
||||
size='small'
|
||||
disabled={disabled}
|
||||
type={getInputType(inputParam.type)}
|
||||
placeholder={inputParam.placeholder}
|
||||
multiline={!!inputParam.rows}
|
||||
rows={inputParam.rows ?? 1}
|
||||
value={myValue}
|
||||
name={inputParam.name}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value)
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
inputProps={{
|
||||
step: inputParam.step ?? 1,
|
||||
style: {
|
||||
height: inputParam.rows ? '90px' : 'inherit'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
{showDialog && (
|
||||
<ExpandTextDialog
|
||||
show={showDialog}
|
||||
dialogProps={dialogProps}
|
||||
onCancel={onDialogCancel}
|
||||
onConfirm={(newValue, inputParamName) => {
|
||||
setMyValue(newValue)
|
||||
onDialogConfirm(newValue, inputParamName)
|
||||
}}
|
||||
></ExpandTextDialog>
|
||||
{inputParam.name === 'note' ? (
|
||||
<FormControl sx={{ width: '100%', height: 'auto' }} size='small'>
|
||||
<InputBase
|
||||
id={nodeId}
|
||||
size='small'
|
||||
disabled={disabled}
|
||||
type={getInputType(inputParam.type)}
|
||||
placeholder={inputParam.placeholder}
|
||||
multiline={!!inputParam.rows}
|
||||
minRows={inputParam.rows ?? 1}
|
||||
value={myValue}
|
||||
name={inputParam.name}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value)
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
inputProps={{
|
||||
step: inputParam.step ?? 1,
|
||||
style: {
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
color: '#212121'
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
padding: '10px 14px',
|
||||
textarea: {
|
||||
'&::placeholder': {
|
||||
color: '#616161'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
) : (
|
||||
<FormControl sx={{ mt: 1, width: '100%' }} size='small'>
|
||||
<OutlinedInput
|
||||
id={inputParam.name}
|
||||
size='small'
|
||||
disabled={disabled}
|
||||
type={getInputType(inputParam.type)}
|
||||
placeholder={inputParam.placeholder}
|
||||
multiline={!!inputParam.rows}
|
||||
rows={inputParam.rows ?? 1}
|
||||
value={myValue}
|
||||
name={inputParam.name}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value)
|
||||
onChange(e.target.value)
|
||||
}}
|
||||
inputProps={{
|
||||
step: inputParam.step ?? 1,
|
||||
style: {
|
||||
height: inputParam.rows ? '90px' : 'inherit'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
<div ref={ref}></div>
|
||||
{inputParam?.acceptVariable && (
|
||||
@@ -131,11 +145,7 @@ Input.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
showDialog: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
nodes: PropTypes.array,
|
||||
edges: PropTypes.array,
|
||||
nodeId: PropTypes.string,
|
||||
onDialogCancel: PropTypes.func,
|
||||
onDialogConfirm: PropTypes.func
|
||||
nodeId: PropTypes.string
|
||||
}
|
||||
|
||||
@@ -141,8 +141,17 @@ const SelectVariable = ({ availableNodesForVariable, disabled = false, onSelectA
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
sx={{ ml: 1 }}
|
||||
primary={node.data.inputs.chainName ? node.data.inputs.chainName : node.data.id}
|
||||
secondary={`${selectedOutputAnchor?.label ?? 'output'} from ${node.data.label}`}
|
||||
primary={
|
||||
node.data.inputs.chainName ??
|
||||
node.data.inputs.functionName ??
|
||||
node.data.inputs.variableName ??
|
||||
node.data.id
|
||||
}
|
||||
secondary={
|
||||
node.data.name === 'ifElseFunction'
|
||||
? `${node.data.description}`
|
||||
: `${selectedOutputAnchor?.label ?? 'output'} from ${node.data.label}`
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
|
||||
@@ -147,8 +147,8 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi })
|
||||
}
|
||||
|
||||
FlowListTable.propTypes = {
|
||||
data: PropTypes.object,
|
||||
images: PropTypes.array,
|
||||
data: PropTypes.array,
|
||||
images: PropTypes.object,
|
||||
filterFunction: PropTypes.func,
|
||||
updateFlowsApi: PropTypes.object
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Table from '@mui/material/Table'
|
||||
import TableBody from '@mui/material/TableBody'
|
||||
import TableCell, { tableCellClasses } from '@mui/material/TableCell'
|
||||
import TableContainer from '@mui/material/TableContainer'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import { Button, Typography } from '@mui/material'
|
||||
|
||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
[`&.${tableCellClasses.head}`]: {
|
||||
backgroundColor: theme.palette.common.black,
|
||||
color: theme.palette.common.white
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: {
|
||||
fontSize: 14
|
||||
}
|
||||
}))
|
||||
|
||||
const StyledTableRow = styled(TableRow)(({ theme }) => ({
|
||||
'&:nth-of-type(odd)': {
|
||||
backgroundColor: theme.palette.action.hover
|
||||
},
|
||||
// hide last border
|
||||
'&:last-child td, &:last-child th': {
|
||||
border: 0
|
||||
}
|
||||
}))
|
||||
|
||||
export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterByType, filterByFramework, goToCanvas, goToTool }) => {
|
||||
const openTemplate = (selectedTemplate) => {
|
||||
if (selectedTemplate.flowData) {
|
||||
goToCanvas(selectedTemplate)
|
||||
} else {
|
||||
goToTool(selectedTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer style={{ marginTop: '30', border: 1 }} component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} size='small' aria-label='a dense table'>
|
||||
<TableHead>
|
||||
<TableRow sx={{ marginTop: '10', backgroundColor: 'primary' }}>
|
||||
<StyledTableCell component='th' scope='row' style={{ width: '15%' }} key='0'>
|
||||
Name
|
||||
</StyledTableCell>
|
||||
<StyledTableCell component='th' scope='row' style={{ width: '5%' }} key='1'>
|
||||
Type
|
||||
</StyledTableCell>
|
||||
<StyledTableCell style={{ width: '35%' }} key='2'>
|
||||
Description
|
||||
</StyledTableCell>
|
||||
<StyledTableCell style={{ width: '35%' }} key='3'>
|
||||
Nodes
|
||||
</StyledTableCell>
|
||||
<StyledTableCell component='th' scope='row' style={{ width: '5%' }} key='4'>
|
||||
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data
|
||||
.filter(filterByBadge)
|
||||
.filter(filterByType)
|
||||
.filter(filterFunction)
|
||||
.filter(filterByFramework)
|
||||
.map((row, index) => (
|
||||
<StyledTableRow key={index}>
|
||||
<TableCell key='0'>
|
||||
<Typography
|
||||
sx={{ fontSize: '1.2rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}
|
||||
>
|
||||
<Button onClick={() => openTemplate(row)} sx={{ textAlign: 'left' }}>
|
||||
{row.templateName || row.name}
|
||||
</Button>
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell key='1'>
|
||||
<Typography>{row.type}</Typography>
|
||||
</TableCell>
|
||||
<TableCell key='2'>
|
||||
<Typography sx={{ overflowWrap: 'break-word', whiteSpace: 'pre-line' }}>
|
||||
{row.description || ''}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell key='3'>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: 5
|
||||
}}
|
||||
>
|
||||
{row.categories &&
|
||||
row.categories
|
||||
.split(',')
|
||||
.map((tag, index) => (
|
||||
<Chip
|
||||
variant='outlined'
|
||||
key={index}
|
||||
size='small'
|
||||
label={tag.toUpperCase()}
|
||||
style={{ marginRight: 3, marginBottom: 3 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell key='4'>
|
||||
<Typography>
|
||||
{row.badge &&
|
||||
row.badge
|
||||
.split(';')
|
||||
.map((tag, index) => (
|
||||
<Chip
|
||||
color={tag === 'POPULAR' ? 'primary' : 'error'}
|
||||
key={index}
|
||||
size='small'
|
||||
label={tag.toUpperCase()}
|
||||
style={{ marginRight: 5, marginBottom: 5 }}
|
||||
/>
|
||||
))}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
MarketplaceTable.propTypes = {
|
||||
data: PropTypes.array,
|
||||
filterFunction: PropTypes.func,
|
||||
filterByBadge: PropTypes.func,
|
||||
filterByType: PropTypes.func,
|
||||
filterByFramework: PropTypes.func,
|
||||
goToTool: PropTypes.func,
|
||||
goToCanvas: PropTypes.func
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip'
|
||||
|
||||
const NodeTooltip = 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]
|
||||
}
|
||||
}))
|
||||
|
||||
export default NodeTooltip
|
||||
@@ -99,6 +99,7 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
id: `${newNodeId}-output-${nodeData.outputs[j].name}-${baseClasses}`,
|
||||
name: nodeData.outputs[j].name,
|
||||
label: nodeData.outputs[j].label,
|
||||
description: nodeData.outputs[j].description ?? '',
|
||||
type
|
||||
}
|
||||
options.push(newOutputOption)
|
||||
@@ -107,6 +108,7 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
name: 'output',
|
||||
label: 'Output',
|
||||
type: 'options',
|
||||
description: nodeData.outputs[0].description ?? '',
|
||||
options,
|
||||
default: nodeData.outputs[0].name
|
||||
}
|
||||
@@ -116,6 +118,7 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
id: `${newNodeId}-output-${nodeData.name}-${nodeData.baseClasses.join('|')}`,
|
||||
name: nodeData.name,
|
||||
label: nodeData.type,
|
||||
description: nodeData.description ?? '',
|
||||
type: nodeData.baseClasses.join(' | ')
|
||||
}
|
||||
outputAnchors.push(newOutput)
|
||||
@@ -182,15 +185,6 @@ export const initNode = (nodeData, newNodeId) => {
|
||||
return nodeData
|
||||
}
|
||||
|
||||
export const getEdgeLabelName = (source) => {
|
||||
const sourceSplit = source.split('-')
|
||||
if (sourceSplit.length && sourceSplit[0].includes('ifElse')) {
|
||||
const outputAnchorsIndex = sourceSplit[sourceSplit.length - 1]
|
||||
return outputAnchorsIndex === '0' ? 'true' : 'false'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export const isValidConnection = (connection, reactFlowInstance) => {
|
||||
const sourceHandle = connection.sourceHandle
|
||||
const targetHandle = connection.targetHandle
|
||||
@@ -286,6 +280,7 @@ export const generateExportFlowData = (flowData) => {
|
||||
name: node.data.name,
|
||||
type: node.data.type,
|
||||
baseClasses: node.data.baseClasses,
|
||||
tags: node.data.tags,
|
||||
category: node.data.category,
|
||||
description: node.data.description,
|
||||
inputParams: node.data.inputParams,
|
||||
@@ -612,3 +607,25 @@ export const getConfigExamplesForCurl = (configData, bodyType, isMultiple, stopN
|
||||
}
|
||||
return finalStr
|
||||
}
|
||||
|
||||
export const getOS = () => {
|
||||
let userAgent = window.navigator.userAgent.toLowerCase(),
|
||||
macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i,
|
||||
windowsPlatforms = /(win32|win64|windows|wince)/i,
|
||||
iosPlatforms = /(iphone|ipad|ipod)/i,
|
||||
os = null
|
||||
|
||||
if (macosPlatforms.test(userAgent)) {
|
||||
os = 'macos'
|
||||
} else if (iosPlatforms.test(userAgent)) {
|
||||
os = 'ios'
|
||||
} else if (windowsPlatforms.test(userAgent)) {
|
||||
os = 'windows'
|
||||
} else if (/android/.test(userAgent)) {
|
||||
os = 'android'
|
||||
} else if (!os && /linux/.test(userAgent)) {
|
||||
os = 'linux'
|
||||
}
|
||||
|
||||
return os
|
||||
}
|
||||
|
||||
@@ -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
|
||||