diff --git a/apps/server/agents/conversational/conversational.py b/apps/server/agents/conversational/conversational.py
index 4c257255..5c063892 100644
--- a/apps/server/agents/conversational/conversational.py
+++ b/apps/server/agents/conversational/conversational.py
@@ -100,6 +100,8 @@ async def run(
},
)
+ yield res
+
try:
configs = agent_with_configs.configs
voice_url = None
@@ -109,6 +111,8 @@ async def run(
except Exception as err:
res = f"{res}\n\n{handle_agent_error(err)}"
+ yield res
+
history.create_ai_message(
res,
human_message_id,
diff --git a/apps/server/services/voice.py b/apps/server/services/voice.py
index 4809cdfa..3fdcdd50 100644
--- a/apps/server/services/voice.py
+++ b/apps/server/services/voice.py
@@ -21,7 +21,8 @@ def text_to_speech(
synthesizers = {
"142e60f5-2d46-4b1a-9054-0764e553eed6": playht_text_to_speech,
- # TODO: add AzureVoice.id: azure_text_to_speech, when available.
+ "509fd791-578f-40be-971f-c6753957c307": eleven_labs_text_to_speech,
+ "dc872426-a95c-4c41-83a2-5e5ed43670cd": azure_text_to_speech,
}
if configs.synthesizer not in synthesizers:
@@ -147,3 +148,56 @@ async def deepgram_speech_to_text(
return transcribed_text.strip('"')
except Exception as err:
raise TranscriberException(str(err))
+
+
+def eleven_labs_text_to_speech(
+ text: str, configs: ConfigsOutput, settings: AccountVoiceSettings
+) -> bytes:
+ if settings.ELEVEN_LABS_API_KEY is None or not settings.ELEVEN_LABS_API_KEY:
+ raise SynthesizerException(
+ "Please set Eleven Labs API Key in [Voice Integrations](/integrations/voice/elevenlabs) in order to synthesize text to speech."
+ )
+
+ url = f"https://api.elevenlabs.io/v1/text-to-speech/{configs.voice_id or configs.default_voice}"
+
+ payload = {
+ "model_id": "eleven_multilingual_v2",
+ "text": text,
+ "voice_settings": {"similarity_boost": 1, "stability": 1, "style": 1},
+ }
+ headers = {
+ "xi-api-key": settings.ELEVEN_LABS_API_KEY,
+ "Content-Type": "application/json",
+ }
+
+ response = requests.post(url, json=payload, headers=headers)
+ return response.content
+
+
+def azure_text_to_speech(
+ text: str, configs: ConfigsOutput, settings: AccountVoiceSettings
+) -> bytes:
+ if settings.AZURE_SPEECH_KEY is None or not settings.AZURE_SPEECH_KEY:
+ raise SynthesizerException(
+ "Please set Azure Speech Key in [Voice Integrations](/integrations/voice/azure) in order to synthesize text to speech."
+ )
+
+ url = f"https://{settings.AZURE_SPEECH_REGION}.tts.speech.microsoft.com/cognitiveservices/v1"
+
+ body = f"""
+
+
+ {text}
+
+
+ """
+
+ headers = {
+ "Ocp-Apim-Subscription-Key": settings.AZURE_SPEECH_KEY,
+ "Content-Type": "application/ssml+xml",
+ "X-Microsoft-OutputFormat": "riff-24khz-16bit-mono-pcm",
+ "User-Agent": "Your application name",
+ }
+
+ response = requests.post(url, headers=headers, data=body)
+ return response.content
diff --git a/apps/ui/src/modals/AIChatModal/components/ChatMessageList/ChatMessageListV2.tsx b/apps/ui/src/modals/AIChatModal/components/ChatMessageList/ChatMessageListV2.tsx
index 3ff5e7a0..69634a13 100644
--- a/apps/ui/src/modals/AIChatModal/components/ChatMessageList/ChatMessageListV2.tsx
+++ b/apps/ui/src/modals/AIChatModal/components/ChatMessageList/ChatMessageListV2.tsx
@@ -17,6 +17,8 @@ import HumanReply from './components/HumanReply'
import AiReply from './components/AiReply'
import { ReplyStateProps } from '../ReplyBox'
+import { ArrowDown } from 'share-ui/components/Icon/Icons'
+
export enum MessageTypeEnum {
AI_MANUAL = 'AI_MANUAL',
User = 'User',
@@ -49,6 +51,7 @@ const ChatMessageListV2 = ({
const [listIsReady, setListIsReady] = useState(false)
const virtuoso = useRef(null)
+ const scrollerRef = useRef(null)
const filteredData = data?.map((chat: any) => {
const chatDate = moment(chat?.created_on).format('HH:mm')
@@ -95,7 +98,7 @@ const ChatMessageListV2 = ({
const scrollToEnd = () => {
virtuoso.current?.scrollToIndex({
- index: initialChat.length + 1,
+ index: initialChat.length + 2,
align: 'end',
})
}
@@ -150,10 +153,44 @@ const ChatMessageListV2 = ({
// eslint-disable-next-line
}, [sessionId])
+ const [showScrollButton, setShowScrollButton] = useState(false)
+
+ useEffect(() => {
+ const handleScroll = () => {
+ if (scrollerRef.current) {
+ const { scrollTop, clientHeight, scrollHeight } = scrollerRef.current
+ const bottomScrollPosition = scrollTop + clientHeight
+ const isScrollable = scrollHeight - bottomScrollPosition > 1 // Using 1 as a threshold
+ setShowScrollButton(isScrollable)
+ }
+ }
+
+ const scrollContainer = scrollerRef.current
+ if (scrollContainer) {
+ scrollContainer.addEventListener('scroll', handleScroll)
+
+ // Initial check in case the list is already scrollable
+ handleScroll()
+ }
+
+ return () => {
+ if (scrollContainer) {
+ scrollContainer.removeEventListener('scroll', handleScroll)
+ }
+ }
+ }, [initialChat])
+
+ useEffect(() => {
+ if (!showScrollButton) scrollToEnd()
+ }, [data])
+
return (
{
+ scrollerRef.current = ref
+ }}
style={{ height: '100%' }}
data={initialChat}
totalCount={data.length}
@@ -253,6 +290,11 @@ const ChatMessageListV2 = ({
>
)}
/>
+ {showScrollButton && (
+
+
+
+ )}
)
}
@@ -260,6 +302,8 @@ const ChatMessageListV2 = ({
export default memo(ChatMessageListV2)
const StyledRoot = styled.div<{ show: boolean }>`
+ position: relative;
+
opacity: 0;
width: 100%;
@@ -331,3 +375,21 @@ const StyledReplyMessageContainer = styled.div`
justify-content: center;
/* align-items: center; */
`
+const StyledScrollButton = styled.div`
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+
+ cursor: pointer;
+
+ width: 38px;
+ height: 38px;
+ border-radius: 100px;
+ background-color: #fff;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); // Add box shadow here
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`
diff --git a/apps/ui/src/pages/Agents/AgentForm/AgentForm.tsx b/apps/ui/src/pages/Agents/AgentForm/AgentForm.tsx
index 3bd9ff49..57c175f6 100644
--- a/apps/ui/src/pages/Agents/AgentForm/AgentForm.tsx
+++ b/apps/ui/src/pages/Agents/AgentForm/AgentForm.tsx
@@ -28,6 +28,7 @@ import AgentRunners, { StyledRunnerFieldsWrapper } from './components/AgentRunne
import { isVoiceAgent } from 'utils/agentUtils'
import VoicePreferences from './FormSections/VoicePreferences'
+import RadioButton from 'share-ui/components/RadioButton/RadioButton'
type AgentFormProps = {
formik: any
@@ -52,6 +53,8 @@ const AgentForm = ({ formik, isVoice = true }: AgentFormProps) => {
agent_integrations,
agent_type,
agent_sentiment_analyzer,
+ agent_voice_response,
+ agent_voice_input_mode,
} = values
const {
@@ -259,87 +262,6 @@ const AgentForm = ({ formik, isVoice = true }: AgentFormProps) => {
voiceSynthesizerOptions={voiceSynthesizerOptions}
voiceTranscriberOptions={voiceTranscriberOptions}
/>
- {/*
-
-
- setFieldValue('agent_voice_response', ['Text'])}
- checked={
- agent_voice_response?.length === 1 && agent_voice_response?.includes('Text')
- }
- />
- setFieldValue('agent_voice_response', ['Voice'])}
- checked={
- agent_voice_response?.length === 1 &&
- agent_voice_response?.includes('Voice')
- }
- />
- setFieldValue('agent_voice_response', ['Text', 'Voice'])}
- checked={agent_voice_response?.length === 2}
- />
- */}
-
- {/*
-
-
- {
- if (agent_voice_input_mode?.includes('Text')) {
- const filteredInput = agent_voice_input_mode?.filter(
- (input: string) => input !== 'Text',
- )
- setFieldValue('agent_voice_input_mode', filteredInput)
- } else {
- setFieldValue('agent_voice_input_mode', [
- ...agent_voice_input_mode,
- 'Text',
- ])
- }
- }}
- />
-
-
- {
- if (agent_voice_input_mode?.includes('Voice')) {
- const filteredInput = agent_voice_input_mode?.filter(
- (input: string) => input !== 'Voice',
- )
- setFieldValue('agent_voice_input_mode', filteredInput)
- } else {
- setFieldValue('agent_voice_input_mode', [
- ...agent_voice_input_mode,
- 'Voice',
- ])
- }
- }}
- />
-
- */}
>
diff --git a/apps/ui/src/pages/Agents/AgentForm/FormSections/VoicePreferences.tsx b/apps/ui/src/pages/Agents/AgentForm/FormSections/VoicePreferences.tsx
index eb337b11..ad4c6d96 100644
--- a/apps/ui/src/pages/Agents/AgentForm/FormSections/VoicePreferences.tsx
+++ b/apps/ui/src/pages/Agents/AgentForm/FormSections/VoicePreferences.tsx
@@ -11,6 +11,12 @@ import { useVoiceOptionsService } from 'plugins/contact/services/voice/useVoiceO
import { useModal } from 'hooks'
import Loader from 'share-ui/components/Loader/Loader'
+import Checkbox from 'share-ui/components/Checkbox/Checkbox'
+import TypographyPrimary from 'components/Typography/Primary'
+import RadioButton from 'share-ui/components/RadioButton/RadioButton'
+import Typography from 'share-ui/components/typography/Typography'
+import { useEffect } from 'react'
+
const VoicePreferences = ({
formik,
voiceSynthesizerOptions,
@@ -24,7 +30,14 @@ const VoicePreferences = ({
const { setFieldValue, values } = formik
- const { agent_voice_synthesizer, agent_voice_transcriber, agent_voice_id, agent_type } = values
+ const {
+ agent_voice_synthesizer,
+ agent_voice_transcriber,
+ agent_voice_id,
+ agent_type,
+ agent_voice_response,
+ agent_voice_input_mode,
+ } = values
const { data: voiceOptions, loading } = useVoiceOptionsService({})
@@ -49,9 +62,9 @@ const VoicePreferences = ({
return {
name: item.name,
sample: item.preview_url,
- language: item.language,
+ language: '-',
id: item.voice_id,
- gender: item.gender,
+ gender: item.labels.gender,
}
})
@@ -133,6 +146,74 @@ const VoicePreferences = ({
label='Twilio Phone Number SID'
/>
)}
+
+ {agent_type === 'text' && (
+ <>
+
+
+ setFieldValue('agent_voice_response', ['Text'])}
+ checked={agent_voice_response?.length === 1 && agent_voice_response?.includes('Text')}
+ />
+ setFieldValue('agent_voice_response', ['Voice'])}
+ checked={agent_voice_response?.length === 1 && agent_voice_response?.includes('Voice')}
+ />
+ setFieldValue('agent_voice_response', ['Text', 'Voice'])}
+ checked={agent_voice_response?.length === 2}
+ />
+
+
+ {
+ if (agent_voice_input_mode?.includes('Text')) {
+ const filteredInput = agent_voice_input_mode?.filter(
+ (input: string) => input !== 'Text',
+ )
+ setFieldValue('agent_voice_input_mode', filteredInput)
+ } else {
+ setFieldValue('agent_voice_input_mode', [...agent_voice_input_mode, 'Text'])
+ }
+ }}
+ />
+
+ {
+ if (agent_voice_input_mode?.includes('Voice')) {
+ const filteredInput = agent_voice_input_mode?.filter(
+ (input: string) => input !== 'Voice',
+ )
+ setFieldValue('agent_voice_input_mode', filteredInput)
+ } else {
+ setFieldValue('agent_voice_input_mode', [...agent_voice_input_mode, 'Voice'])
+ }
+ }}
+ />
+ >
+ )}
)
}
diff --git a/apps/ui/src/share-ui/components/Icon/Icons/components/ArrowDown.tsx b/apps/ui/src/share-ui/components/Icon/Icons/components/ArrowDown.tsx
index 72aad611..123ea522 100644
--- a/apps/ui/src/share-ui/components/Icon/Icons/components/ArrowDown.tsx
+++ b/apps/ui/src/share-ui/components/Icon/Icons/components/ArrowDown.tsx
@@ -1,30 +1,24 @@
/* eslint-disable */
/* tslint:disable */
-import * as React from 'react';
+import * as React from 'react'
export interface ArrowDownProps extends React.SVGAttributes {
-size?: string | number;
+ size?: string | number
}
-const ArrowDown: React.FC = ({size, ...props}) => (
-