diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index a976e6a1..3828535e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -24,7 +24,6 @@ import { IChatMessageFeedback, IDepthQueue, INodeDirectedGraph, - ChatMessageRatingType, IUploadFileSizeAndTypes } from './Interface' import { diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 8c7d08b8..9b83aaa4 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -1,16 +1,5 @@ // assets -import { - IconTrash, - IconFileUpload, - IconFileExport, - IconCopy, - IconSearch, - IconMessage, - IconPictureInPictureOff, - IconLink, - IconMicrophone, - IconThumbUp -} from '@tabler/icons' +import { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconMessage, IconAdjustmentsHorizontal } from '@tabler/icons' // constant const icons = { @@ -18,12 +7,8 @@ const icons = { IconFileUpload, IconFileExport, IconCopy, - IconSearch, IconMessage, - IconPictureInPictureOff, - IconLink, - IconMicrophone, - IconThumbUp + IconAdjustmentsHorizontal } // ==============================|| SETTINGS MENU ITEMS ||============================== // @@ -33,13 +18,6 @@ const settings = { title: '', type: 'group', children: [ - { - id: 'conversationStarters', - title: 'Starter Prompts', - type: 'item', - url: '', - icon: icons.IconPictureInPictureOff - }, { id: 'viewMessages', title: 'View Messages', @@ -48,25 +26,11 @@ const settings = { icon: icons.IconMessage }, { - id: 'chatFeedback', - title: 'Chat Feedback', + id: 'chatflowConfiguration', + title: 'Configuration', type: 'item', url: '', - icon: icons.IconThumbUp - }, - { - id: 'allowedDomains', - title: 'Allowed Domains', - type: 'item', - url: '', - icon: icons.IconLink - }, - { - id: 'enableSpeechToText', - title: 'Speech to Text', - type: 'item', - url: '', - icon: icons.IconMicrophone + icon: icons.IconAdjustmentsHorizontal }, { id: 'duplicateChatflow', @@ -89,13 +53,6 @@ const settings = { url: '', icon: icons.IconFileExport }, - { - id: 'analyseChatflow', - title: 'Analyse Chatflow', - type: 'item', - url: '', - icon: icons.IconSearch - }, { id: 'deleteChatflow', title: 'Delete Chatflow', diff --git a/packages/ui/src/ui-component/button/FlowListMenu.jsx b/packages/ui/src/ui-component/button/FlowListMenu.jsx index e8c8786a..bfee4eb1 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.jsx +++ b/packages/ui/src/ui-component/button/FlowListMenu.jsx @@ -12,6 +12,9 @@ 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 ThumbsUpDownOutlinedIcon from '@mui/icons-material/ThumbsUpDownOutlined' +import VpnLockOutlinedIcon from '@mui/icons-material/VpnLockOutlined' +import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined' import Button from '@mui/material/Button' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import { IconX } from '@tabler/icons' @@ -29,6 +32,9 @@ import StarterPromptsDialog from '@/ui-component/dialog/StarterPromptsDialog' import { generateExportFlowData } from '@/utils/genericHelper' import useNotifier from '@/utils/useNotifier' +import ChatFeedbackDialog from '../dialog/ChatFeedbackDialog' +import AllowedDomainsDialog from '../dialog/AllowedDomainsDialog' +import SpeechToTextDialog from '../dialog/SpeechToTextDialog' const StyledMenu = styled((props) => ( { setAnchorEl(event.currentTarget) @@ -105,9 +117,31 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { setConversationStartersDialogOpen(true) } - const saveFlowStarterPrompts = async () => { - setConversationStartersDialogOpen(false) - await updateFlowsApi.request() + const handleFlowChatFeedback = () => { + setAnchorEl(null) + setChatFeedbackDialogProps({ + title: 'Chat Feedback - ' + chatflow.name, + chatflow: chatflow + }) + setChatFeedbackDialogOpen(true) + } + + const handleAllowedDomains = () => { + setAnchorEl(null) + setAllowedDomainsDialogProps({ + title: 'Allowed Domains - ' + chatflow.name, + chatflow: chatflow + }) + setAllowedDomainsDialogOpen(true) + } + + const handleSpeechToText = () => { + setAnchorEl(null) + setSpeechToTextDialogProps({ + title: 'Speech To Text - ' + chatflow.name, + chatflow: chatflow + }) + setSpeechToTextDialogOpen(true) } const saveFlowRename = async (chatflowName) => { @@ -275,6 +309,18 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { Starter Prompts + + + Chat Feedback + + + + Allowed Domains + + + + Speech To Text + Update Category @@ -304,9 +350,23 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { setConversationStartersDialogOpen(false)} /> + setChatFeedbackDialogOpen(false)} + /> + setAllowedDomainsDialogOpen(false)} + /> + setSpeechToTextDialogOpen(false)} + /> ) } diff --git a/packages/ui/src/ui-component/dialog/AllowedDomainsDialog.jsx b/packages/ui/src/ui-component/dialog/AllowedDomainsDialog.jsx index a5ed317a..6147c173 100644 --- a/packages/ui/src/ui-component/dialog/AllowedDomainsDialog.jsx +++ b/packages/ui/src/ui-component/dialog/AllowedDomainsDialog.jsx @@ -1,130 +1,24 @@ import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' -import { useState, useEffect } from 'react' +import { useEffect } from 'react' import PropTypes from 'prop-types' // material-ui -import { - Button, - IconButton, - Dialog, - DialogContent, - OutlinedInput, - DialogTitle, - DialogActions, - Box, - List, - InputAdornment -} from '@mui/material' -import { IconX, IconTrash, IconPlus } from '@tabler/icons' - -// Project import -import { StyledButton } from '@/ui-component/button/StyledButton' +import { Dialog, DialogContent, DialogTitle } from '@mui/material' // store -import { - enqueueSnackbar as enqueueSnackbarAction, - closeSnackbar as closeSnackbarAction, - SET_CHATFLOW, - HIDE_CANVAS_DIALOG, - SHOW_CANVAS_DIALOG -} from '@/store/actions' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' import useNotifier from '@/utils/useNotifier' -// API -import chatflowsApi from '@/api/chatflows' +// Project imports +import AllowedDomains from '@/ui-component/extended/AllowedDomains' -const AllowedDomainsDialog = ({ show, dialogProps, onCancel, onConfirm }) => { +const AllowedDomainsDialog = ({ 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 [inputFields, setInputFields] = useState(['']) - - const [chatbotConfig, setChatbotConfig] = useState({}) - - const addInputField = () => { - setInputFields([...inputFields, '']) - } - const removeInputFields = (index) => { - const rows = [...inputFields] - rows.splice(index, 1) - setInputFields(rows) - } - - const handleChange = (index, evnt) => { - const { value } = evnt.target - const list = [...inputFields] - list[index] = value - setInputFields(list) - } - - const onSave = async () => { - try { - let value = { - allowedOrigins: [...inputFields] - } - chatbotConfig.allowedOrigins = value.allowedOrigins - const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { - chatbotConfig: JSON.stringify(chatbotConfig) - }) - if (saveResp.data) { - enqueueSnackbar({ - message: 'Allowed Origins Saved', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - 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 Allowed Origins: ${errorData}`, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ) - } - }) - } - } - - useEffect(() => { - if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { - try { - let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) - setChatbotConfig(chatbotConfig || {}) - if (chatbotConfig.allowedOrigins) { - let inputFields = [...chatbotConfig.allowedOrigins] - setInputFields(inputFields) - } else { - setInputFields(['']) - } - } catch (e) { - setInputFields(['']) - } - } - - return () => {} - }, [dialogProps]) - useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) else dispatch({ type: HIDE_CANVAS_DIALOG }) @@ -141,69 +35,11 @@ const AllowedDomainsDialog = ({ show, dialogProps, onCancel, onConfirm }) => { aria-describedby='alert-dialog-description' > - {dialogProps.title || 'Allowed Origins'} + {dialogProps.title || 'Allowed Domains'} -
- Your chatbot will only work when used from the following domains. -
- :not(style)': { m: 1 }, pt: 2 }}> - - {inputFields.map((origin, index) => { - return ( -
- - handleChange(index, e)} - size='small' - value={origin} - name='origin' - placeholder='https://example.com' - endAdornment={ - - {inputFields.length > 1 && ( - removeInputFields(index)} - edge='end' - > - - - )} - - } - /> - - - {index === inputFields.length - 1 && ( - - - - )} - -
- ) - })} -
-
+
- - - - Save - - ) : null diff --git a/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.jsx b/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.jsx deleted file mode 100644 index 891c2efc..00000000 --- a/packages/ui/src/ui-component/dialog/AnalyseFlowDialog.jsx +++ /dev/null @@ -1,358 +0,0 @@ -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, - Accordion, - AccordionSummary, - AccordionDetails, - ListItem, - ListItemAvatar, - ListItemText -} from '@mui/material' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -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 langsmithPNG from '@/assets/images/langchain.png' -import langfuseSVG from '@/assets/images/langfuse.svg' -import lunarySVG from '@/assets/images/lunary.svg' - -// store -import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' -import useNotifier from '@/utils/useNotifier' - -// API -import chatflowsApi from '@/api/chatflows' - -const analyticProviders = [ - { - label: 'LangSmith', - name: 'langSmith', - icon: langsmithPNG, - url: 'https://smith.langchain.com', - inputs: [ - { - label: 'Connect Credential', - name: 'credential', - type: 'credential', - credentialNames: ['langsmithApi'] - }, - { - label: 'Project Name', - name: 'projectName', - type: 'string', - optional: true, - description: 'If not provided, default will be used', - placeholder: 'default' - }, - { - label: 'On/Off', - name: 'status', - type: 'boolean', - optional: true - } - ] - }, - { - label: 'LangFuse', - name: 'langFuse', - icon: langfuseSVG, - url: 'https://langfuse.com', - inputs: [ - { - label: 'Connect Credential', - name: 'credential', - type: 'credential', - credentialNames: ['langfuseApi'] - }, - { - label: 'Release', - name: 'release', - type: 'string', - optional: true, - description: 'The release number/hash of the application to provide analytics grouped by release' - }, - { - label: 'On/Off', - name: 'status', - type: 'boolean', - optional: true - } - ] - }, - { - label: 'Lunary', - name: 'lunary', - icon: lunarySVG, - url: 'https://lunary.ai', - inputs: [ - { - label: 'Connect Credential', - name: 'credential', - type: 'credential', - credentialNames: ['lunaryApi'] - }, - { - label: 'On/Off', - name: 'status', - type: 'boolean', - optional: true - } - ] - } -] - -const AnalyseFlowDialog = ({ 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 [analytic, setAnalytic] = useState({}) - const [providerExpanded, setProviderExpanded] = useState({}) - - const onSave = async () => { - try { - const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { - analytic: JSON.stringify(analytic) - }) - if (saveResp.data) { - enqueueSnackbar({ - message: 'Analytic Configuration Saved', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - 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 Analytic Configuration: ${errorData}`, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ) - } - }) - } - } - - const setValue = (value, providerName, inputParamName) => { - let newVal = {} - if (!Object.prototype.hasOwnProperty.call(analytic, providerName)) { - newVal = { ...analytic, [providerName]: {} } - } else { - newVal = { ...analytic } - } - - newVal[providerName][inputParamName] = value - setAnalytic(newVal) - } - - const handleAccordionChange = (providerName) => (event, isExpanded) => { - const accordianProviders = { ...providerExpanded } - accordianProviders[providerName] = isExpanded - setProviderExpanded(accordianProviders) - } - - useEffect(() => { - if (dialogProps.chatflow && dialogProps.chatflow.analytic) { - try { - setAnalytic(JSON.parse(dialogProps.chatflow.analytic)) - } catch (e) { - setAnalytic({}) - console.error(e) - } - } - - return () => { - setAnalytic({}) - setProviderExpanded({}) - } - }, [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 ? ( - - - Analyse Chatflow - - - {analyticProviders.map((provider, index) => ( - - } aria-controls={provider.name} id={provider.name}> - - -
- AI -
-
- - {provider.url} - - } - /> - {analytic[provider.name] && analytic[provider.name].status && ( -
-
- ON -
- )} - - - - {provider.inputs.map((inputParam, index) => ( - -
- - {inputParam.label} - {!inputParam.optional &&  *} - {inputParam.description && ( - - )} - -
- {providerExpanded[provider.name] && inputParam.type === 'credential' && ( - setValue(newValue, provider.name, 'credentialId')} - /> - )} - {providerExpanded[provider.name] && inputParam.type === 'boolean' && ( - setValue(newValue, provider.name, inputParam.name)} - value={ - analytic[provider.name] - ? analytic[provider.name][inputParam.name] - : inputParam.default ?? false - } - /> - )} - {providerExpanded[provider.name] && - (inputParam.type === 'string' || - inputParam.type === 'password' || - inputParam.type === 'number') && ( - setValue(newValue, provider.name, inputParam.name)} - value={ - analytic[provider.name] - ? analytic[provider.name][inputParam.name] - : inputParam.default ?? '' - } - /> - )} -
- ))} -
- - ))} - - - - Save - - -
- ) : null - - return createPortal(component, portalElement) -} - -AnalyseFlowDialog.propTypes = { - show: PropTypes.bool, - dialogProps: PropTypes.object, - onCancel: PropTypes.func -} - -export default AnalyseFlowDialog diff --git a/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.jsx b/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.jsx index 54bb4408..a57e8ce4 100644 --- a/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ChatFeedbackDialog.jsx @@ -1,102 +1,24 @@ import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' -import { useState, useEffect } from 'react' +import { useEffect } from 'react' import PropTypes from 'prop-types' // material-ui -import { Button, Dialog, DialogContent, DialogTitle, DialogActions, Box } from '@mui/material' -import { IconX } from '@tabler/icons' - -// Project import -import { StyledButton } from '@/ui-component/button/StyledButton' -import { SwitchInput } from '@/ui-component/switch/Switch' +import { Dialog, DialogContent, DialogTitle } from '@mui/material' // store -import { - enqueueSnackbar as enqueueSnackbarAction, - closeSnackbar as closeSnackbarAction, - SET_CHATFLOW, - HIDE_CANVAS_DIALOG, - SHOW_CANVAS_DIALOG -} from '@/store/actions' +import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' import useNotifier from '@/utils/useNotifier' -// API -import chatflowsApi from '@/api/chatflows' +// Project imports +import ChatFeedback from '@/ui-component/extended/ChatFeedback' -const ChatFeedbackDialog = ({ show, dialogProps, onCancel, onConfirm }) => { +const ChatFeedbackDialog = ({ 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 [chatFeedbackStatus, setChatFeedbackStatus] = useState(false) - const [chatbotConfig, setChatbotConfig] = useState({}) - - const handleChange = (value) => { - setChatFeedbackStatus(value) - } - - const onSave = async () => { - try { - let value = { - chatFeedback: { - status: chatFeedbackStatus - } - } - chatbotConfig.chatFeedback = value.chatFeedback - const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { - chatbotConfig: JSON.stringify(chatbotConfig) - }) - if (saveResp.data) { - enqueueSnackbar({ - message: 'Chat Feedback Settings Saved', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - 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 Chat Feedback Settings: ${errorData}`, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ) - } - }) - } - } - - useEffect(() => { - if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { - let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) - setChatbotConfig(chatbotConfig || {}) - if (chatbotConfig.chatFeedback) { - setChatFeedbackStatus(chatbotConfig.chatFeedback.status) - } - } - - return () => {} - }, [dialogProps]) - useEffect(() => { if (show) dispatch({ type: SHOW_CANVAS_DIALOG }) else dispatch({ type: HIDE_CANVAS_DIALOG }) @@ -113,19 +35,11 @@ const ChatFeedbackDialog = ({ show, dialogProps, onCancel, onConfirm }) => { aria-describedby='alert-dialog-description' > - {dialogProps.title || 'Chat Feedback'} + {dialogProps.title || 'Allowed Domains'} - - - + - - - - Save - - ) : null diff --git a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx new file mode 100644 index 00000000..5fde038a --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types' +import { useState } from 'react' +import { createPortal } from 'react-dom' +import { Box, Dialog, DialogContent, DialogTitle, Tabs, Tab } from '@mui/material' +import SpeechToText from '@/ui-component/extended/SpeechToText' +import RateLimit from '@/ui-component/extended/RateLimit' +import AllowedDomains from '@/ui-component/extended/AllowedDomains' +import ChatFeedback from '@/ui-component/extended/ChatFeedback' +import AnalyseFlow from '@/ui-component/extended/AnalyseFlow' +import StarterPrompts from '@/ui-component/extended/StarterPrompts' + +const CHATFLOW_CONFIGURATION_TABS = [ + { + label: 'Rate Limiting', + id: 'rateLimiting' + }, + { + label: 'Starter Prompts', + id: 'conversationStarters' + }, + { + label: 'Speech to Text', + id: 'speechToText' + }, + { + label: 'Chat Feedback', + id: 'chatFeedback' + }, + { + label: 'Allowed Domains', + id: 'allowedDomains' + }, + { + label: 'Analyse Chatflow', + id: 'analyseChatflow' + } +] + +function TabPanel(props) { + const { children, value, index, ...other } = props + return ( + + ) +} + +TabPanel.propTypes = { + children: PropTypes.node, + index: PropTypes.number.isRequired, + value: PropTypes.number.isRequired +} + +function a11yProps(index) { + return { + id: `chatflow-config-tab-${index}`, + 'aria-controls': `chatflow-config-tabpanel-${index}` + } +} + +const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => { + const portalElement = document.getElementById('portal') + const [tabValue, setTabValue] = useState(0) + + const component = show ? ( + + +
{dialogProps.title}
+
+ + setTabValue(value)} + aria-label='tabs' + > + {CHATFLOW_CONFIGURATION_TABS.map((item, index) => ( + + ))} + + {CHATFLOW_CONFIGURATION_TABS.map((item, index) => ( + + {item.id === 'rateLimiting' && } + {item.id === 'conversationStarters' ? : null} + {item.id === 'speechToText' ? : null} + {item.id === 'chatFeedback' ? : null} + {item.id === 'allowedDomains' ? : null} + {item.id === 'analyseChatflow' ? : null} + + ))} + +
+ ) : null + + return createPortal(component, portalElement) +} + +ChatflowConfigurationDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default ChatflowConfigurationDialog diff --git a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.jsx b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.jsx index ef410ae6..a01dafae 100644 --- a/packages/ui/src/ui-component/dialog/SpeechToTextDialog.jsx +++ b/packages/ui/src/ui-component/dialog/SpeechToTextDialog.jsx @@ -1,99 +1,17 @@ import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' -import { useState, useEffect } from 'react' +import { 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' +import { Dialog, DialogContent, DialogTitle } from '@mui/material' // 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'] - } - ] - } -} +// Project imports +import SpeechToText from '@/ui-component/extended/SpeechToText' const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') @@ -101,111 +19,13 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { 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) => ( - - ) - } - }) - 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) => ( - - ) - } - }) - } - } - - 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 = ( + const component = show ? ( { aria-describedby='alert-dialog-description' > - Speech To Text Configuration + {dialogProps.title || 'Allowed Domains'} - - Speech To Text Providers - - - - - {selectedProvider !== 'none' && ( - <> - - -
- AI -
-
- - {speechToTextProviders[selectedProvider].url} - - } - /> -
- {speechToTextProviders[selectedProvider].inputs.map((inputParam, index) => ( - -
- - {inputParam.label} - {!inputParam.optional &&  *} - {inputParam.description && ( - - )} - -
- {inputParam.type === 'credential' && ( - setValue(newValue, selectedProvider, 'credentialId')} - /> - )} - {inputParam.type === 'boolean' && ( - setValue(newValue, selectedProvider, inputParam.name)} - value={ - speechToText[selectedProvider] - ? speechToText[selectedProvider][inputParam.name] - : inputParam.default ?? false - } - /> - )} - {(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && ( - setValue(newValue, selectedProvider, inputParam.name)} - value={ - speechToText[selectedProvider] - ? speechToText[selectedProvider][inputParam.name] - : inputParam.default ?? '' - } - /> - )} - - {inputParam.type === 'options' && ( - setValue(newValue, selectedProvider, inputParam.name)} - value={ - speechToText[selectedProvider] - ? speechToText[selectedProvider][inputParam.name] - : inputParam.default ?? 'choose an option' - } - /> - )} -
- ))} - - )} +
- - - Save - -
- ) + ) : null return createPortal(component, portalElement) } @@ -342,7 +49,8 @@ const SpeechToTextDialog = ({ show, dialogProps, onCancel }) => { SpeechToTextDialog.propTypes = { show: PropTypes.bool, dialogProps: PropTypes.object, - onCancel: PropTypes.func + onCancel: PropTypes.func, + onConfirm: PropTypes.func } export default SpeechToTextDialog diff --git a/packages/ui/src/ui-component/dialog/StarterPromptsDialog.jsx b/packages/ui/src/ui-component/dialog/StarterPromptsDialog.jsx index 1f8f98bd..2bfb2f23 100644 --- a/packages/ui/src/ui-component/dialog/StarterPromptsDialog.jsx +++ b/packages/ui/src/ui-component/dialog/StarterPromptsDialog.jsx @@ -1,149 +1,24 @@ import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' -import { useState, useEffect } from 'react' +import { 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' +import { Dialog, DialogContent, DialogTitle } from '@mui/material' // store import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' import useNotifier from '@/utils/useNotifier' -// API -import chatflowsApi from '@/api/chatflows' +// Project imports +import StarterPrompts from '@/ui-component/extended/StarterPrompts' -const StarterPromptsDialog = ({ show, dialogProps, onCancel, onConfirm }) => { +const StarterPromptsDialog = ({ 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 [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) => ( - - ) - } - }) - 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) => ( - - ) - } - }) - } - } - - 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 }) @@ -163,79 +38,8 @@ const StarterPromptsDialog = ({ show, dialogProps, onCancel, onConfirm }) => { {dialogProps.title || 'Conversation Starter Prompts'} -
-
- - - Starter prompts will only be shown when there is no messages on the chat - -
-
- :not(style)': { m: 1 }, pt: 2 }}> - - {inputFields.map((data, index) => { - return ( -
- - handleChange(index, e)} - size='small' - value={data.prompt} - name='prompt' - endAdornment={ - - {inputFields.length > 1 && ( - removeInputFields(index)} - edge='end' - > - - - )} - - } - /> - - - {index === inputFields.length - 1 && ( - - - - )} - -
- ) - })} -
-
+
- - - - Save - - ) : null diff --git a/packages/ui/src/ui-component/extended/AllowedDomains.jsx b/packages/ui/src/ui-component/extended/AllowedDomains.jsx new file mode 100644 index 00000000..e0d01dd3 --- /dev/null +++ b/packages/ui/src/ui-component/extended/AllowedDomains.jsx @@ -0,0 +1,173 @@ +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, OutlinedInput, Box, List, InputAdornment } from '@mui/material' +import { IconX, IconTrash, IconPlus } from '@tabler/icons' + +// Project import +import { StyledButton } from '@/ui-component/button/StyledButton' + +// store +import useNotifier from '@/utils/useNotifier' + +// API +import chatflowsApi from '@/api/chatflows' + +const AllowedDomains = ({ dialogProps }) => { + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [inputFields, setInputFields] = useState(['']) + + const [chatbotConfig, setChatbotConfig] = useState({}) + + const addInputField = () => { + setInputFields([...inputFields, '']) + } + const removeInputFields = (index) => { + const rows = [...inputFields] + rows.splice(index, 1) + setInputFields(rows) + } + + const handleChange = (index, evnt) => { + const { value } = evnt.target + const list = [...inputFields] + list[index] = value + setInputFields(list) + } + + const onSave = async () => { + try { + let value = { + allowedOrigins: [...inputFields] + } + chatbotConfig.allowedOrigins = value.allowedOrigins + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + chatbotConfig: JSON.stringify(chatbotConfig) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Allowed Origins Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Allowed Origins: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { + try { + let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + setChatbotConfig(chatbotConfig || {}) + if (chatbotConfig.allowedOrigins) { + let inputFields = [...chatbotConfig.allowedOrigins] + setInputFields(inputFields) + } else { + setInputFields(['']) + } + } catch (e) { + setInputFields(['']) + } + } + + return () => {} + }, [dialogProps]) + + return ( + <> +
+ Your chatbot will only work when used from the following domains. +
+ :not(style)': { m: 1 }, pt: 2 }}> + + {inputFields.map((origin, index) => { + return ( +
+ + handleChange(index, e)} + size='small' + value={origin} + name='origin' + endAdornment={ + + {inputFields.length > 1 && ( + removeInputFields(index)} + edge='end' + > + + + )} + + } + /> + + + {index === inputFields.length - 1 && ( + + + + )} + +
+ ) + })} +
+
+ + Save + + + ) +} + +AllowedDomains.propTypes = { + dialogProps: PropTypes.object +} + +export default AllowedDomains diff --git a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx new file mode 100644 index 00000000..4d00a12f --- /dev/null +++ b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx @@ -0,0 +1,324 @@ +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, + Accordion, + AccordionSummary, + AccordionDetails, + ListItem, + ListItemAvatar, + ListItemText +} from '@mui/material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +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 langsmithPNG from '@/assets/images/langchain.png' +import langfuseSVG from '@/assets/images/langfuse.svg' +import lunarySVG from '@/assets/images/lunary.svg' + +// store +import useNotifier from '@/utils/useNotifier' + +// API +import chatflowsApi from '@/api/chatflows' + +const analyticProviders = [ + { + label: 'LangSmith', + name: 'langSmith', + icon: langsmithPNG, + url: 'https://smith.langchain.com', + inputs: [ + { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['langsmithApi'] + }, + { + label: 'Project Name', + name: 'projectName', + type: 'string', + optional: true, + description: 'If not provided, default will be used', + placeholder: 'default' + }, + { + label: 'On/Off', + name: 'status', + type: 'boolean', + optional: true + } + ] + }, + { + label: 'LangFuse', + name: 'langFuse', + icon: langfuseSVG, + url: 'https://langfuse.com', + inputs: [ + { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['langfuseApi'] + }, + { + label: 'Release', + name: 'release', + type: 'string', + optional: true, + description: 'The release number/hash of the application to provide analytics grouped by release' + }, + { + label: 'On/Off', + name: 'status', + type: 'boolean', + optional: true + } + ] + }, + { + label: 'Lunary', + name: 'lunary', + icon: lunarySVG, + url: 'https://lunary.ai', + inputs: [ + { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['lunaryApi'] + }, + { + label: 'On/Off', + name: 'status', + type: 'boolean', + optional: true + } + ] + } +] + +const AnalyseFlow = ({ dialogProps }) => { + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [analytic, setAnalytic] = useState({}) + const [providerExpanded, setProviderExpanded] = useState({}) + + const onSave = async () => { + try { + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + analytic: JSON.stringify(analytic) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Analytic Configuration Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Analytic Configuration: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + const setValue = (value, providerName, inputParamName) => { + let newVal = {} + if (!Object.prototype.hasOwnProperty.call(analytic, providerName)) { + newVal = { ...analytic, [providerName]: {} } + } else { + newVal = { ...analytic } + } + + newVal[providerName][inputParamName] = value + setAnalytic(newVal) + } + + const handleAccordionChange = (providerName) => (event, isExpanded) => { + const accordianProviders = { ...providerExpanded } + accordianProviders[providerName] = isExpanded + setProviderExpanded(accordianProviders) + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.analytic) { + try { + setAnalytic(JSON.parse(dialogProps.chatflow.analytic)) + } catch (e) { + setAnalytic({}) + console.error(e) + } + } + + return () => { + setAnalytic({}) + setProviderExpanded({}) + } + }, [dialogProps]) + + return ( + <> + {analyticProviders.map((provider, index) => ( + + } aria-controls={provider.name} id={provider.name}> + + +
+ AI +
+
+ + {provider.url} + + } + /> + {analytic[provider.name] && analytic[provider.name].status && ( +
+
+ ON +
+ )} + + + + {provider.inputs.map((inputParam, index) => ( + +
+ + {inputParam.label} + {!inputParam.optional &&  *} + {inputParam.description && ( + + )} + +
+ {providerExpanded[provider.name] && inputParam.type === 'credential' && ( + setValue(newValue, provider.name, 'credentialId')} + /> + )} + {providerExpanded[provider.name] && inputParam.type === 'boolean' && ( + setValue(newValue, provider.name, inputParam.name)} + value={ + analytic[provider.name] ? analytic[provider.name][inputParam.name] : inputParam.default ?? false + } + /> + )} + {providerExpanded[provider.name] && + (inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && ( + setValue(newValue, provider.name, inputParam.name)} + value={ + analytic[provider.name] + ? analytic[provider.name][inputParam.name] + : inputParam.default ?? '' + } + /> + )} +
+ ))} +
+ + ))} + + Save + + + ) +} + +AnalyseFlow.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func +} + +export default AnalyseFlow diff --git a/packages/ui/src/ui-component/extended/ChatFeedback.jsx b/packages/ui/src/ui-component/extended/ChatFeedback.jsx new file mode 100644 index 00000000..1706d624 --- /dev/null +++ b/packages/ui/src/ui-component/extended/ChatFeedback.jsx @@ -0,0 +1,107 @@ +import { useDispatch } from 'react-redux' +import { useState, useEffect } from 'react' +import PropTypes from 'prop-types' + +// material-ui +import { Button, Box } from '@mui/material' +import { IconX } from '@tabler/icons' + +// Project import +import { StyledButton } from '@/ui-component/button/StyledButton' +import { SwitchInput } from '@/ui-component/switch/Switch' + +// store +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' +import useNotifier from '@/utils/useNotifier' + +// API +import chatflowsApi from '@/api/chatflows' + +const ChatFeedback = ({ dialogProps }) => { + const dispatch = useDispatch() + + useNotifier() + + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false) + const [chatbotConfig, setChatbotConfig] = useState({}) + + const handleChange = (value) => { + setChatFeedbackStatus(value) + } + + const onSave = async () => { + try { + let value = { + chatFeedback: { + status: chatFeedbackStatus + } + } + chatbotConfig.chatFeedback = value.chatFeedback + const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, { + chatbotConfig: JSON.stringify(chatbotConfig) + }) + if (saveResp.data) { + enqueueSnackbar({ + message: 'Chat Feedback Settings Saved', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } catch (error) { + const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}` + enqueueSnackbar({ + message: `Failed to save Chat Feedback Settings: ${errorData}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + useEffect(() => { + if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) { + let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig) + setChatbotConfig(chatbotConfig || {}) + if (chatbotConfig.chatFeedback) { + setChatFeedbackStatus(chatbotConfig.chatFeedback.status) + } + } + + return () => {} + }, [dialogProps]) + + return ( + <> + + + + + Save + + + ) +} + +ChatFeedback.propTypes = { + dialogProps: PropTypes.object +} + +export default ChatFeedback diff --git a/packages/ui/src/views/chatflows/Configuration.jsx b/packages/ui/src/ui-component/extended/RateLimit.jsx similarity index 95% rename from packages/ui/src/views/chatflows/Configuration.jsx rename to packages/ui/src/ui-component/extended/RateLimit.jsx index 5652f179..5fc88cae 100644 --- a/packages/ui/src/views/chatflows/Configuration.jsx +++ b/packages/ui/src/ui-component/extended/RateLimit.jsx @@ -18,7 +18,7 @@ import chatflowsApi from '@/api/chatflows' import useNotifier from '@/utils/useNotifier' import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' -const Configuration = () => { +const RateLimit = () => { const dispatch = useDispatch() const chatflow = useSelector((state) => state.canvas.chatflow) const chatflowid = chatflow.id @@ -59,7 +59,7 @@ const Configuration = () => { }) if (saveResp.data) { enqueueSnackbar({ - message: 'API Configuration Saved', + message: 'Rate Limit Configuration Saved', options: { key: new Date().getTime() + Math.random(), variant: 'success', @@ -78,7 +78,7 @@ const Configuration = () => { ? error.response.data || `${error.response.status}: ${error.response.statusText}` : error.message enqueueSnackbar({ - message: `Failed to save API Configuration: ${errorData}`, + message: `Failed to save Rate Limit Configuration: ${errorData}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -131,7 +131,7 @@ const Configuration = () => { return ( <> {/*Rate Limit*/} - + Rate Limit{' '} { ) } -Configuration.propTypes = { +RateLimit.propTypes = { isSessionMemory: PropTypes.bool } -export default Configuration +export default RateLimit diff --git a/packages/ui/src/ui-component/extended/SpeechToText.jsx b/packages/ui/src/ui-component/extended/SpeechToText.jsx new file mode 100644 index 00000000..b56c8f85 --- /dev/null +++ b/packages/ui/src/ui-component/extended/SpeechToText.jsx @@ -0,0 +1,309 @@ +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, 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 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 SpeechToText = ({ dialogProps }) => { + 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) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } 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) => ( + + ) + } + }) + } + } + + 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]) + + return ( + <> + + + Providers + + + + + + {selectedProvider !== 'none' && ( + <> + + +
+ AI +
+
+ + {speechToTextProviders[selectedProvider].url} + + } + /> +
+ {speechToTextProviders[selectedProvider].inputs.map((inputParam, index) => ( + +
+ + {inputParam.label} + {!inputParam.optional &&  *} + {inputParam.description && ( + + )} + +
+ {inputParam.type === 'credential' && ( + setValue(newValue, selectedProvider, 'credentialId')} + /> + )} + {inputParam.type === 'boolean' && ( + setValue(newValue, selectedProvider, inputParam.name)} + value={ + speechToText[selectedProvider] + ? speechToText[selectedProvider][inputParam.name] + : inputParam.default ?? false + } + /> + )} + {(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && ( + setValue(newValue, selectedProvider, inputParam.name)} + value={ + speechToText[selectedProvider] + ? speechToText[selectedProvider][inputParam.name] + : inputParam.default ?? '' + } + /> + )} + + {inputParam.type === 'options' && ( + setValue(newValue, selectedProvider, inputParam.name)} + value={ + speechToText[selectedProvider] + ? speechToText[selectedProvider][inputParam.name] + : inputParam.default ?? 'choose an option' + } + /> + )} +
+ ))} + + )} + + Save + + + ) +} + +SpeechToText.propTypes = { + dialogProps: PropTypes.object +} + +export default SpeechToText diff --git a/packages/ui/src/ui-component/extended/StarterPrompts.jsx b/packages/ui/src/ui-component/extended/StarterPrompts.jsx new file mode 100644 index 00000000..a12c51bb --- /dev/null +++ b/packages/ui/src/ui-component/extended/StarterPrompts.jsx @@ -0,0 +1,214 @@ +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, OutlinedInput, 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 useNotifier from '@/utils/useNotifier' + +// API +import chatflowsApi from '@/api/chatflows' + +const StarterPrompts = ({ dialogProps }) => { + 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) => ( + + ) + } + }) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + } + } 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) => ( + + ) + } + }) + } + } + + 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]) + + return ( + <> +
+
+ + + Starter prompts will only be shown when there is no messages on the chat + +
+
+ :not(style)': { m: 1 }, pt: 2 }}> + + {inputFields.map((data, index) => { + return ( +
+ + handleChange(index, e)} + size='small' + value={data.prompt} + name='prompt' + endAdornment={ + + {inputFields.length > 1 && ( + removeInputFields(index)} + edge='end' + > + + + )} + + } + /> + + + {index === inputFields.length - 1 && ( + + + + )} + +
+ ) + })} +
+
+ + Save + + + ) +} + +StarterPrompts.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default StarterPrompts diff --git a/packages/ui/src/views/canvas/CanvasHeader.jsx b/packages/ui/src/views/canvas/CanvasHeader.jsx index cfaf3d7e..4728c6b0 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.jsx +++ b/packages/ui/src/views/canvas/CanvasHeader.jsx @@ -14,12 +14,8 @@ import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, import Settings from '@/views/settings' 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' -import ChatFeedbackDialog from '@/ui-component/dialog/ChatFeedbackDialog' -import AllowedDomainsDialog from '@/ui-component/dialog/AllowedDomainsDialog' +import ChatflowConfigurationDialog from '@/ui-component/dialog/ChatflowConfigurationDialog' // API import chatflowsApi from '@/api/chatflows' @@ -47,18 +43,10 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl const [flowDialogOpen, setFlowDialogOpen] = useState(false) const [apiDialogOpen, setAPIDialogOpen] = useState(false) 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({}) - const [chatFeedbackDialogOpen, setChatFeedbackDialogOpen] = useState(false) - const [chatFeedbackDialogProps, setChatFeedbackDialogProps] = useState({}) - const [allowedDomainsDialogOpen, setAllowedDomainsDialogOpen] = useState(false) - const [allowedDomainsDialogProps, setAllowedDomainsDialogProps] = useState({}) + const [chatflowConfigurationDialogOpen, setChatflowConfigurationDialogOpen] = useState(false) + const [chatflowConfigurationDialogProps, setChatflowConfigurationDialogProps] = useState({}) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) const canvas = useSelector((state) => state.canvas) @@ -68,42 +56,18 @@ 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 === 'chatFeedback') { - setChatFeedbackDialogProps({ - title: `Chat Feedback - ${chatflow.name}`, - chatflow: chatflow - }) - setChatFeedbackDialogOpen(true) - } else if (setting === 'allowedDomains') { - setAllowedDomainsDialogProps({ - title: 'Allowed Domains - ' + chatflow.name, - chatflow: chatflow - }) - setAllowedDomainsDialogOpen(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', chatflow: chatflow }) setViewMessagesDialogOpen(true) + } else if (setting === 'chatflowConfiguration') { + setChatflowConfigurationDialogProps({ + title: 'Chatflow Configuration', + chatflow: chatflow + }) + setChatflowConfigurationDialogOpen(true) } else if (setting === 'duplicateChatflow') { try { localStorage.setItem('duplicatedFlowData', chatflow.flowData) @@ -207,8 +171,15 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl useEffect(() => { if (chatflow) { setFlowName(chatflow.name) + // if configuration dialog is open, update its data + if (chatflowConfigurationDialogOpen) { + setChatflowConfigurationDialogProps({ + title: 'Chatflow Configuration', + chatflow + }) + } } - }, [chatflow]) + }, [chatflow, chatflowConfigurationDialogOpen]) return ( <> @@ -411,35 +382,17 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl onConfirm={onConfirmSaveName} /> setAPIDialogOpen(false)} /> - setAnalyseDialogOpen(false)} /> - setSpeechToAudioDialogOpen(false)} - /> - setConversationStartersDialogOpen(false)} - onCancel={() => setConversationStartersDialogOpen(false)} - /> - setChatFeedbackDialogOpen(false)} - onCancel={() => setChatFeedbackDialogOpen(false)} - /> - setAllowedDomainsDialogOpen(false)} - onCancel={() => setAllowedDomainsDialogOpen(false)} - /> setViewMessagesDialogOpen(false)} /> + setChatflowConfigurationDialogOpen(false)} + /> ) } diff --git a/packages/ui/src/views/chatflows/APICodeDialog.jsx b/packages/ui/src/views/chatflows/APICodeDialog.jsx index e3a32d0d..b2cd4ea6 100644 --- a/packages/ui/src/views/chatflows/APICodeDialog.jsx +++ b/packages/ui/src/views/chatflows/APICodeDialog.jsx @@ -23,7 +23,6 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { Dropdown } from '@/ui-component/dropdown/Dropdown' import ShareChatbot from './ShareChatbot' import EmbedChat from './EmbedChat' -import Configuration from './Configuration' // Const import { baseURL } from '@/store/constant' @@ -84,7 +83,7 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => { const navigate = useNavigate() const dispatch = useDispatch() - const codes = ['Embed', 'Python', 'JavaScript', 'cURL', 'Share Chatbot', 'Configuration'] + const codes = ['Embed', 'Python', 'JavaScript', 'cURL', 'Share Chatbot'] const [value, setValue] = useState(0) const [keyOptions, setKeyOptions] = useState([]) const [apiKeys, setAPIKeys] = useState([]) @@ -721,7 +720,6 @@ formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")` {codeLang === 'Share Chatbot' && !chatflowApiKeyId && ( )} - {codeLang === 'Configuration' && } ))} diff --git a/packages/ui/src/views/settings/index.jsx b/packages/ui/src/views/settings/index.jsx index d5d41890..41d22c42 100644 --- a/packages/ui/src/views/settings/index.jsx +++ b/packages/ui/src/views/settings/index.jsx @@ -65,7 +65,7 @@ const Settings = ({ chatflow, isSettingsOpen, anchorEl, onSettingsItemClick, onU 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'} + fontSize={'inherit'} /> ) return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f4ae235..b0e32d6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4228,7 +4228,7 @@ packages: dependencies: lodash.camelcase: 4.3.0 long: 5.2.3 - protobufjs: 7.2.4 + protobufjs: 7.2.6 yargs: 17.7.2 dev: false @@ -23847,6 +23847,24 @@ packages: long: 5.2.3 dev: false + /protobufjs@7.2.6: + resolution: {integrity: sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==} + engines: {node: '>=12.0.0'} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.11.26 + long: 5.2.3 + dev: false + /protoc-gen-ts@0.8.7: resolution: {integrity: sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==} hasBin: true