Skip to content

Commit cc29018

Browse files
committed
Merge branch 'main' of github.com:l3vels/L3AGI
2 parents ac4f5ce + 2c85a2a commit cc29018

File tree

7 files changed

+234
-110
lines changed

7 files changed

+234
-110
lines changed

apps/server/agents/conversational/conversational.py

+4
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ async def run(
100100
},
101101
)
102102

103+
yield res
104+
103105
try:
104106
configs = agent_with_configs.configs
105107
voice_url = None
@@ -109,6 +111,8 @@ async def run(
109111
except Exception as err:
110112
res = f"{res}\n\n{handle_agent_error(err)}"
111113

114+
yield res
115+
112116
history.create_ai_message(
113117
res,
114118
human_message_id,

apps/server/services/voice.py

+55-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ def text_to_speech(
2121

2222
synthesizers = {
2323
"142e60f5-2d46-4b1a-9054-0764e553eed6": playht_text_to_speech,
24-
# TODO: add AzureVoice.id: azure_text_to_speech, when available.
24+
"509fd791-578f-40be-971f-c6753957c307": eleven_labs_text_to_speech,
25+
"dc872426-a95c-4c41-83a2-5e5ed43670cd": azure_text_to_speech,
2526
}
2627

2728
if configs.synthesizer not in synthesizers:
@@ -147,3 +148,56 @@ async def deepgram_speech_to_text(
147148
return transcribed_text.strip('"')
148149
except Exception as err:
149150
raise TranscriberException(str(err))
151+
152+
153+
def eleven_labs_text_to_speech(
154+
text: str, configs: ConfigsOutput, settings: AccountVoiceSettings
155+
) -> bytes:
156+
if settings.ELEVEN_LABS_API_KEY is None or not settings.ELEVEN_LABS_API_KEY:
157+
raise SynthesizerException(
158+
"Please set Eleven Labs API Key in [Voice Integrations](/integrations/voice/elevenlabs) in order to synthesize text to speech."
159+
)
160+
161+
url = f"https://api.elevenlabs.io/v1/text-to-speech/{configs.voice_id or configs.default_voice}"
162+
163+
payload = {
164+
"model_id": "eleven_multilingual_v2",
165+
"text": text,
166+
"voice_settings": {"similarity_boost": 1, "stability": 1, "style": 1},
167+
}
168+
headers = {
169+
"xi-api-key": settings.ELEVEN_LABS_API_KEY,
170+
"Content-Type": "application/json",
171+
}
172+
173+
response = requests.post(url, json=payload, headers=headers)
174+
return response.content
175+
176+
177+
def azure_text_to_speech(
178+
text: str, configs: ConfigsOutput, settings: AccountVoiceSettings
179+
) -> bytes:
180+
if settings.AZURE_SPEECH_KEY is None or not settings.AZURE_SPEECH_KEY:
181+
raise SynthesizerException(
182+
"Please set Azure Speech Key in [Voice Integrations](/integrations/voice/azure) in order to synthesize text to speech."
183+
)
184+
185+
url = f"https://{settings.AZURE_SPEECH_REGION}.tts.speech.microsoft.com/cognitiveservices/v1"
186+
187+
body = f"""
188+
<speak version='1.0' xml:lang='en-US'>
189+
<voice xml:lang='en-US' name='{configs.voice_id}'>
190+
{text}
191+
</voice>
192+
</speak>
193+
"""
194+
195+
headers = {
196+
"Ocp-Apim-Subscription-Key": settings.AZURE_SPEECH_KEY,
197+
"Content-Type": "application/ssml+xml",
198+
"X-Microsoft-OutputFormat": "riff-24khz-16bit-mono-pcm",
199+
"User-Agent": "Your application name",
200+
}
201+
202+
response = requests.post(url, headers=headers, data=body)
203+
return response.content

apps/ui/src/modals/AIChatModal/components/ChatMessageList/ChatMessageListV2.tsx

+63-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import HumanReply from './components/HumanReply'
1717
import AiReply from './components/AiReply'
1818
import { ReplyStateProps } from '../ReplyBox'
1919

20+
import { ArrowDown } from 'share-ui/components/Icon/Icons'
21+
2022
export enum MessageTypeEnum {
2123
AI_MANUAL = 'AI_MANUAL',
2224
User = 'User',
@@ -49,6 +51,7 @@ const ChatMessageListV2 = ({
4951
const [listIsReady, setListIsReady] = useState(false)
5052

5153
const virtuoso = useRef<VirtuosoHandle>(null)
54+
const scrollerRef = useRef<any>(null)
5255

5356
const filteredData = data?.map((chat: any) => {
5457
const chatDate = moment(chat?.created_on).format('HH:mm')
@@ -95,7 +98,7 @@ const ChatMessageListV2 = ({
9598

9699
const scrollToEnd = () => {
97100
virtuoso.current?.scrollToIndex({
98-
index: initialChat.length + 1,
101+
index: initialChat.length + 2,
99102
align: 'end',
100103
})
101104
}
@@ -150,10 +153,44 @@ const ChatMessageListV2 = ({
150153
// eslint-disable-next-line
151154
}, [sessionId])
152155

156+
const [showScrollButton, setShowScrollButton] = useState(false)
157+
158+
useEffect(() => {
159+
const handleScroll = () => {
160+
if (scrollerRef.current) {
161+
const { scrollTop, clientHeight, scrollHeight } = scrollerRef.current
162+
const bottomScrollPosition = scrollTop + clientHeight
163+
const isScrollable = scrollHeight - bottomScrollPosition > 1 // Using 1 as a threshold
164+
setShowScrollButton(isScrollable)
165+
}
166+
}
167+
168+
const scrollContainer = scrollerRef.current
169+
if (scrollContainer) {
170+
scrollContainer.addEventListener('scroll', handleScroll)
171+
172+
// Initial check in case the list is already scrollable
173+
handleScroll()
174+
}
175+
176+
return () => {
177+
if (scrollContainer) {
178+
scrollContainer.removeEventListener('scroll', handleScroll)
179+
}
180+
}
181+
}, [initialChat])
182+
183+
useEffect(() => {
184+
if (!showScrollButton) scrollToEnd()
185+
}, [data])
186+
153187
return (
154188
<StyledRoot show={true}>
155189
<Virtuoso
156190
ref={virtuoso}
191+
scrollerRef={ref => {
192+
scrollerRef.current = ref
193+
}}
157194
style={{ height: '100%' }}
158195
data={initialChat}
159196
totalCount={data.length}
@@ -253,13 +290,20 @@ const ChatMessageListV2 = ({
253290
</>
254291
)}
255292
/>
293+
{showScrollButton && (
294+
<StyledScrollButton onClick={scrollToEnd}>
295+
<ArrowDown size={18} />
296+
</StyledScrollButton>
297+
)}
256298
</StyledRoot>
257299
)
258300
}
259301

260302
export default memo(ChatMessageListV2)
261303

262304
const StyledRoot = styled.div<{ show: boolean }>`
305+
position: relative;
306+
263307
opacity: 0;
264308
width: 100%;
265309
@@ -331,3 +375,21 @@ const StyledReplyMessageContainer = styled.div`
331375
justify-content: center;
332376
/* align-items: center; */
333377
`
378+
const StyledScrollButton = styled.div`
379+
position: absolute;
380+
bottom: 0;
381+
left: 50%;
382+
transform: translateX(-50%);
383+
384+
cursor: pointer;
385+
386+
width: 38px;
387+
height: 38px;
388+
border-radius: 100px;
389+
background-color: #fff;
390+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); // Add box shadow here
391+
392+
display: flex;
393+
align-items: center;
394+
justify-content: center;
395+
`

apps/ui/src/pages/Agents/AgentForm/AgentForm.tsx

+3-81
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import AgentRunners, { StyledRunnerFieldsWrapper } from './components/AgentRunne
2828
import { isVoiceAgent } from 'utils/agentUtils'
2929

3030
import VoicePreferences from './FormSections/VoicePreferences'
31+
import RadioButton from 'share-ui/components/RadioButton/RadioButton'
3132

3233
type AgentFormProps = {
3334
formik: any
@@ -52,6 +53,8 @@ const AgentForm = ({ formik, isVoice = true }: AgentFormProps) => {
5253
agent_integrations,
5354
agent_type,
5455
agent_sentiment_analyzer,
56+
agent_voice_response,
57+
agent_voice_input_mode,
5558
} = values
5659

5760
const {
@@ -259,87 +262,6 @@ const AgentForm = ({ formik, isVoice = true }: AgentFormProps) => {
259262
voiceSynthesizerOptions={voiceSynthesizerOptions}
260263
voiceTranscriberOptions={voiceTranscriberOptions}
261264
/>
262-
{/* <StyledFormInputWrapper>
263-
<TypographyPrimary
264-
value={t('response-mode')}
265-
type={Typography.types.LABEL}
266-
size={Typography.sizes.md}
267-
/>
268-
269-
<RadioButton
270-
text={t('text')}
271-
name='agent_voice_response'
272-
onSelect={() => setFieldValue('agent_voice_response', ['Text'])}
273-
checked={
274-
agent_voice_response?.length === 1 && agent_voice_response?.includes('Text')
275-
}
276-
/>
277-
<RadioButton
278-
text={t('voice')}
279-
name='agent_voice_response'
280-
onSelect={() => setFieldValue('agent_voice_response', ['Voice'])}
281-
checked={
282-
agent_voice_response?.length === 1 &&
283-
agent_voice_response?.includes('Voice')
284-
}
285-
/>
286-
<RadioButton
287-
text={`${t('text')} & ${t('voice')}`}
288-
name='agent_voice_response'
289-
onSelect={() => setFieldValue('agent_voice_response', ['Text', 'Voice'])}
290-
checked={agent_voice_response?.length === 2}
291-
/>
292-
</StyledFormInputWrapper> */}
293-
294-
{/* <StyledFormInputWrapper>
295-
<TypographyPrimary
296-
value={t('input-mode')}
297-
type={Typography.types.LABEL}
298-
size={Typography.sizes.md}
299-
/>
300-
<StyledCheckboxWrapper>
301-
<Checkbox
302-
label={t('text')}
303-
kind='secondary'
304-
// name='agent_is_template'
305-
checked={agent_voice_input_mode?.includes('Text')}
306-
onChange={() => {
307-
if (agent_voice_input_mode?.includes('Text')) {
308-
const filteredInput = agent_voice_input_mode?.filter(
309-
(input: string) => input !== 'Text',
310-
)
311-
setFieldValue('agent_voice_input_mode', filteredInput)
312-
} else {
313-
setFieldValue('agent_voice_input_mode', [
314-
...agent_voice_input_mode,
315-
'Text',
316-
])
317-
}
318-
}}
319-
/>
320-
</StyledCheckboxWrapper>
321-
<StyledCheckboxWrapper>
322-
<Checkbox
323-
label={t('voice')}
324-
kind='secondary'
325-
// name='agent_is_template'
326-
checked={agent_voice_input_mode?.includes('Voice')}
327-
onChange={() => {
328-
if (agent_voice_input_mode?.includes('Voice')) {
329-
const filteredInput = agent_voice_input_mode?.filter(
330-
(input: string) => input !== 'Voice',
331-
)
332-
setFieldValue('agent_voice_input_mode', filteredInput)
333-
} else {
334-
setFieldValue('agent_voice_input_mode', [
335-
...agent_voice_input_mode,
336-
'Voice',
337-
])
338-
}
339-
}}
340-
/>
341-
</StyledCheckboxWrapper>
342-
</StyledFormInputWrapper> */}
343265
</>
344266
</TabPanel>
345267

0 commit comments

Comments
 (0)