mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
Fix merge conflicts
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flowise-ui",
|
||||
"version": "1.5.1",
|
||||
"version": "1.6.0",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://flowiseai.com",
|
||||
"author": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ const getAllChatmessageFromChatflow = (id, params = {}) =>
|
||||
client.get(`/chatmessage/${id}`, { params: { order: 'DESC', feedback: true, ...params } })
|
||||
const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', feedback: true, ...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
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.5 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 |
Binary file not shown.
|
After Width: | Height: | Size: 323 KiB |
@@ -7,11 +7,22 @@ import {
|
||||
IconSearch,
|
||||
IconMessage,
|
||||
IconPictureInPictureOff,
|
||||
IconMicrophone,
|
||||
IconThumbUp
|
||||
} from '@tabler/icons'
|
||||
|
||||
// constant
|
||||
const icons = { IconTrash, IconFileUpload, IconFileExport, IconCopy, IconSearch, IconMessage, IconPictureInPictureOff, IconThumbUp }
|
||||
const icons = {
|
||||
IconTrash,
|
||||
IconFileUpload,
|
||||
IconFileExport,
|
||||
IconCopy,
|
||||
IconSearch,
|
||||
IconMessage,
|
||||
IconPictureInPictureOff,
|
||||
IconMicrophone,
|
||||
IconThumbUp
|
||||
}
|
||||
|
||||
// ==============================|| SETTINGS MENU ITEMS ||============================== //
|
||||
|
||||
@@ -41,6 +52,13 @@ const settings = {
|
||||
url: '',
|
||||
icon: icons.IconThumbUp
|
||||
},
|
||||
{
|
||||
id: 'enableSpeechToText',
|
||||
title: 'Speech to Text',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconMicrophone
|
||||
},
|
||||
{
|
||||
id: 'duplicateChatflow',
|
||||
title: 'Duplicate Chatflow',
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
@@ -1,7 +1,4 @@
|
||||
.button-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch; /* For momentum scroll on mobile devices */
|
||||
@@ -10,5 +7,4 @@
|
||||
|
||||
.button {
|
||||
flex: 0 0 auto; /* Don't grow, don't shrink, base width on content */
|
||||
margin: 5px; /* Adjust as needed for spacing between buttons */
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ import PropTypes from 'prop-types'
|
||||
import { Chip } from '@mui/material'
|
||||
import './StarterPromptsCard.css'
|
||||
|
||||
const StarterPromptsCard = ({ isGrid, starterPrompts, onPromptClick }) => {
|
||||
const StarterPromptsCard = ({ isGrid, starterPrompts, sx, onPromptClick }) => {
|
||||
return (
|
||||
<Box className={'button-container'} sx={{ maxWidth: isGrid ? 'inherit' : '400px', m: 1 }}>
|
||||
<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)} />
|
||||
))}
|
||||
@@ -15,7 +18,8 @@ const StarterPromptsCard = ({ isGrid, starterPrompts, onPromptClick }) => {
|
||||
|
||||
StarterPromptsCard.propTypes = {
|
||||
isGrid: PropTypes.bool,
|
||||
starterPrompts: PropTypes.arrayOf(PropTypes.string),
|
||||
starterPrompts: PropTypes.array,
|
||||
sx: PropTypes.object,
|
||||
onPromptClick: PropTypes.func
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Stack,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { IconTrash, IconX } from '@tabler/icons'
|
||||
import { IconEraser, IconTrash, IconX } from '@tabler/icons'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
||||
@@ -113,6 +113,10 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => {
|
||||
setSelectedLinks(links)
|
||||
}
|
||||
|
||||
const handleRemoveAllLinks = () => {
|
||||
setSelectedLinks([])
|
||||
}
|
||||
|
||||
const handleSaveLinks = () => {
|
||||
onSave(url, selectedLinks)
|
||||
}
|
||||
@@ -145,6 +149,7 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => {
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
disabled={!url}
|
||||
sx={{ borderRadius: '12px', mt: 1, display: 'flex', flexShrink: 0 }}
|
||||
size='small'
|
||||
variant='contained'
|
||||
@@ -154,7 +159,21 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => {
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Typography sx={{ mb: 2, fontWeight: 500 }}>Scraped Links</Typography>
|
||||
<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 ? (
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -50,7 +52,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'
|
||||
|
||||
@@ -72,6 +74,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()
|
||||
@@ -97,6 +105,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow)
|
||||
const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK)
|
||||
const getStatsApi = useApi(feedbackApi.getStatsFromChatflow)
|
||||
const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath)
|
||||
let storagePath = ''
|
||||
|
||||
const onStartDateSelected = (date) => {
|
||||
setStartDate(date)
|
||||
@@ -125,16 +135,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)
|
||||
@@ -256,6 +285,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,
|
||||
@@ -364,6 +401,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (getChatmessageApi.data) {
|
||||
getStoragePathFromServer.request()
|
||||
|
||||
setAllChatLogs(getChatmessageApi.data)
|
||||
const chatPK = processChatLogs(getChatmessageApi.data)
|
||||
setSelectedMessageIndex(0)
|
||||
@@ -633,8 +672,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' }}
|
||||
@@ -684,6 +723,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
|
||||
|
||||
@@ -607,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
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import useApi from 'hooks/useApi'
|
||||
import { generateExportFlowData } from 'utils/genericHelper'
|
||||
import { uiBaseURL } from 'store/constant'
|
||||
import { SET_CHATFLOW } from 'store/actions'
|
||||
import SpeechToTextDialog from '../../ui-component/dialog/SpeechToTextDialog'
|
||||
|
||||
// ==============================|| CANVAS HEADER ||============================== //
|
||||
|
||||
@@ -47,6 +48,8 @@ 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)
|
||||
@@ -80,6 +83,12 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
|
||||
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',
|
||||
@@ -394,6 +403,11 @@ 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}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,12 +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'
|
||||
import StarterPromptsCard from '../../ui-component/cards/StarterPromptsCard'
|
||||
// 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)
|
||||
|
||||
@@ -51,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)
|
||||
@@ -58,10 +82,220 @@ 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 })
|
||||
setSourceDialogOpen(true)
|
||||
@@ -118,7 +352,10 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
if (e) e.preventDefault()
|
||||
|
||||
if (!promptStarterInput && userInput.trim() === '') {
|
||||
return
|
||||
const containsAudio = previews.filter((item) => item.type === 'audio').length > 0
|
||||
if (!(previews.length >= 1 && containsAudio)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let input = userInput
|
||||
@@ -126,7 +363,16 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
if (promptStarterInput !== undefined && promptStarterInput.trim() !== '') input = promptStarterInput
|
||||
|
||||
setLoading(true)
|
||||
setMessages((prevMessages) => [...prevMessages, { message: input, 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 {
|
||||
@@ -135,6 +381,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
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)
|
||||
@@ -144,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
|
||||
@@ -222,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])
|
||||
@@ -239,6 +505,15 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
|
||||
// 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)) {
|
||||
@@ -273,11 +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', () => {
|
||||
@@ -311,147 +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>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
{messages && messages.length === 1 && (
|
||||
<StarterPromptsCard starterPrompts={starterPrompts || []} onPromptClick={handlePromptClick} isGrid={isDialog} />
|
||||
)}
|
||||
<Divider />
|
||||
</div>
|
||||
{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}
|
||||
@@ -467,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.*/
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user