From 719ced580002c001554e45bfbae0f282ea0f1b9d Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Tue, 26 Aug 2025 13:42:27 +0530 Subject: [PATCH 01/10] Add TTS feature with autoplay --- src/components/Bot.tsx | 448 +++++++++++++++++++++++- src/components/bubbles/BotBubble.tsx | 55 ++- src/components/buttons/TTSButton.tsx | 51 +++ src/components/icons/SquareStopIcon.tsx | 19 + src/components/icons/VolumeIcon.tsx | 21 ++ src/components/icons/XIcon.tsx | 2 +- src/components/icons/index.ts | 2 + src/queries/sendMessageQuery.ts | 30 ++ src/utils/index.ts | 2 + 9 files changed, 612 insertions(+), 18 deletions(-) create mode 100644 src/components/buttons/TTSButton.tsx create mode 100644 src/components/icons/SquareStopIcon.tsx create mode 100644 src/components/icons/VolumeIcon.tsx diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index ca42113d0..f15284d2e 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -1,4 +1,4 @@ -import { createSignal, createEffect, For, onMount, Show, mergeProps, on, createMemo } from 'solid-js'; +import { createSignal, createEffect, For, onMount, Show, mergeProps, on, createMemo, onCleanup } from 'solid-js'; import { v4 as uuidv4 } from 'uuid'; import { sendMessageQuery, @@ -8,6 +8,7 @@ import { getChatbotConfig, FeedbackRatingType, createAttachmentWithFormData, + generateTTSQuery, } from '@/queries/sendMessageQuery'; import { TextInput } from './inputs/textInput'; import { GuestBubble } from './bubbles/GuestBubble'; @@ -516,6 +517,21 @@ export const Bot = (botProps: BotProps & { class?: string }) => { const [uploadedFiles, setUploadedFiles] = createSignal<{ file: File; type: string }[]>([]); const [fullFileUploadAllowedTypes, setFullFileUploadAllowedTypes] = createSignal('*'); + // TTS state + const [isTTSLoading, setIsTTSLoading] = createSignal>({}); + const [isTTSPlaying, setIsTTSPlaying] = createSignal>({}); + const [ttsAudio, setTtsAudio] = createSignal>({}); + const [isTTSEnabled, setIsTTSEnabled] = createSignal(false); + const [ttsStreamingState, setTtsStreamingState] = createSignal({ + mediaSource: null as MediaSource | null, + sourceBuffer: null as SourceBuffer | null, + audio: null as HTMLAudioElement | null, + chunkQueue: [] as Uint8Array[], + isBuffering: false, + audioFormat: null as string | null, + abortController: null as AbortController | null, + }); + createMemo(() => { const customerId = (props.chatflowConfig?.vars as any)?.customerId; setChatId(customerId ? `${customerId.toString()}+${uuidv4()}` : uuidv4()); @@ -861,6 +877,15 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setLocalStorageChatflow(chatflowid, chatId); closeResponse(); break; + case 'tts_start': + handleTTSStart(payload.data); + break; + case 'tts_data': + handleTTSDataChunk(payload.data.audioChunk); + break; + case 'tts_end': + handleTTSEnd(); + break; } }, async onclose() { @@ -1401,6 +1426,9 @@ export const Bot = (botProps: BotProps & { class?: string }) => { setFullFileUploadAllowedTypes(chatbotConfig.fullFileUpload?.allowedUploadFileTypes); } } + if (chatbotConfig.isTTSEnabled) { + setIsTTSEnabled(chatbotConfig.isTTSEnabled); + } } // eslint-disable-next-line solid/reactivity @@ -1417,6 +1445,42 @@ export const Bot = (botProps: BotProps & { class?: string }) => { }; }); + // TTS sourceBuffer updateend listener management + let currentSourceBuffer: SourceBuffer | null = null; + let updateEndHandler: (() => void) | null = null; + + createEffect(() => { + const streamingState = ttsStreamingState(); + + // Remove previous listener if sourceBuffer changed + if (currentSourceBuffer && currentSourceBuffer !== streamingState.sourceBuffer && updateEndHandler) { + currentSourceBuffer.removeEventListener('updateend', updateEndHandler); + currentSourceBuffer = null; + updateEndHandler = null; + } + + // Add listener to new sourceBuffer + if (streamingState.sourceBuffer && streamingState.sourceBuffer !== currentSourceBuffer) { + const sourceBuffer = streamingState.sourceBuffer; + currentSourceBuffer = sourceBuffer; + + updateEndHandler = () => { + setTtsStreamingState((prevState) => ({ + ...prevState, + isBuffering: false, + })); + setTimeout(() => processChunkQueue(), 0); + }; + + sourceBuffer.addEventListener('updateend', updateEndHandler); + } + }); + + // TTS cleanup on component unmount + onCleanup(() => { + cleanupTTSStreaming(); + }); + createEffect(() => { if (followUpPromptsStatus() && messages().length > 0) { const lastMessage = messages()[messages().length - 1]; @@ -1680,6 +1744,383 @@ export const Bot = (botProps: BotProps & { class?: string }) => { return false; }; + // TTS Functions + const processChunkQueue = () => { + const currentState = ttsStreamingState(); + if (!currentState.sourceBuffer || currentState.sourceBuffer.updating || currentState.chunkQueue.length === 0) { + return; + } + + const chunk = currentState.chunkQueue[0]; + if (!chunk) return; + + try { + currentState.sourceBuffer.appendBuffer(chunk); + setTtsStreamingState((prevState) => ({ + ...prevState, + chunkQueue: prevState.chunkQueue.slice(1), + isBuffering: true, + })); + } catch (error) { + console.error('Error appending chunk to buffer:', error); + } + }; + + const handleTTSStart = (data: { chatMessageId: string; format: string }) => { + setIsTTSLoading((prevState) => ({ + ...prevState, + [data.chatMessageId]: true, + })); + + setMessages((prevMessages) => { + const allMessages = [...cloneDeep(prevMessages)]; + const lastMessage = allMessages[allMessages.length - 1]; + if (lastMessage.type === 'userMessage') return allMessages; + if (lastMessage.id) return allMessages; + allMessages[allMessages.length - 1].id = data.chatMessageId; + return allMessages; + }); + + setTtsStreamingState({ + mediaSource: null, + sourceBuffer: null, + audio: null, + chunkQueue: [], + isBuffering: false, + audioFormat: data.format, + abortController: null, + }); + + setTimeout(() => initializeTTSStreaming(data), 0); + }; + + const handleTTSDataChunk = (base64Data: string) => { + try { + const audioBuffer = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)); + + setTtsStreamingState((prevState) => { + const newState = { + ...prevState, + chunkQueue: [...prevState.chunkQueue, audioBuffer], + }; + + // Schedule processing after state update + if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { + setTimeout(() => processChunkQueue(), 0); + } + + return newState; + }); + } catch (error) { + console.error('Error handling TTS data chunk:', error); + } + }; + + const handleTTSEnd = () => { + const currentState = ttsStreamingState(); + if (currentState.mediaSource && currentState.mediaSource.readyState === 'open') { + try { + // Process any remaining chunks first + if (currentState.sourceBuffer && currentState.chunkQueue.length > 0) { + let processedCount = 0; + const totalChunks = currentState.chunkQueue.length; + + const processRemainingChunks = () => { + const state = ttsStreamingState(); + if (processedCount < totalChunks && state.sourceBuffer && !state.sourceBuffer.updating) { + const chunk = state.chunkQueue[0]; + if (chunk) { + try { + state.sourceBuffer.appendBuffer(chunk); + setTtsStreamingState((prevState) => ({ + ...prevState, + chunkQueue: prevState.chunkQueue.slice(1), + })); + processedCount++; + } catch (error) { + console.error('Error appending remaining chunk:', error); + } + } + } else if (processedCount >= totalChunks) { + // All chunks processed, end the stream + setTimeout(() => { + const finalState = ttsStreamingState(); + if (finalState.mediaSource && finalState.mediaSource.readyState === 'open') { + finalState.mediaSource.endOfStream(); + } + }, 100); + } + }; + + // Set up listener for processing remaining chunks + if (currentState.sourceBuffer) { + const handleFinalUpdateEnd = () => { + processRemainingChunks(); + }; + currentState.sourceBuffer.addEventListener('updateend', handleFinalUpdateEnd); + processRemainingChunks(); + } + } else if (currentState.sourceBuffer && !currentState.sourceBuffer.updating) { + currentState.mediaSource.endOfStream(); + } + } catch (error) { + console.error('Error ending TTS stream:', error); + } + } + }; + + const initializeTTSStreaming = (data: { chatMessageId: string; format: string }) => { + try { + const mediaSource = new MediaSource(); + const audio = new Audio(); + audio.src = URL.createObjectURL(mediaSource); + + mediaSource.addEventListener('sourceopen', () => { + try { + const mimeType = data.format === 'mp3' ? 'audio/mpeg' : 'audio/mpeg'; + const sourceBuffer = mediaSource.addSourceBuffer(mimeType); + + setTtsStreamingState((prevState) => ({ + ...prevState, + mediaSource, + sourceBuffer, + audio, + })); + + // Start audio playback + audio.play().catch((playError) => { + console.error('Error starting audio playback:', playError); + }); + } catch (error) { + console.error('Error setting up source buffer:', error); + console.error('MediaSource readyState:', mediaSource.readyState); + } + }); + + audio.addEventListener('playing', () => { + setIsTTSLoading((prevState) => { + const newState = { ...prevState }; + newState[data.chatMessageId] = false; + return newState; + }); + setIsTTSPlaying((prevState) => ({ + ...prevState, + [data.chatMessageId]: true, + })); + }); + + audio.addEventListener('ended', () => { + setIsTTSPlaying((prevState) => { + const newState = { ...prevState }; + delete newState[data.chatMessageId]; + return newState; + }); + cleanupTTSStreaming(); + }); + } catch (error) { + console.error('Error initializing TTS streaming:', error); + } + }; + + const cleanupTTSStreaming = () => { + const currentState = ttsStreamingState(); + + if (currentState.abortController) { + currentState.abortController.abort(); + } + + if (currentState.audio) { + currentState.audio.pause(); + currentState.audio.removeAttribute('src'); + if (currentState.audio.src) { + URL.revokeObjectURL(currentState.audio.src); + } + // Remove all event listeners + currentState.audio.removeEventListener('playing'); + currentState.audio.removeEventListener('ended'); + } + + if (currentState.sourceBuffer) { + // Remove update listeners + if (currentState.sourceBuffer.onupdateend) { + currentState.sourceBuffer.removeEventListener('updateend', currentState.sourceBuffer.onupdateend); + currentState.sourceBuffer.onupdateend = null; + } + } + + if (currentState.mediaSource) { + if (currentState.mediaSource.readyState === 'open') { + try { + currentState.mediaSource.endOfStream(); + } catch (e) { + // Ignore errors during cleanup + } + } + } + + setTtsStreamingState({ + mediaSource: null, + sourceBuffer: null, + audio: null, + chunkQueue: [], + isBuffering: false, + audioFormat: null, + abortController: null, + }); + }; + + const handleTTSStop = (messageId: string) => { + const audioElement = ttsAudio()[messageId]; + if (audioElement) { + audioElement.pause(); + audioElement.currentTime = 0; + setTtsAudio((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + } + + const streamingState = ttsStreamingState(); + if (streamingState.audio) { + streamingState.audio.pause(); + cleanupTTSStreaming(); + } + + setIsTTSPlaying((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + + setIsTTSLoading((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + }; + + const stopAllTTS = () => { + const audioElements = ttsAudio(); + Object.keys(audioElements).forEach((messageId) => { + if (audioElements[messageId]) { + audioElements[messageId].pause(); + audioElements[messageId].currentTime = 0; + } + }); + setTtsAudio({}); + + const streamingState = ttsStreamingState(); + if (streamingState.abortController) { + streamingState.abortController.abort(); + } + + if (streamingState.audio) { + streamingState.audio.pause(); + cleanupTTSStreaming(); + } + + setIsTTSPlaying({}); + setIsTTSLoading({}); + }; + + const handleTTSClick = async (messageId: string, messageText: string) => { + const loadingState = isTTSLoading(); + if (loadingState[messageId]) return; + + const playingState = isTTSPlaying(); + const audioElement = ttsAudio()[messageId]; + if (playingState[messageId] || audioElement) { + handleTTSStop(messageId); + return; + } + + stopAllTTS(); + handleTTSStart({ chatMessageId: messageId, format: 'mp3' }); + + try { + const abortController = new AbortController(); + setTtsStreamingState((prev) => ({ ...prev, abortController })); + + const response = await generateTTSQuery({ + apiHost: props.apiHost, + body: { + chatId: chatId(), + chatflowId: props.chatflowid, + chatMessageId: messageId, + text: messageText, + }, + onRequest: props.onRequest, + signal: abortController.signal, + }); + + if (!response.ok) { + throw new Error(`TTS request failed: ${response.status}`); + } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + if (reader) { + let buffer = ''; + let done = false; + while (!done) { + if (abortController.signal.aborted) { + break; + } + + const result = await reader.read(); + done = result.done; + if (done) break; + + const value = result.value; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.trim() && line.startsWith('data: ')) { + try { + const eventData = line.slice(6); + if (eventData === '[DONE]') break; + + const event = JSON.parse(eventData); + switch (event.event) { + case 'tts_start': + break; + case 'tts_data': + if (!abortController.signal.aborted) { + handleTTSDataChunk(event.data.audioChunk); + } + break; + case 'tts_end': + if (!abortController.signal.aborted) { + handleTTSEnd(); + } + break; + } + } catch (parseError) { + console.error('Error parsing SSE event:', parseError); + } + } + } + } + } + } catch (error: any) { + if (error.name === 'AbortError') { + console.error('TTS request was aborted'); + } else { + console.error('Error with TTS:', error); + } + } finally { + setIsTTSLoading((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + } + }; + createEffect( // listen for changes in previews on(previews, (uploads) => { @@ -1856,6 +2297,11 @@ export const Bot = (botProps: BotProps & { class?: string }) => { }} dateTimeToggle={props.dateTimeToggle} renderHTML={props.renderHTML} + isTTSEnabled={isTTSEnabled()} + isTTSLoading={isTTSLoading()} + isTTSPlaying={isTTSPlaying()} + handleTTSClick={handleTTSClick} + handleTTSStop={handleTTSStop} /> )} {message.type === 'leadCaptureMessage' && leadsConfig()?.status && !getLocalStorageChatflow(props.chatflowid)?.lead && ( diff --git a/src/components/bubbles/BotBubble.tsx b/src/components/bubbles/BotBubble.tsx index e747fa6e9..8f5fa308b 100644 --- a/src/components/bubbles/BotBubble.tsx +++ b/src/components/bubbles/BotBubble.tsx @@ -4,6 +4,7 @@ import { Marked } from '@ts-stack/markdown'; import { FeedbackRatingType, sendFeedbackQuery, sendFileDownloadQuery, updateFeedbackQuery } from '@/queries/sendMessageQuery'; import { FileUpload, IAction, MessageType } from '../Bot'; import { CopyToClipboardButton, ThumbsDownButton, ThumbsUpButton } from '../buttons/FeedbackButtons'; +import { TTSButton } from '../buttons/TTSButton'; import FeedbackContentDialog from '../FeedbackContentDialog'; import { AgentReasoningBubble } from './AgentReasoningBubble'; import { TickIcon, XIcon } from '../icons'; @@ -32,6 +33,12 @@ type Props = { renderHTML?: boolean; handleActionClick: (elem: any, action: IAction | undefined | null) => void; handleSourceDocumentsClick: (src: any) => void; + // TTS props + isTTSEnabled?: boolean; + isTTSLoading?: Record; + isTTSPlaying?: Record; + handleTTSClick?: (messageId: string, messageText: string) => void; + handleTTSStop?: (messageId: string) => void; }; const defaultBackgroundColor = '#f7f8ff'; @@ -481,7 +488,7 @@ export const BotBubble = (props: Props) => { {action.label} ) : ( - + )} ); @@ -521,9 +528,25 @@ export const BotBubble = (props: Props) => { )}
- {props.chatFeedbackStatus && props.message.messageId && ( - <> -
+
+ + { + const messageId = props.message.id || ''; + const messageText = props.message.message || ''; + if (props.isTTSPlaying?.[messageId]) { + props.handleTTSStop?.(messageId); + } else { + props.handleTTSClick?.(messageId, messageText); + } + }} + /> + + {props.chatFeedbackStatus && props.message.messageId && ( + <> copyMessageToClipboard()} />
@@ -546,18 +569,18 @@ export const BotBubble = (props: Props) => { {formatDateTime(props.message.dateTime, props?.dateTimeToggle?.date, props?.dateTimeToggle?.time)}
-
- - setShowFeedbackContentModal(false)} - onSubmit={submitFeedbackContent} - backgroundColor={props.backgroundColor} - textColor={props.textColor} - /> - - - )} + + )} +
+ + setShowFeedbackContentModal(false)} + onSubmit={submitFeedbackContent} + backgroundColor={props.backgroundColor} + textColor={props.textColor} + /> +
); diff --git a/src/components/buttons/TTSButton.tsx b/src/components/buttons/TTSButton.tsx new file mode 100644 index 000000000..55985dd57 --- /dev/null +++ b/src/components/buttons/TTSButton.tsx @@ -0,0 +1,51 @@ +import { Show } from 'solid-js'; +import { VolumeIcon, SquareStopIcon } from '../icons'; + +type Props = { + isLoading?: boolean; + isPlaying?: boolean; + feedbackColor?: string; + onClick: () => void; + class?: string; +}; + +const defaultButtonColor = '#3B81F6'; + +export const TTSButton = (props: Props) => { + const handleClick = (event: MouseEvent) => { + event.preventDefault(); + props.onClick(); + }; + + return ( + + ); +}; diff --git a/src/components/icons/SquareStopIcon.tsx b/src/components/icons/SquareStopIcon.tsx new file mode 100644 index 000000000..6adbe6152 --- /dev/null +++ b/src/components/icons/SquareStopIcon.tsx @@ -0,0 +1,19 @@ +import { JSX } from 'solid-js/jsx-runtime'; +const defaultButtonColor = '#3B81F6'; +export const SquareStopIcon = (props: JSX.SvgSVGAttributes) => ( + + + +); diff --git a/src/components/icons/VolumeIcon.tsx b/src/components/icons/VolumeIcon.tsx new file mode 100644 index 000000000..b5e0fcaf9 --- /dev/null +++ b/src/components/icons/VolumeIcon.tsx @@ -0,0 +1,21 @@ +import { JSX } from 'solid-js/jsx-runtime'; +const defaultButtonColor = '#3B81F6'; +export const VolumeIcon = (props: JSX.SvgSVGAttributes) => ( + + + + + +); diff --git a/src/components/icons/XIcon.tsx b/src/components/icons/XIcon.tsx index 98af59746..6587e92ab 100644 --- a/src/components/icons/XIcon.tsx +++ b/src/components/icons/XIcon.tsx @@ -7,7 +7,7 @@ export const XIcon = (props: JSX.SvgSVGAttributes & { isCurrentCo height="24" viewBox="0 0 24 24" fill="none" - stroke={props.isCurrentColor ? 'currentColor' : props.color ?? defaultButtonColor} + stroke={props.isCurrentColor ? 'currentColor' : (props.color ?? defaultButtonColor)} stroke-width="2" stroke-linecap="round" stroke-linejoin="round" diff --git a/src/components/icons/index.ts b/src/components/icons/index.ts index 5f3e3496e..543629d55 100644 --- a/src/components/icons/index.ts +++ b/src/components/icons/index.ts @@ -11,3 +11,5 @@ export * from './XIcon'; export * from './TickIcon'; export * from './AttachmentIcon'; export * from './SparklesIcon'; +export * from './VolumeIcon'; +export * from './SquareStopIcon'; diff --git a/src/queries/sendMessageQuery.ts b/src/queries/sendMessageQuery.ts index cbea3e745..60523bc12 100644 --- a/src/queries/sendMessageQuery.ts +++ b/src/queries/sendMessageQuery.ts @@ -61,6 +61,16 @@ export type LeadCaptureRequest = BaseRequest & { body: Partial; }; +export type GenerateTTSRequest = BaseRequest & { + body: { + chatId: string; + chatflowId: string; + chatMessageId: string; + text: string; + }; + signal?: AbortSignal; +}; + export const sendFeedbackQuery = ({ chatflowid, apiHost = 'http://localhost:3000', body, onRequest }: CreateFeedbackRequest) => sendRequest({ method: 'POST', @@ -137,3 +147,23 @@ export const addLeadQuery = ({ apiHost = 'http://localhost:3000', body, onReques body, onRequest: onRequest, }); + +export const generateTTSQuery = async ({ apiHost = 'http://localhost:3000', body, onRequest, signal }: GenerateTTSRequest): Promise => { + const headers = { + 'Content-Type': 'application/json', + }; + + const requestInfo: RequestInit = { + method: 'POST', + mode: 'cors', + headers, + body: JSON.stringify(body), + signal, + }; + + if (onRequest) { + await onRequest(requestInfo); + } + + return fetch(`${apiHost}/api/v1/text-to-speech/generate`, requestInfo); +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 3285ec0b8..8c291db0e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -16,6 +16,7 @@ export const sendRequest = async ( headers?: Record; formData?: FormData; onRequest?: (request: RequestInit) => Promise; + signal?: AbortSignal; } | string, ): Promise<{ data?: ResponseData; error?: Error }> => { @@ -36,6 +37,7 @@ export const sendRequest = async ( mode: 'cors', headers, body, + signal: typeof params !== 'string' ? params.signal : undefined, }; if (typeof params !== 'string' && params.onRequest) { From 204c5a6dd35d440b61949561b20462a7a3367a64 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Tue, 26 Aug 2025 13:44:54 +0530 Subject: [PATCH 02/10] Fix lint errors and formatting --- src/components/Bot.tsx | 4 ++-- src/components/buttons/TTSButton.tsx | 4 +++- src/components/icons/XIcon.tsx | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Bot.tsx b/src/components/Bot.tsx index f15284d2e..03730d543 100644 --- a/src/components/Bot.tsx +++ b/src/components/Bot.tsx @@ -1936,8 +1936,8 @@ export const Bot = (botProps: BotProps & { class?: string }) => { URL.revokeObjectURL(currentState.audio.src); } // Remove all event listeners - currentState.audio.removeEventListener('playing'); - currentState.audio.removeEventListener('ended'); + currentState.audio.removeEventListener('playing', () => console.log('Playing')); + currentState.audio.removeEventListener('ended', () => console.log('Ended')); } if (currentState.sourceBuffer) { diff --git a/src/components/buttons/TTSButton.tsx b/src/components/buttons/TTSButton.tsx index 55985dd57..4b1135485 100644 --- a/src/components/buttons/TTSButton.tsx +++ b/src/components/buttons/TTSButton.tsx @@ -19,7 +19,9 @@ export const TTSButton = (props: Props) => { return (
+ + + +
+ ) +} + +ChatMessage.propTypes = { + open: PropTypes.bool, + chatflowid: PropTypes.string, + isAgentCanvas: PropTypes.bool, + isDialog: PropTypes.bool, + previews: PropTypes.array, + setPreviews: PropTypes.func +} + +export default memo(ChatMessage) diff --git a/src/components/bubbles/BotBubble.tsx b/src/components/bubbles/BotBubble.tsx index 228bea69b..c7aae9320 100644 --- a/src/components/bubbles/BotBubble.tsx +++ b/src/components/bubbles/BotBubble.tsx @@ -395,11 +395,6 @@ export const BotBubble = (props: Props) => { } }; - createEffect(() => { - console.log('messageId', props.message.id || props.message.messageId); - console.log('isTTSLoading', props.isTTSLoading); - }); - return (
diff --git a/src/queries/sendMessageQuery.ts b/src/queries/sendMessageQuery.ts index 8a3cb9f66..daf380528 100644 --- a/src/queries/sendMessageQuery.ts +++ b/src/queries/sendMessageQuery.ts @@ -73,6 +73,7 @@ export type GenerateTTSRequest = BaseRequest & { export type AbortTTSRequest = BaseRequest & { body: { + chatflowId: string; chatId: string; chatMessageId: string; }; From 8086b38e3df6ffc4c4a91fc2114c82fd392c6621 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 26 Sep 2025 13:48:52 +0530 Subject: [PATCH 07/10] Apply prettier --- src/components/ChatMessage.jsx | 5860 ++++++++++++++++---------------- 1 file changed, 2869 insertions(+), 2991 deletions(-) diff --git a/src/components/ChatMessage.jsx b/src/components/ChatMessage.jsx index a70fa5726..7baa627ed 100644 --- a/src/components/ChatMessage.jsx +++ b/src/components/ChatMessage.jsx @@ -1,3068 +1,2946 @@ -import { useState, useRef, useEffect, useCallback, Fragment, useContext, memo } from 'react' -import { useSelector, useDispatch } from 'react-redux' -import PropTypes from 'prop-types' -import { cloneDeep } from 'lodash' -import axios from 'axios' -import { v4 as uuidv4 } from 'uuid' -import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source' +import { useState, useRef, useEffect, useCallback, Fragment, useContext, memo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { cloneDeep } from 'lodash'; +import axios from 'axios'; +import { v4 as uuidv4 } from 'uuid'; +import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'; import { - Box, - Button, - Card, - CardMedia, - Chip, - CircularProgress, - Divider, - IconButton, - InputAdornment, - OutlinedInput, - Typography, - Stack, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - TextField -} from '@mui/material' -import { darken, useTheme } from '@mui/material/styles' + Box, + Button, + Card, + CardMedia, + Chip, + CircularProgress, + Divider, + IconButton, + InputAdornment, + OutlinedInput, + Typography, + Stack, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, +} from '@mui/material'; +import { darken, useTheme } from '@mui/material/styles'; import { - IconCircleDot, - IconDownload, - IconSend, - IconMicrophone, - IconPhotoPlus, - IconTrash, - IconX, - IconTool, - IconSquareFilled, - IconCheck, - IconPaperclip, - IconSparkles, - IconVolume -} from '@tabler/icons-react' -import robotPNG from '@/assets/images/robot.png' -import userPNG from '@/assets/images/account.png' -import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png' -import multiagent_workerPNG from '@/assets/images/multiagent_worker.png' -import audioUploadSVG from '@/assets/images/wave-sound.jpg' + IconCircleDot, + IconDownload, + IconSend, + IconMicrophone, + IconPhotoPlus, + IconTrash, + IconX, + IconTool, + IconSquareFilled, + IconCheck, + IconPaperclip, + IconSparkles, + IconVolume, +} from '@tabler/icons-react'; +import robotPNG from '@/assets/images/robot.png'; +import userPNG from '@/assets/images/account.png'; +import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'; +import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'; +import audioUploadSVG from '@/assets/images/wave-sound.jpg'; // project import -import NodeInputHandler from '@/views/canvas/NodeInputHandler' -import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' -import { SafeHTML } from '@/ui-component/safe/SafeHTML' -import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog' -import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog' -import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard' -import AgentReasoningCard from './AgentReasoningCard' -import AgentExecutedDataCard from './AgentExecutedDataCard' -import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton' -import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton' -import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton' -import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton' -import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording' -import './audio-recording.css' -import './ChatMessage.css' +import NodeInputHandler from '@/views/canvas/NodeInputHandler'; +import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'; +import { SafeHTML } from '@/ui-component/safe/SafeHTML'; +import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'; +import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'; +import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'; +import AgentReasoningCard from './AgentReasoningCard'; +import AgentExecutedDataCard from './AgentExecutedDataCard'; +import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'; +import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'; +import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'; +import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton'; +import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'; +import './audio-recording.css'; +import './ChatMessage.css'; // api -import chatmessageApi from '@/api/chatmessage' -import chatflowsApi from '@/api/chatflows' -import predictionApi from '@/api/prediction' -import vectorstoreApi from '@/api/vectorstore' -import attachmentsApi from '@/api/attachments' -import chatmessagefeedbackApi from '@/api/chatmessagefeedback' -import leadsApi from '@/api/lead' -import executionsApi from '@/api/executions' -import ttsApi from '@/api/tts' +import chatmessageApi from '@/api/chatmessage'; +import chatflowsApi from '@/api/chatflows'; +import predictionApi from '@/api/prediction'; +import vectorstoreApi from '@/api/vectorstore'; +import attachmentsApi from '@/api/attachments'; +import chatmessagefeedbackApi from '@/api/chatmessagefeedback'; +import leadsApi from '@/api/lead'; +import executionsApi from '@/api/executions'; +import ttsApi from '@/api/tts'; // Hooks -import useApi from '@/hooks/useApi' -import { flowContext } from '@/store/context/ReactFlowContext' +import useApi from '@/hooks/useApi'; +import { flowContext } from '@/store/context/ReactFlowContext'; // Const -import { baseURL, maxScroll } from '@/store/constant' -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import { baseURL, maxScroll } from '@/store/constant'; +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'; // Utils -import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper' -import useNotifier from '@/utils/useNotifier' -import FollowUpPromptsCard from '@/ui-component/cards/FollowUpPromptsCard' +import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper'; +import useNotifier from '@/utils/useNotifier'; +import FollowUpPromptsCard from '@/ui-component/cards/FollowUpPromptsCard'; // History -import { ChatInputHistory } from './ChatInputHistory' +import { ChatInputHistory } from './ChatInputHistory'; const messageImageStyle = { - width: '128px', - height: '128px', - objectFit: 'cover' -} + width: '128px', + height: '128px', + objectFit: 'cover', +}; const CardWithDeleteOverlay = ({ item, disabled, customization, onDelete }) => { - const [isHovered, setIsHovered] = useState(false) - const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent' - - return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - style={{ position: 'relative', display: 'inline-block' }} - > - - - - {item.name} - - - {isHovered && !disabled && ( - - )} -
- ) -} - -CardWithDeleteOverlay.propTypes = { - item: PropTypes.object, - customization: PropTypes.object, - disabled: PropTypes.bool, - onDelete: PropTypes.func -} - -const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => { - const theme = useTheme() - const customization = useSelector((state) => state.customization) - - const ps = useRef() - - const dispatch = useDispatch() - const { onAgentflowNodeStatusUpdate, clearAgentflowNodeStatus } = useContext(flowContext) - - useNotifier() - const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) - const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) - - const [userInput, setUserInput] = useState('') - const [loading, setLoading] = useState(false) - const [messages, setMessages] = useState([ - { - message: 'Hi there! How can I help?', - type: 'apiMessage' - } - ]) - const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false) - const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false) - const [sourceDialogOpen, setSourceDialogOpen] = useState(false) - const [sourceDialogProps, setSourceDialogProps] = useState({}) - const [chatId, setChatId] = useState(uuidv4()) - const [isMessageStopping, setIsMessageStopping] = useState(false) - const [uploadedFiles, setUploadedFiles] = useState([]) - const [imageUploadAllowedTypes, setImageUploadAllowedTypes] = useState('') - const [fileUploadAllowedTypes, setFileUploadAllowedTypes] = useState('') - const [inputHistory] = useState(new ChatInputHistory(10)) - - const inputRef = useRef(null) - const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow) - const getAllExecutionsApi = useApi(executionsApi.getAllExecutions) - const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming) - const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads) - const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow) - - const [starterPrompts, setStarterPrompts] = useState([]) - - // full file upload - const [fullFileUpload, setFullFileUpload] = useState(false) - const [fullFileUploadAllowedTypes, setFullFileUploadAllowedTypes] = useState('*') - - // feedback - const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false) - const [feedbackId, setFeedbackId] = useState('') - const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false) - - // leads - const [leadsConfig, setLeadsConfig] = useState(null) - const [leadName, setLeadName] = useState('') - const [leadEmail, setLeadEmail] = useState('') - const [leadPhone, setLeadPhone] = useState('') - const [isLeadSaving, setIsLeadSaving] = useState(false) - const [isLeadSaved, setIsLeadSaved] = useState(false) - - // follow-up prompts - const [followUpPromptsStatus, setFollowUpPromptsStatus] = useState(false) - const [followUpPrompts, setFollowUpPrompts] = useState([]) - - // drag & drop and file input - const imgUploadRef = useRef(null) - const fileUploadRef = useRef(null) - const [isChatFlowAvailableForImageUploads, setIsChatFlowAvailableForImageUploads] = useState(false) - const [isChatFlowAvailableForFileUploads, setIsChatFlowAvailableForFileUploads] = useState(false) - const [isChatFlowAvailableForRAGFileUploads, setIsChatFlowAvailableForRAGFileUploads] = useState(false) - const [isDragActive, setIsDragActive] = useState(false) - - // recording - const [isRecording, setIsRecording] = useState(false) - const [recordingNotSupported, setRecordingNotSupported] = useState(false) - const [isLoadingRecording, setIsLoadingRecording] = useState(false) - - const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false) - const [feedback, setFeedback] = useState('') - const [pendingActionData, setPendingActionData] = useState(null) - const [feedbackType, setFeedbackType] = useState('') - - // start input type - const [startInputType, setStartInputType] = useState('') - const [formTitle, setFormTitle] = useState('') - const [formDescription, setFormDescription] = useState('') - const [formInputsData, setFormInputsData] = useState({}) - const [formInputParams, setFormInputParams] = useState([]) - - const [isConfigLoading, setIsConfigLoading] = useState(true) - - // TTS state - const [isTTSLoading, setIsTTSLoading] = useState({}) - const [isTTSPlaying, setIsTTSPlaying] = useState({}) - const [ttsAudio, setTtsAudio] = useState({}) - const [isTTSEnabled, setIsTTSEnabled] = useState(false) - - // TTS streaming state - const [ttsStreamingState, setTtsStreamingState] = useState({ - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: null, - abortController: null - }) - - // Ref to prevent auto-scroll during TTS actions (using ref to avoid re-renders) - const isTTSActionRef = useRef(false) - const ttsTimeoutRef = useRef(null) - - const isFileAllowedForUpload = (file) => { - const constraints = getAllowChatFlowUploads.data - /** - * {isImageUploadAllowed: boolean, imgUploadSizeAndTypes: Array<{ fileTypes: string[], maxUploadSize: number }>} - */ - let acceptFile = false - - // Early return if constraints are not available yet - if (!constraints) { - console.warn('Upload constraints not loaded yet') - return false - } - - if (constraints.isImageUploadAllowed) { - const fileType = file.type - const sizeInMB = file.size / 1024 / 1024 - if (constraints.imgUploadSizeAndTypes && Array.isArray(constraints.imgUploadSizeAndTypes)) { - constraints.imgUploadSizeAndTypes.forEach((allowed) => { - if (allowed.fileTypes && allowed.fileTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { - acceptFile = true - } - }) - } - } - - if (fullFileUpload) { - return true - } else if (constraints.isRAGFileUploadAllowed) { - const fileExt = file.name.split('.').pop() - if (fileExt && constraints.fileUploadSizeAndTypes && Array.isArray(constraints.fileUploadSizeAndTypes)) { - constraints.fileUploadSizeAndTypes.forEach((allowed) => { - if (allowed.fileTypes && allowed.fileTypes.length === 1 && allowed.fileTypes[0] === '*') { - acceptFile = true - } else if (allowed.fileTypes && allowed.fileTypes.includes(`.${fileExt}`)) { - 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 (!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads) { - return - } - e.preventDefault() - setIsDragActive(false) - let files = [] - let uploadedFiles = [] - - 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 - // Only add files - if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { - uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }) - } - 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 { - 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) - setUploadedFiles(uploadedFiles) - setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) - } - - if (e.dataTransfer.items) { - //TODO set files - 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 ? 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 ? s.substring(s.indexOf('href') + 6) : '' - let hrefStr = start.substring(0, start.indexOf('"')) - - let upload = { - data: hrefStr, - preview: hrefStr, - type: 'url', - name: hrefStr ? 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 = [] - let uploadedFiles = [] - for (const file of event.target.files) { - if (isFileAllowedForUpload(file) === false) { - return - } - // Only add files - if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { - uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }) - } - 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) - setUploadedFiles(uploadedFiles) - setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]) - // 👇️ reset file input - event.target.value = null - } - - const addRecordingToPreviews = (blob) => { - let mimeType = '' - const pos = blob.type.indexOf(';') - if (pos === -1) { - mimeType = blob.type - } else { - mimeType = blob.type ? blob.type.substring(0, pos) : '' - } - // 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_${Date.now()}.wav`, - mime: mimeType - } - setPreviews((prevPreviews) => [...prevPreviews, upload]) - } - } - - const handleDrag = (e) => { - if (isChatFlowAvailableForImageUploads || isChatFlowAvailableForFileUploads) { - e.preventDefault() - e.stopPropagation() - if (e.type === 'dragenter' || e.type === 'dragover') { - setIsDragActive(true) - } else if (e.type === 'dragleave') { - setIsDragActive(false) - } - } - } - - const handleAbort = async () => { - setIsMessageStopping(true) - try { - // Stop all TTS streams first - stopAllTTS() - - // Abort TTS for any active streams - const activeTTSMessages = Object.keys(isTTSLoading).concat(Object.keys(isTTSPlaying)) - for (const messageId of activeTTSMessages) { - await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }) - } - - await chatmessageApi.abortMessage(chatflowid, chatId) - } catch (error) { - setIsMessageStopping(false) - enqueueSnackbar({ - message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ) - } - }) - } - } - - const handleDeletePreview = (itemToDelete) => { - if (itemToDelete.type === 'file') { - URL.revokeObjectURL(itemToDelete.preview) // Clean up for file - } - setPreviews(previews.filter((item) => item !== itemToDelete)) - } - - const handleFileUploadClick = () => { - // 👇️ open file input box on click of another element - fileUploadRef.current.click() - } - - const handleImageUploadClick = () => { - // 👇️ open file input box on click of another element - imgUploadRef.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) - } - - const onURLClick = (data) => { - window.open(data, '_blank') - } - - const scrollToBottom = () => { - if (ps.current) { - ps.current.scrollTo({ top: maxScroll }) - } - } - - // Helper function to manage TTS action flag - const setTTSAction = (isActive) => { - isTTSActionRef.current = isActive - if (ttsTimeoutRef.current) { - clearTimeout(ttsTimeoutRef.current) - ttsTimeoutRef.current = null - } - if (isActive) { - // Reset the flag after a longer delay to ensure all state changes are complete - ttsTimeoutRef.current = setTimeout(() => { - isTTSActionRef.current = false - ttsTimeoutRef.current = null - }, 300) - } - } - - const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput]) - - const updateLastMessage = (text) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].message += text - allMessages[allMessages.length - 1].feedback = null - return allMessages - }) - } - - const updateErrorMessage = (errorMessage) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - allMessages.push({ message: errorMessage, type: 'apiMessage' }) - return allMessages - }) - } - - const updateLastMessageSourceDocuments = (sourceDocuments) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].sourceDocuments = sourceDocuments - return allMessages - }) - } - - const updateLastMessageAgentReasoning = (agentReasoning) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].agentReasoning = agentReasoning - return allMessages - }) - } - - const updateAgentFlowEvent = (event) => { - if (event === 'INPROGRESS') { - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage', agentFlowEventStatus: event }]) - } else { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].agentFlowEventStatus = event - return allMessages - }) - } - } - - const updateAgentFlowExecutedData = (agentFlowExecutedData) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].agentFlowExecutedData = agentFlowExecutedData - return allMessages - }) - } - - const updateLastMessageAction = (action) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].action = action - return allMessages - }) - } - - const updateLastMessageArtifacts = (artifacts) => { - artifacts.forEach((artifact) => { - if (artifact.type === 'png' || artifact.type === 'jpeg') { - artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( - 'FILE-STORAGE::', - '' - )}` - } - }) - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].artifacts = artifacts - return allMessages - }) - } - - const updateLastMessageNextAgent = (nextAgent) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning - if (lastAgentReasoning && lastAgentReasoning.length > 0) { - lastAgentReasoning.push({ nextAgent }) - } - allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning - return allMessages - }) - } - - const updateLastMessageNextAgentFlow = (nextAgentFlow) => { - onAgentflowNodeStatusUpdate(nextAgentFlow) - } - - const updateLastMessageUsedTools = (usedTools) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].usedTools = usedTools - return allMessages - }) - } - - const updateLastMessageFileAnnotations = (fileAnnotations) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations - return allMessages - }) - } - - const abortMessage = () => { - setIsMessageStopping(false) - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning - if (lastAgentReasoning && lastAgentReasoning.length > 0) { - allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning.filter((reasoning) => !reasoning.nextAgent) - } - return allMessages - }) - setTimeout(() => { - inputRef.current?.focus() - }, 100) - enqueueSnackbar({ - message: 'Message stopped', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - } - - const handleError = (message = 'Oops! There seems to be an error. Please try again.') => { - message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, '') - setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]) - setLoading(false) - setUserInput('') - setUploadedFiles([]) - setTimeout(() => { - inputRef.current?.focus() - }, 100) - } - - const handlePromptClick = async (promptStarterInput) => { - setUserInput(promptStarterInput) - handleSubmit(undefined, promptStarterInput) - } - - const handleFollowUpPromptClick = async (promptStarterInput) => { - setUserInput(promptStarterInput) - setFollowUpPrompts([]) - handleSubmit(undefined, promptStarterInput) - } - - const onSubmitResponse = (actionData, feedback = '', type = '') => { - let fbType = feedbackType - if (type) { - fbType = type - } - const question = feedback ? feedback : fbType.charAt(0).toUpperCase() + fbType.slice(1) - handleSubmit(undefined, question, undefined, { - type: fbType, - startNodeId: actionData?.nodeId, - feedback - }) - } - - const handleSubmitFeedback = () => { - if (pendingActionData) { - onSubmitResponse(pendingActionData, feedback) - setOpenFeedbackDialog(false) - setFeedback('') - setPendingActionData(null) - setFeedbackType('') - } - } - - const handleActionClick = async (elem, action) => { - setUserInput(elem.label) - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages - allMessages[allMessages.length - 1].action = null - return allMessages - }) - if (elem.type.includes('agentflowv2')) { - const type = elem.type.includes('approve') ? 'proceed' : 'reject' - setFeedbackType(type) - - if (action.data && action.data.input && action.data.input.humanInputEnableFeedback) { - setPendingActionData(action.data) - setOpenFeedbackDialog(true) - } else { - onSubmitResponse(action.data, '', type) - } - } else { - handleSubmit(undefined, elem.label, action) - } - } - - const updateMetadata = (data, input) => { - // set message id that is needed for feedback - if (data.chatMessageId) { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type === 'apiMessage') { - allMessages[allMessages.length - 1].id = data.chatMessageId - } - return allMessages - }) - } - - if (data.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 (data.followUpPrompts) { - const followUpPrompts = JSON.parse(data.followUpPrompts) - if (typeof followUpPrompts === 'string') { - setFollowUpPrompts(JSON.parse(followUpPrompts)) - } else { - setFollowUpPrompts(followUpPrompts) - } - } - } - - const handleFileUploads = async (uploads) => { - if (!uploadedFiles.length) return uploads - - if (fullFileUpload) { - const filesWithFullUploadType = uploadedFiles.filter((file) => file.type === 'file:full') - if (filesWithFullUploadType.length > 0) { - const formData = new FormData() - for (const file of filesWithFullUploadType) { - formData.append('files', file.file) - } - formData.append('chatId', chatId) - - const response = await attachmentsApi.createAttachment(chatflowid, chatId, formData) - const data = response.data - - for (const extractedFileData of data) { - const content = extractedFileData.content - const fileName = extractedFileData.name - - // find matching name in previews and replace data with content - const uploadIndex = uploads.findIndex((upload) => upload.name === fileName) - - if (uploadIndex !== -1) { - uploads[uploadIndex] = { - ...uploads[uploadIndex], - data: content, - name: fileName, - type: 'file:full' - } - } - } - } - } else if (isChatFlowAvailableForRAGFileUploads) { - const filesWithRAGUploadType = uploadedFiles.filter((file) => file.type === 'file:rag') - - if (filesWithRAGUploadType.length > 0) { - const formData = new FormData() - for (const file of filesWithRAGUploadType) { - formData.append('files', file.file) - } - formData.append('chatId', chatId) - - await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData) - - // delay for vector store to be updated - const delay = (delayInms) => { - return new Promise((resolve) => setTimeout(resolve, delayInms)) - } - await delay(2500) //TODO: check if embeddings can be retrieved using file name as metadata filter - - uploads = uploads.map((upload) => { - return { - ...upload, - type: 'file:rag' - } - }) - } - } - return uploads - } - - // Handle form submission - const handleSubmit = async (e, selectedInput, action, humanInput) => { - if (e) e.preventDefault() - - if (!selectedInput && userInput.trim() === '') { - const containsFile = previews.filter((item) => !item.mime.startsWith('image') && item.type !== 'audio').length > 0 - if (!previews.length || (previews.length && containsFile)) { - return - } - } - - let input = userInput - - if (typeof selectedInput === 'string') { - if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput - - if (input.trim()) { - inputHistory.addToHistory(input) - } - } else if (typeof selectedInput === 'object') { - input = Object.entries(selectedInput) - .map(([key, value]) => `${key}: ${value}`) - .join('\n') - } - - setLoading(true) - clearAgentflowNodeStatus() - - let uploads = previews.map((item) => { - return { - data: item.data, - type: item.type, - name: item.name, - mime: item.mime - } - }) - - try { - uploads = await handleFileUploads(uploads) - } catch (error) { - handleError('Unable to upload documents') - return - } - - clearPreviews() - setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: uploads }]) - - // Send user question to Prediction Internal API - try { - const params = { - question: input, - chatId - } - if (typeof selectedInput === 'object') { - params.form = selectedInput - delete params.question - } - if (uploads && uploads.length > 0) params.uploads = uploads - if (leadEmail) params.leadEmail = leadEmail - if (action) params.action = action - if (humanInput) params.humanInput = humanInput - - if (isChatFlowAvailableToStream) { - fetchResponseFromEventStream(chatflowid, params) - } else { - const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params) - if (response.data) { - const data = response.data - - updateMetadata(data, input) - - let text = '' - if (data.text) text = data.text - else if (data.json) text = '```json\n' + JSON.stringify(data.json, null, 2) - else text = JSON.stringify(data, null, 2) - - setMessages((prevMessages) => [ - ...prevMessages, - { - message: text, - id: data?.chatMessageId, - sourceDocuments: data?.sourceDocuments, - usedTools: data?.usedTools, - calledTools: data?.calledTools, - fileAnnotations: data?.fileAnnotations, - agentReasoning: data?.agentReasoning, - agentFlowExecutedData: data?.agentFlowExecutedData, - action: data?.action, - artifacts: data?.artifacts, - type: 'apiMessage', - feedback: null - } - ]) - - setLocalStorageChatflow(chatflowid, data.chatId) - setLoading(false) - setUserInput('') - setUploadedFiles([]) - - setTimeout(() => { - inputRef.current?.focus() - scrollToBottom() - }, 100) - } - } - } catch (error) { - handleError(error.response.data.message) - return - } - } - - const fetchResponseFromEventStream = async (chatflowid, params) => { - const chatId = params.chatId - const input = params.question - params.streaming = true - await fetchEventSource(`${baseURL}/api/v1/internal-prediction/${chatflowid}`, { - openWhenHidden: true, - method: 'POST', - body: JSON.stringify(params), - headers: { - 'Content-Type': 'application/json', - 'x-request-from': 'internal' - }, - async onopen(response) { - if (response.ok && response.headers.get('content-type') === EventStreamContentType) { - //console.log('EventSource Open') - } - }, - async onmessage(ev) { - const payload = JSON.parse(ev.data) - switch (payload.event) { - case 'start': - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }]) - break - case 'token': - updateLastMessage(payload.data) - break - case 'sourceDocuments': - updateLastMessageSourceDocuments(payload.data) - break - case 'usedTools': - updateLastMessageUsedTools(payload.data) - break - case 'fileAnnotations': - updateLastMessageFileAnnotations(payload.data) - break - case 'agentReasoning': - updateLastMessageAgentReasoning(payload.data) - break - case 'agentFlowEvent': - updateAgentFlowEvent(payload.data) - break - case 'agentFlowExecutedData': - updateAgentFlowExecutedData(payload.data) - break - case 'artifacts': - updateLastMessageArtifacts(payload.data) - break - case 'action': - updateLastMessageAction(payload.data) - break - case 'nextAgent': - updateLastMessageNextAgent(payload.data) - break - case 'nextAgentFlow': - updateLastMessageNextAgentFlow(payload.data) - break - case 'metadata': - updateMetadata(payload.data, input) - break - case 'error': - updateErrorMessage(payload.data) - break - case 'abort': - abortMessage(payload.data) - closeResponse() - break - case 'tts_start': - handleTTSStart(payload.data) - break - case 'tts_data': - handleTTSDataChunk(payload.data.audioChunk) - break - case 'tts_end': - handleTTSEnd() - break - case 'tts_abort': - handleTTSAbort(payload.data) - break - case 'end': - setLocalStorageChatflow(chatflowid, chatId) - closeResponse() - break - } - }, - async onclose() { - closeResponse() - }, - async onerror(err) { - console.error('EventSource Error: ', err) - closeResponse() - throw err - } - }) - } - - const closeResponse = () => { - setLoading(false) - setUserInput('') - setUploadedFiles([]) - setTimeout(() => { - inputRef.current?.focus() - scrollToBottom() - }, 100) - } - // Prevent blank submissions and allow for multiline input - const handleEnter = (e) => { - // Check if IME composition is in progress - const isIMEComposition = e.isComposing || e.keyCode === 229 - if (e.key === 'ArrowUp' && !isIMEComposition) { - e.preventDefault() - const previousInput = inputHistory.getPreviousInput(userInput) - setUserInput(previousInput) - } else if (e.key === 'ArrowDown' && !isIMEComposition) { - e.preventDefault() - const nextInput = inputHistory.getNextInput() - setUserInput(nextInput) - } else if (e.key === 'Enter' && userInput && !isIMEComposition) { - if (!e.shiftKey && userInput) { - handleSubmit(e) - } - } else if (e.key === 'Enter') { - e.preventDefault() - } - } - - const getLabel = (URL, source) => { - if (URL && typeof URL === 'object') { - if (URL.pathname && typeof URL.pathname === 'string') { - if (URL.pathname.substring(0, 15) === '/') { - return URL.host || '' - } else { - return `${URL.pathname.substring(0, 15)}...` - } - } else if (URL.host) { - return URL.host - } - } - - if (source && source.pageContent && typeof source.pageContent === 'string') { - return `${source.pageContent.substring(0, 15)}...` - } - - return '' - } - - const getFileUploadAllowedTypes = () => { - if (fullFileUpload) { - return fullFileUploadAllowedTypes === '' ? '*' : fullFileUploadAllowedTypes - } - return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*' - } - - const downloadFile = async (fileAnnotation) => { - try { - const response = await axios.post( - `${baseURL}/api/v1/openai-assistants-file/download`, - { fileName: fileAnnotation.fileName, chatflowId: chatflowid, chatId: chatId }, - { responseType: 'blob' } - ) - const blob = new Blob([response.data], { type: response.headers['content-type'] }) - const downloadUrl = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = downloadUrl - link.download = fileAnnotation.fileName - document.body.appendChild(link) - link.click() - link.remove() - } catch (error) { - console.error('Download failed:', error) - } - } - - const getAgentIcon = (nodeName, instructions) => { - if (nodeName) { - return `${baseURL}/api/v1/node-icon/${nodeName}` - } else if (instructions) { - return multiagent_supervisorPNG - } else { - return multiagent_workerPNG - } - } - - // Get chatmessages successful - useEffect(() => { - if (getChatmessageApi.data?.length) { - const chatId = getChatmessageApi.data[0]?.chatId - setChatId(chatId) - const loadedMessages = getChatmessageApi.data.map((message) => { - const obj = { - id: message.id, - message: message.content, - feedback: message.feedback, - type: message.role - } - if (message.sourceDocuments) obj.sourceDocuments = message.sourceDocuments - if (message.usedTools) obj.usedTools = message.usedTools - if (message.fileAnnotations) obj.fileAnnotations = message.fileAnnotations - if (message.agentReasoning) obj.agentReasoning = message.agentReasoning - if (message.action) obj.action = message.action - if (message.artifacts) { - obj.artifacts = message.artifacts - obj.artifacts.forEach((artifact) => { - if (artifact.type === 'png' || artifact.type === 'jpeg') { - artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( - 'FILE-STORAGE::', - '' - )}` - } - }) - } - if (message.fileUploads) { - obj.fileUploads = 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}` - } - }) - } - if (message.followUpPrompts) obj.followUpPrompts = JSON.parse(message.followUpPrompts) - if (message.role === 'apiMessage' && message.execution && message.execution.executionData) - obj.agentFlowExecutedData = JSON.parse(message.execution.executionData) - return obj - }) - setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) - setLocalStorageChatflow(chatflowid, chatId) - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatmessageApi.data]) - - useEffect(() => { - if (getAllExecutionsApi.data?.length) { - const chatId = getAllExecutionsApi.data[0]?.sessionId - setChatId(chatId) - const loadedMessages = getAllExecutionsApi.data.map((execution) => { - const executionData = - typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData - const obj = { - id: execution.id, - agentFlow: executionData - } - return obj - }) - setMessages((prevMessages) => [...prevMessages, ...loadedMessages]) - setLocalStorageChatflow(chatflowid, chatId) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllExecutionsApi.data]) - - // Get chatflow streaming capability - useEffect(() => { - if (getIsChatflowStreamingApi.data) { - setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getIsChatflowStreamingApi.data]) - - // Get chatflow uploads capability - useEffect(() => { - if (getAllowChatFlowUploads.data) { - setIsChatFlowAvailableForImageUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false) - setIsChatFlowAvailableForRAGFileUploads(getAllowChatFlowUploads.data?.isRAGFileUploadAllowed ?? false) - setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false) - setImageUploadAllowedTypes(getAllowChatFlowUploads.data?.imgUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')) - setFileUploadAllowedTypes(getAllowChatFlowUploads.data?.fileUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllowChatFlowUploads.data]) - - useEffect(() => { - if (getChatflowConfig.data) { - setIsConfigLoading(false) - if (getChatflowConfig.data?.flowData) { - let nodes = JSON.parse(getChatflowConfig.data?.flowData).nodes ?? [] - const startNode = nodes.find((node) => node.data.name === 'startAgentflow') - if (startNode) { - const startInputType = startNode.data.inputs?.startInputType - setStartInputType(startInputType) - - const formInputTypes = startNode.data.inputs?.formInputTypes - if (startInputType === 'formInput' && formInputTypes && formInputTypes.length > 0) { - for (const formInputType of formInputTypes) { - if (formInputType.type === 'options') { - formInputType.options = formInputType.addOptions.map((option) => ({ - label: option.option, - name: option.option - })) - } - } - setFormInputParams(formInputTypes) - setFormInputsData({ - id: 'formInput', - inputs: {}, - inputParams: formInputTypes - }) - setFormTitle(startNode.data.inputs?.formTitle) - setFormDescription(startNode.data.inputs?.formDescription) - } - - getAllExecutionsApi.request({ agentflowId: chatflowid }) - } - } - - if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) { - let config = JSON.parse(getChatflowConfig.data?.chatbotConfig) - if (config.starterPrompts) { - let inputFields = [] - Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => { - if (config.starterPrompts[key]) { - inputFields.push(config.starterPrompts[key]) - } - }) - setStarterPrompts(inputFields.filter((field) => field.prompt !== '')) - } - if (config.chatFeedback) { - setChatFeedbackStatus(config.chatFeedback.status) - } - - if (config.leads) { - setLeadsConfig(config.leads) - if (config.leads.status && !getLocalStorageChatflow(chatflowid).lead) { - setMessages((prevMessages) => { - const leadCaptureMessage = { - message: '', - type: 'leadCaptureMessage' - } - - return [...prevMessages, leadCaptureMessage] - }) - } - } - - if (config.followUpPrompts) { - setFollowUpPromptsStatus(config.followUpPrompts.status) - } - - if (config.fullFileUpload) { - setFullFileUpload(config.fullFileUpload.status) - if (config.fullFileUpload?.allowedUploadFileTypes) { - setFullFileUploadAllowedTypes(config.fullFileUpload?.allowedUploadFileTypes) - } - } - } - } - - // Check if TTS is configured - if (getChatflowConfig.data && getChatflowConfig.data.textToSpeech) { - try { - const ttsConfig = - typeof getChatflowConfig.data.textToSpeech === 'string' - ? JSON.parse(getChatflowConfig.data.textToSpeech) - : getChatflowConfig.data.textToSpeech - - let isEnabled = false - if (ttsConfig) { - Object.keys(ttsConfig).forEach((provider) => { - if (provider !== 'none' && ttsConfig?.[provider]?.status) { - isEnabled = true - } - }) - } - setIsTTSEnabled(isEnabled) - } catch (error) { - setIsTTSEnabled(false) - } - } else { - setIsTTSEnabled(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatflowConfig.data]) - - useEffect(() => { - if (getChatflowConfig.error) { - setIsConfigLoading(false) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatflowConfig.error]) - - useEffect(() => { - if (fullFileUpload) { - setIsChatFlowAvailableForFileUploads(true) - } else if (isChatFlowAvailableForRAGFileUploads) { - setIsChatFlowAvailableForFileUploads(true) - } else { - setIsChatFlowAvailableForFileUploads(false) - } - }, [isChatFlowAvailableForRAGFileUploads, fullFileUpload]) - - // Auto scroll chat to bottom (but not during TTS actions) - useEffect(() => { - if (!isTTSActionRef.current) { - scrollToBottom() - } - }, [messages]) - - useEffect(() => { - if (isDialog && inputRef) { - setTimeout(() => { - inputRef.current?.focus() - }, 100) - } - }, [isDialog, inputRef]) - - useEffect(() => { - if (open && chatflowid) { - // API request - getChatmessageApi.request(chatflowid) - getIsChatflowStreamingApi.request(chatflowid) - getAllowChatFlowUploads.request(chatflowid) - getChatflowConfig.request(chatflowid) - - // Add a small delay to ensure content is rendered before scrolling - setTimeout(() => { - scrollToBottom() - }, 100) - - setIsRecording(false) - setIsConfigLoading(true) - - // leads - const savedLead = getLocalStorageChatflow(chatflowid)?.lead - if (savedLead) { - setIsLeadSaved(!!savedLead) - setLeadEmail(savedLead.email) - } - } - - return () => { - setUserInput('') - setUploadedFiles([]) - setLoading(false) - setMessages([ - { - message: 'Hi there! How can I help?', - type: 'apiMessage' - } - ]) - } - - // 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]) - - useEffect(() => { - if (followUpPromptsStatus && messages.length > 0) { - const lastMessage = messages[messages.length - 1] - if (lastMessage.type === 'apiMessage' && lastMessage.followUpPrompts) { - if (Array.isArray(lastMessage.followUpPrompts)) { - setFollowUpPrompts(lastMessage.followUpPrompts) - } - if (typeof lastMessage.followUpPrompts === 'string') { - const followUpPrompts = JSON.parse(lastMessage.followUpPrompts) - setFollowUpPrompts(followUpPrompts) - } - } else if (lastMessage.type === 'userMessage') { - setFollowUpPrompts([]) - } - } - }, [followUpPromptsStatus, messages]) - - const copyMessageToClipboard = async (text) => { - try { - await navigator.clipboard.writeText(text || '') - } catch (error) { - console.error('Error copying to clipboard:', error) - } - } - - const onThumbsUpClick = async (messageId) => { - const body = { - chatflowid, - chatId, - messageId, - rating: 'THUMBS_UP', - content: '' - } - const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body) - if (result.data) { - const data = result.data - let id = '' - if (data && data.id) id = data.id - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)] - return allMessages.map((message) => { - if (message.id === messageId) { - message.feedback = { - rating: 'THUMBS_UP' - } - } - return message - }) - }) - setFeedbackId(id) - setShowFeedbackContentDialog(true) - } - } + const [isHovered, setIsHovered] = useState(false); + const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent'; + + return ( +
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ position: 'relative', display: 'inline-block' }}> + + + + {item.name} + + + {isHovered && !disabled && ( + + )} +
+ ); +}; - const onThumbsDownClick = async (messageId) => { - const body = { - chatflowid, - chatId, - messageId, - rating: 'THUMBS_DOWN', - content: '' - } - const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body) - if (result.data) { - const data = result.data - let id = '' - if (data && data.id) id = data.id - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)] - return allMessages.map((message) => { - if (message.id === messageId) { - message.feedback = { - rating: 'THUMBS_DOWN' - } - } - return message - }) - }) - setFeedbackId(id) - setShowFeedbackContentDialog(true) - } - } +CardWithDeleteOverlay.propTypes = { + item: PropTypes.object, + customization: PropTypes.object, + disabled: PropTypes.bool, + onDelete: PropTypes.func, +}; - const submitFeedbackContent = async (text) => { - const body = { - content: text +const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => { + const theme = useTheme(); + const customization = useSelector((state) => state.customization); + + const ps = useRef(); + + const dispatch = useDispatch(); + const { onAgentflowNodeStatusUpdate, clearAgentflowNodeStatus } = useContext(flowContext); + + useNotifier(); + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)); + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)); + + const [userInput, setUserInput] = useState(''); + const [loading, setLoading] = useState(false); + const [messages, setMessages] = useState([ + { + message: 'Hi there! How can I help?', + type: 'apiMessage', + }, + ]); + const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false); + const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false); + const [sourceDialogOpen, setSourceDialogOpen] = useState(false); + const [sourceDialogProps, setSourceDialogProps] = useState({}); + const [chatId, setChatId] = useState(uuidv4()); + const [isMessageStopping, setIsMessageStopping] = useState(false); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [imageUploadAllowedTypes, setImageUploadAllowedTypes] = useState(''); + const [fileUploadAllowedTypes, setFileUploadAllowedTypes] = useState(''); + const [inputHistory] = useState(new ChatInputHistory(10)); + + const inputRef = useRef(null); + const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow); + const getAllExecutionsApi = useApi(executionsApi.getAllExecutions); + const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming); + const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads); + const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow); + + const [starterPrompts, setStarterPrompts] = useState([]); + + // full file upload + const [fullFileUpload, setFullFileUpload] = useState(false); + const [fullFileUploadAllowedTypes, setFullFileUploadAllowedTypes] = useState('*'); + + // feedback + const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false); + const [feedbackId, setFeedbackId] = useState(''); + const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false); + + // leads + const [leadsConfig, setLeadsConfig] = useState(null); + const [leadName, setLeadName] = useState(''); + const [leadEmail, setLeadEmail] = useState(''); + const [leadPhone, setLeadPhone] = useState(''); + const [isLeadSaving, setIsLeadSaving] = useState(false); + const [isLeadSaved, setIsLeadSaved] = useState(false); + + // follow-up prompts + const [followUpPromptsStatus, setFollowUpPromptsStatus] = useState(false); + const [followUpPrompts, setFollowUpPrompts] = useState([]); + + // drag & drop and file input + const imgUploadRef = useRef(null); + const fileUploadRef = useRef(null); + const [isChatFlowAvailableForImageUploads, setIsChatFlowAvailableForImageUploads] = useState(false); + const [isChatFlowAvailableForFileUploads, setIsChatFlowAvailableForFileUploads] = useState(false); + const [isChatFlowAvailableForRAGFileUploads, setIsChatFlowAvailableForRAGFileUploads] = useState(false); + const [isDragActive, setIsDragActive] = useState(false); + + // recording + const [isRecording, setIsRecording] = useState(false); + const [recordingNotSupported, setRecordingNotSupported] = useState(false); + const [isLoadingRecording, setIsLoadingRecording] = useState(false); + + const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false); + const [feedback, setFeedback] = useState(''); + const [pendingActionData, setPendingActionData] = useState(null); + const [feedbackType, setFeedbackType] = useState(''); + + // start input type + const [startInputType, setStartInputType] = useState(''); + const [formTitle, setFormTitle] = useState(''); + const [formDescription, setFormDescription] = useState(''); + const [formInputsData, setFormInputsData] = useState({}); + const [formInputParams, setFormInputParams] = useState([]); + + const [isConfigLoading, setIsConfigLoading] = useState(true); + + // TTS state + const [isTTSLoading, setIsTTSLoading] = useState({}); + const [isTTSPlaying, setIsTTSPlaying] = useState({}); + const [ttsAudio, setTtsAudio] = useState({}); + const [isTTSEnabled, setIsTTSEnabled] = useState(false); + + // TTS streaming state + const [ttsStreamingState, setTtsStreamingState] = useState({ + mediaSource: null, + sourceBuffer: null, + audio: null, + chunkQueue: [], + isBuffering: false, + audioFormat: null, + abortController: null, + }); + + // Ref to prevent auto-scroll during TTS actions (using ref to avoid re-renders) + const isTTSActionRef = useRef(false); + const ttsTimeoutRef = useRef(null); + + const isFileAllowedForUpload = (file) => { + const constraints = getAllowChatFlowUploads.data; + /** + * {isImageUploadAllowed: boolean, imgUploadSizeAndTypes: Array<{ fileTypes: string[], maxUploadSize: number }>} + */ + let acceptFile = false; + + // Early return if constraints are not available yet + if (!constraints) { + console.warn('Upload constraints not loaded yet'); + return false; + } + + if (constraints.isImageUploadAllowed) { + const fileType = file.type; + const sizeInMB = file.size / 1024 / 1024; + if (constraints.imgUploadSizeAndTypes && Array.isArray(constraints.imgUploadSizeAndTypes)) { + constraints.imgUploadSizeAndTypes.forEach((allowed) => { + if (allowed.fileTypes && allowed.fileTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { + acceptFile = true; + } + }); + } + } + + if (fullFileUpload) { + return true; + } else if (constraints.isRAGFileUploadAllowed) { + const fileExt = file.name.split('.').pop(); + if (fileExt && constraints.fileUploadSizeAndTypes && Array.isArray(constraints.fileUploadSizeAndTypes)) { + constraints.fileUploadSizeAndTypes.forEach((allowed) => { + if (allowed.fileTypes && allowed.fileTypes.length === 1 && allowed.fileTypes[0] === '*') { + acceptFile = true; + } else if (allowed.fileTypes && allowed.fileTypes.includes(`.${fileExt}`)) { + 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 (!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads) { + return; + } + e.preventDefault(); + setIsDragActive(false); + let files = []; + let uploadedFiles = []; + + 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; + // Only add files + if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { + uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }); + } + 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 { + 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); + setUploadedFiles(uploadedFiles); + setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]); + } + + if (e.dataTransfer.items) { + //TODO set files + 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 ? 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 ? s.substring(s.indexOf('href') + 6) : ''; + let hrefStr = start.substring(0, start.indexOf('"')); + + let upload = { + data: hrefStr, + preview: hrefStr, + type: 'url', + name: hrefStr ? 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 = []; + let uploadedFiles = []; + for (const file of event.target.files) { + if (isFileAllowedForUpload(file) === false) { + return; + } + // Only add files + if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { + uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }); + } + 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); + setUploadedFiles(uploadedFiles); + setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]); + // 👇️ reset file input + event.target.value = null; + }; + + const addRecordingToPreviews = (blob) => { + let mimeType = ''; + const pos = blob.type.indexOf(';'); + if (pos === -1) { + mimeType = blob.type; + } else { + mimeType = blob.type ? blob.type.substring(0, pos) : ''; + } + // 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_${Date.now()}.wav`, + mime: mimeType, + }; + setPreviews((prevPreviews) => [...prevPreviews, upload]); + }; + }; + + const handleDrag = (e) => { + if (isChatFlowAvailableForImageUploads || isChatFlowAvailableForFileUploads) { + e.preventDefault(); + e.stopPropagation(); + if (e.type === 'dragenter' || e.type === 'dragover') { + setIsDragActive(true); + } else if (e.type === 'dragleave') { + setIsDragActive(false); + } + } + }; + + const handleAbort = async () => { + setIsMessageStopping(true); + try { + // Stop all TTS streams first + stopAllTTS(); + + // Abort TTS for any active streams + const activeTTSMessages = Object.keys(isTTSLoading).concat(Object.keys(isTTSPlaying)); + for (const messageId of activeTTSMessages) { + await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }); + } + + await chatmessageApi.abortMessage(chatflowid, chatId); + } catch (error) { + setIsMessageStopping(false); + enqueueSnackbar({ + message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ), + }, + }); + } + }; + + const handleDeletePreview = (itemToDelete) => { + if (itemToDelete.type === 'file') { + URL.revokeObjectURL(itemToDelete.preview); // Clean up for file + } + setPreviews(previews.filter((item) => item !== itemToDelete)); + }; + + const handleFileUploadClick = () => { + // 👇️ open file input box on click of another element + fileUploadRef.current.click(); + }; + + const handleImageUploadClick = () => { + // 👇️ open file input box on click of another element + imgUploadRef.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); + }; + + const onURLClick = (data) => { + window.open(data, '_blank'); + }; + + const scrollToBottom = () => { + if (ps.current) { + ps.current.scrollTo({ top: maxScroll }); + } + }; + + // Helper function to manage TTS action flag + const setTTSAction = (isActive) => { + isTTSActionRef.current = isActive; + if (ttsTimeoutRef.current) { + clearTimeout(ttsTimeoutRef.current); + ttsTimeoutRef.current = null; + } + if (isActive) { + // Reset the flag after a longer delay to ensure all state changes are complete + ttsTimeoutRef.current = setTimeout(() => { + isTTSActionRef.current = false; + ttsTimeoutRef.current = null; + }, 300); + } + }; + + const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput]); + + const updateLastMessage = (text) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].message += text; + allMessages[allMessages.length - 1].feedback = null; + return allMessages; + }); + }; + + const updateErrorMessage = (errorMessage) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + allMessages.push({ message: errorMessage, type: 'apiMessage' }); + return allMessages; + }); + }; + + const updateLastMessageSourceDocuments = (sourceDocuments) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].sourceDocuments = sourceDocuments; + return allMessages; + }); + }; + + const updateLastMessageAgentReasoning = (agentReasoning) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].agentReasoning = agentReasoning; + return allMessages; + }); + }; + + const updateAgentFlowEvent = (event) => { + if (event === 'INPROGRESS') { + setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage', agentFlowEventStatus: event }]); + } else { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].agentFlowEventStatus = event; + return allMessages; + }); + } + }; + + const updateAgentFlowExecutedData = (agentFlowExecutedData) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].agentFlowExecutedData = agentFlowExecutedData; + return allMessages; + }); + }; + + const updateLastMessageAction = (action) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].action = action; + return allMessages; + }); + }; + + const updateLastMessageArtifacts = (artifacts) => { + artifacts.forEach((artifact) => { + if (artifact.type === 'png' || artifact.type === 'jpeg') { + artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( + 'FILE-STORAGE::', + '', + )}`; + } + }); + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].artifacts = artifacts; + return allMessages; + }); + }; + + const updateLastMessageNextAgent = (nextAgent) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning; + if (lastAgentReasoning && lastAgentReasoning.length > 0) { + lastAgentReasoning.push({ nextAgent }); + } + allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning; + return allMessages; + }); + }; + + const updateLastMessageNextAgentFlow = (nextAgentFlow) => { + onAgentflowNodeStatusUpdate(nextAgentFlow); + }; + + const updateLastMessageUsedTools = (usedTools) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].usedTools = usedTools; + return allMessages; + }); + }; + + const updateLastMessageFileAnnotations = (fileAnnotations) => { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations; + return allMessages; + }); + }; + + const abortMessage = () => { + setIsMessageStopping(false); + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning; + if (lastAgentReasoning && lastAgentReasoning.length > 0) { + allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning.filter((reasoning) => !reasoning.nextAgent); + } + return allMessages; + }); + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + enqueueSnackbar({ + message: 'Message stopped', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ), + }, + }); + }; + + const handleError = (message = 'Oops! There seems to be an error. Please try again.') => { + message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, ''); + setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]); + setLoading(false); + setUserInput(''); + setUploadedFiles([]); + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + }; + + const handlePromptClick = async (promptStarterInput) => { + setUserInput(promptStarterInput); + handleSubmit(undefined, promptStarterInput); + }; + + const handleFollowUpPromptClick = async (promptStarterInput) => { + setUserInput(promptStarterInput); + setFollowUpPrompts([]); + handleSubmit(undefined, promptStarterInput); + }; + + const onSubmitResponse = (actionData, feedback = '', type = '') => { + let fbType = feedbackType; + if (type) { + fbType = type; + } + const question = feedback ? feedback : fbType.charAt(0).toUpperCase() + fbType.slice(1); + handleSubmit(undefined, question, undefined, { + type: fbType, + startNodeId: actionData?.nodeId, + feedback, + }); + }; + + const handleSubmitFeedback = () => { + if (pendingActionData) { + onSubmitResponse(pendingActionData, feedback); + setOpenFeedbackDialog(false); + setFeedback(''); + setPendingActionData(null); + setFeedbackType(''); + } + }; + + const handleActionClick = async (elem, action) => { + setUserInput(elem.label); + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; + allMessages[allMessages.length - 1].action = null; + return allMessages; + }); + if (elem.type.includes('agentflowv2')) { + const type = elem.type.includes('approve') ? 'proceed' : 'reject'; + setFeedbackType(type); + + if (action.data && action.data.input && action.data.input.humanInputEnableFeedback) { + setPendingActionData(action.data); + setOpenFeedbackDialog(true); + } else { + onSubmitResponse(action.data, '', type); + } + } else { + handleSubmit(undefined, elem.label, action); + } + }; + + const updateMetadata = (data, input) => { + // set message id that is needed for feedback + if (data.chatMessageId) { + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type === 'apiMessage') { + allMessages[allMessages.length - 1].id = data.chatMessageId; + } + return allMessages; + }); + } + + if (data.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 (data.followUpPrompts) { + const followUpPrompts = JSON.parse(data.followUpPrompts); + if (typeof followUpPrompts === 'string') { + setFollowUpPrompts(JSON.parse(followUpPrompts)); + } else { + setFollowUpPrompts(followUpPrompts); + } + } + }; + + const handleFileUploads = async (uploads) => { + if (!uploadedFiles.length) return uploads; + + if (fullFileUpload) { + const filesWithFullUploadType = uploadedFiles.filter((file) => file.type === 'file:full'); + if (filesWithFullUploadType.length > 0) { + const formData = new FormData(); + for (const file of filesWithFullUploadType) { + formData.append('files', file.file); + } + formData.append('chatId', chatId); + + const response = await attachmentsApi.createAttachment(chatflowid, chatId, formData); + const data = response.data; + + for (const extractedFileData of data) { + const content = extractedFileData.content; + const fileName = extractedFileData.name; + + // find matching name in previews and replace data with content + const uploadIndex = uploads.findIndex((upload) => upload.name === fileName); + + if (uploadIndex !== -1) { + uploads[uploadIndex] = { + ...uploads[uploadIndex], + data: content, + name: fileName, + type: 'file:full', + }; + } + } + } + } else if (isChatFlowAvailableForRAGFileUploads) { + const filesWithRAGUploadType = uploadedFiles.filter((file) => file.type === 'file:rag'); + + if (filesWithRAGUploadType.length > 0) { + const formData = new FormData(); + for (const file of filesWithRAGUploadType) { + formData.append('files', file.file); + } + formData.append('chatId', chatId); + + await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData); + + // delay for vector store to be updated + const delay = (delayInms) => { + return new Promise((resolve) => setTimeout(resolve, delayInms)); + }; + await delay(2500); //TODO: check if embeddings can be retrieved using file name as metadata filter + + uploads = uploads.map((upload) => { + return { + ...upload, + type: 'file:rag', + }; + }); + } + } + return uploads; + }; + + // Handle form submission + const handleSubmit = async (e, selectedInput, action, humanInput) => { + if (e) e.preventDefault(); + + if (!selectedInput && userInput.trim() === '') { + const containsFile = previews.filter((item) => !item.mime.startsWith('image') && item.type !== 'audio').length > 0; + if (!previews.length || (previews.length && containsFile)) { + return; + } + } + + let input = userInput; + + if (typeof selectedInput === 'string') { + if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput; + + if (input.trim()) { + inputHistory.addToHistory(input); + } + } else if (typeof selectedInput === 'object') { + input = Object.entries(selectedInput) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + } + + setLoading(true); + clearAgentflowNodeStatus(); + + let uploads = previews.map((item) => { + return { + data: item.data, + type: item.type, + name: item.name, + mime: item.mime, + }; + }); + + try { + uploads = await handleFileUploads(uploads); + } catch (error) { + handleError('Unable to upload documents'); + return; + } + + clearPreviews(); + setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: uploads }]); + + // Send user question to Prediction Internal API + try { + const params = { + question: input, + chatId, + }; + if (typeof selectedInput === 'object') { + params.form = selectedInput; + delete params.question; + } + if (uploads && uploads.length > 0) params.uploads = uploads; + if (leadEmail) params.leadEmail = leadEmail; + if (action) params.action = action; + if (humanInput) params.humanInput = humanInput; + + if (isChatFlowAvailableToStream) { + fetchResponseFromEventStream(chatflowid, params); + } else { + const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params); + if (response.data) { + const data = response.data; + + updateMetadata(data, input); + + let text = ''; + if (data.text) text = data.text; + else if (data.json) text = '```json\n' + JSON.stringify(data.json, null, 2); + else text = JSON.stringify(data, null, 2); + + setMessages((prevMessages) => [ + ...prevMessages, + { + message: text, + id: data?.chatMessageId, + sourceDocuments: data?.sourceDocuments, + usedTools: data?.usedTools, + calledTools: data?.calledTools, + fileAnnotations: data?.fileAnnotations, + agentReasoning: data?.agentReasoning, + agentFlowExecutedData: data?.agentFlowExecutedData, + action: data?.action, + artifacts: data?.artifacts, + type: 'apiMessage', + feedback: null, + }, + ]); + + setLocalStorageChatflow(chatflowid, data.chatId); + setLoading(false); + setUserInput(''); + setUploadedFiles([]); + + setTimeout(() => { + inputRef.current?.focus(); + scrollToBottom(); + }, 100); + } + } + } catch (error) { + handleError(error.response.data.message); + return; + } + }; + + const fetchResponseFromEventStream = async (chatflowid, params) => { + const chatId = params.chatId; + const input = params.question; + params.streaming = true; + await fetchEventSource(`${baseURL}/api/v1/internal-prediction/${chatflowid}`, { + openWhenHidden: true, + method: 'POST', + body: JSON.stringify(params), + headers: { + 'Content-Type': 'application/json', + 'x-request-from': 'internal', + }, + async onopen(response) { + if (response.ok && response.headers.get('content-type') === EventStreamContentType) { + //console.log('EventSource Open') + } + }, + async onmessage(ev) { + const payload = JSON.parse(ev.data); + switch (payload.event) { + case 'start': + setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }]); + break; + case 'token': + updateLastMessage(payload.data); + break; + case 'sourceDocuments': + updateLastMessageSourceDocuments(payload.data); + break; + case 'usedTools': + updateLastMessageUsedTools(payload.data); + break; + case 'fileAnnotations': + updateLastMessageFileAnnotations(payload.data); + break; + case 'agentReasoning': + updateLastMessageAgentReasoning(payload.data); + break; + case 'agentFlowEvent': + updateAgentFlowEvent(payload.data); + break; + case 'agentFlowExecutedData': + updateAgentFlowExecutedData(payload.data); + break; + case 'artifacts': + updateLastMessageArtifacts(payload.data); + break; + case 'action': + updateLastMessageAction(payload.data); + break; + case 'nextAgent': + updateLastMessageNextAgent(payload.data); + break; + case 'nextAgentFlow': + updateLastMessageNextAgentFlow(payload.data); + break; + case 'metadata': + updateMetadata(payload.data, input); + break; + case 'error': + updateErrorMessage(payload.data); + break; + case 'abort': + abortMessage(payload.data); + closeResponse(); + break; + case 'tts_start': + handleTTSStart(payload.data); + break; + case 'tts_data': + handleTTSDataChunk(payload.data.audioChunk); + break; + case 'tts_end': + handleTTSEnd(); + break; + case 'tts_abort': + handleTTSAbort(payload.data); + break; + case 'end': + setLocalStorageChatflow(chatflowid, chatId); + closeResponse(); + break; + } + }, + async onclose() { + closeResponse(); + }, + async onerror(err) { + console.error('EventSource Error: ', err); + closeResponse(); + throw err; + }, + }); + }; + + const closeResponse = () => { + setLoading(false); + setUserInput(''); + setUploadedFiles([]); + setTimeout(() => { + inputRef.current?.focus(); + scrollToBottom(); + }, 100); + }; + // Prevent blank submissions and allow for multiline input + const handleEnter = (e) => { + // Check if IME composition is in progress + const isIMEComposition = e.isComposing || e.keyCode === 229; + if (e.key === 'ArrowUp' && !isIMEComposition) { + e.preventDefault(); + const previousInput = inputHistory.getPreviousInput(userInput); + setUserInput(previousInput); + } else if (e.key === 'ArrowDown' && !isIMEComposition) { + e.preventDefault(); + const nextInput = inputHistory.getNextInput(); + setUserInput(nextInput); + } else if (e.key === 'Enter' && userInput && !isIMEComposition) { + if (!e.shiftKey && userInput) { + handleSubmit(e); + } + } else if (e.key === 'Enter') { + e.preventDefault(); + } + }; + + const getLabel = (URL, source) => { + if (URL && typeof URL === 'object') { + if (URL.pathname && typeof URL.pathname === 'string') { + if (URL.pathname.substring(0, 15) === '/') { + return URL.host || ''; + } else { + return `${URL.pathname.substring(0, 15)}...`; + } + } else if (URL.host) { + return URL.host; + } + } + + if (source && source.pageContent && typeof source.pageContent === 'string') { + return `${source.pageContent.substring(0, 15)}...`; + } + + return ''; + }; + + const getFileUploadAllowedTypes = () => { + if (fullFileUpload) { + return fullFileUploadAllowedTypes === '' ? '*' : fullFileUploadAllowedTypes; + } + return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*'; + }; + + const downloadFile = async (fileAnnotation) => { + try { + const response = await axios.post( + `${baseURL}/api/v1/openai-assistants-file/download`, + { fileName: fileAnnotation.fileName, chatflowId: chatflowid, chatId: chatId }, + { responseType: 'blob' }, + ); + const blob = new Blob([response.data], { type: response.headers['content-type'] }); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = fileAnnotation.fileName; + document.body.appendChild(link); + link.click(); + link.remove(); + } catch (error) { + console.error('Download failed:', error); + } + }; + + const getAgentIcon = (nodeName, instructions) => { + if (nodeName) { + return `${baseURL}/api/v1/node-icon/${nodeName}`; + } else if (instructions) { + return multiagent_supervisorPNG; + } else { + return multiagent_workerPNG; + } + }; + + // Get chatmessages successful + useEffect(() => { + if (getChatmessageApi.data?.length) { + const chatId = getChatmessageApi.data[0]?.chatId; + setChatId(chatId); + const loadedMessages = getChatmessageApi.data.map((message) => { + const obj = { + id: message.id, + message: message.content, + feedback: message.feedback, + type: message.role, + }; + if (message.sourceDocuments) obj.sourceDocuments = message.sourceDocuments; + if (message.usedTools) obj.usedTools = message.usedTools; + if (message.fileAnnotations) obj.fileAnnotations = message.fileAnnotations; + if (message.agentReasoning) obj.agentReasoning = message.agentReasoning; + if (message.action) obj.action = message.action; + if (message.artifacts) { + obj.artifacts = message.artifacts; + obj.artifacts.forEach((artifact) => { + if (artifact.type === 'png' || artifact.type === 'jpeg') { + artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( + 'FILE-STORAGE::', + '', + )}`; + } + }); } - const result = await chatmessagefeedbackApi.updateFeedback(feedbackId, body) - if (result.data) { - setFeedbackId('') - setShowFeedbackContentDialog(false) + if (message.fileUploads) { + obj.fileUploads = 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}`; + } + }); + } + if (message.followUpPrompts) obj.followUpPrompts = JSON.parse(message.followUpPrompts); + if (message.role === 'apiMessage' && message.execution && message.execution.executionData) + obj.agentFlowExecutedData = JSON.parse(message.execution.executionData); + return obj; + }); + setMessages((prevMessages) => [...prevMessages, ...loadedMessages]); + setLocalStorageChatflow(chatflowid, chatId); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatmessageApi.data]); + + useEffect(() => { + if (getAllExecutionsApi.data?.length) { + const chatId = getAllExecutionsApi.data[0]?.sessionId; + setChatId(chatId); + const loadedMessages = getAllExecutionsApi.data.map((execution) => { + const executionData = typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData; + const obj = { + id: execution.id, + agentFlow: executionData, + }; + return obj; + }); + setMessages((prevMessages) => [...prevMessages, ...loadedMessages]); + setLocalStorageChatflow(chatflowid, chatId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllExecutionsApi.data]); + + // Get chatflow streaming capability + useEffect(() => { + if (getIsChatflowStreamingApi.data) { + setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getIsChatflowStreamingApi.data]); + + // Get chatflow uploads capability + useEffect(() => { + if (getAllowChatFlowUploads.data) { + setIsChatFlowAvailableForImageUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false); + setIsChatFlowAvailableForRAGFileUploads(getAllowChatFlowUploads.data?.isRAGFileUploadAllowed ?? false); + setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false); + setImageUploadAllowedTypes(getAllowChatFlowUploads.data?.imgUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')); + setFileUploadAllowedTypes(getAllowChatFlowUploads.data?.fileUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getAllowChatFlowUploads.data]); + + useEffect(() => { + if (getChatflowConfig.data) { + setIsConfigLoading(false); + if (getChatflowConfig.data?.flowData) { + let nodes = JSON.parse(getChatflowConfig.data?.flowData).nodes ?? []; + const startNode = nodes.find((node) => node.data.name === 'startAgentflow'); + if (startNode) { + const startInputType = startNode.data.inputs?.startInputType; + setStartInputType(startInputType); + + const formInputTypes = startNode.data.inputs?.formInputTypes; + if (startInputType === 'formInput' && formInputTypes && formInputTypes.length > 0) { + for (const formInputType of formInputTypes) { + if (formInputType.type === 'options') { + formInputType.options = formInputType.addOptions.map((option) => ({ + label: option.option, + name: option.option, + })); + } + } + setFormInputParams(formInputTypes); + setFormInputsData({ + id: 'formInput', + inputs: {}, + inputParams: formInputTypes, + }); + setFormTitle(startNode.data.inputs?.formTitle); + setFormDescription(startNode.data.inputs?.formDescription); + } + + getAllExecutionsApi.request({ agentflowId: chatflowid }); + } + } + + if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) { + let config = JSON.parse(getChatflowConfig.data?.chatbotConfig); + if (config.starterPrompts) { + let inputFields = []; + Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => { + if (config.starterPrompts[key]) { + inputFields.push(config.starterPrompts[key]); + } + }); + setStarterPrompts(inputFields.filter((field) => field.prompt !== '')); } - } - - const handleLeadCaptureSubmit = async (event) => { - if (event) event.preventDefault() - setIsLeadSaving(true) - - const body = { - chatflowid, - chatId, - name: leadName, - email: leadEmail, - phone: leadPhone + if (config.chatFeedback) { + setChatFeedbackStatus(config.chatFeedback.status); } - const result = await leadsApi.addLead(body) - if (result.data) { - const data = result.data - setChatId(data.chatId) - setLocalStorageChatflow(chatflowid, data.chatId, { lead: { name: leadName, email: leadEmail, phone: leadPhone } }) - setIsLeadSaved(true) - setLeadEmail(leadEmail) + if (config.leads) { + setLeadsConfig(config.leads); + if (config.leads.status && !getLocalStorageChatflow(chatflowid).lead) { setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)] - if (allMessages[allMessages.length - 1].type !== 'leadCaptureMessage') return allMessages - allMessages[allMessages.length - 1].message = - leadsConfig.successMessage || 'Thank you for submitting your contact information.' - return allMessages - }) - } - - setIsLeadSaving(false) - } - - const cleanupTTSForMessage = (messageId) => { - if (ttsAudio[messageId]) { - ttsAudio[messageId].pause() - ttsAudio[messageId].currentTime = 0 - setTtsAudio((prev) => { - const newState = { ...prev } - delete newState[messageId] - return newState - }) - } + const leadCaptureMessage = { + message: '', + type: 'leadCaptureMessage', + }; - if (ttsStreamingState.audio) { - ttsStreamingState.audio.pause() - cleanupTTSStreaming() + return [...prevMessages, leadCaptureMessage]; + }); + } } - setIsTTSPlaying((prev) => { - const newState = { ...prev } - delete newState[messageId] - return newState - }) - - setIsTTSLoading((prev) => { - const newState = { ...prev } - delete newState[messageId] - return newState - }) - } - - const handleTTSStop = async (messageId) => { - setTTSAction(true) - await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }) - cleanupTTSForMessage(messageId) - } - - const stopAllTTS = () => { - Object.keys(ttsAudio).forEach((messageId) => { - if (ttsAudio[messageId]) { - ttsAudio[messageId].pause() - ttsAudio[messageId].currentTime = 0 - } - }) - setTtsAudio({}) - - if (ttsStreamingState.abortController) { - ttsStreamingState.abortController.abort() + if (config.followUpPrompts) { + setFollowUpPromptsStatus(config.followUpPrompts.status); } - if (ttsStreamingState.audio) { - ttsStreamingState.audio.pause() - cleanupTTSStreaming() + if (config.fullFileUpload) { + setFullFileUpload(config.fullFileUpload.status); + if (config.fullFileUpload?.allowedUploadFileTypes) { + setFullFileUploadAllowedTypes(config.fullFileUpload?.allowedUploadFileTypes); + } } - - setIsTTSPlaying({}) - setIsTTSLoading({}) + } } - const handleTTSClick = async (messageId, messageText) => { - if (isTTSLoading[messageId]) return - - if (isTTSPlaying[messageId] || ttsAudio[messageId]) { - handleTTSStop(messageId) - return - } - - setTTSAction(true) - stopAllTTS() - - handleTTSStart({ chatMessageId: messageId, format: 'mp3' }) - try { - const abortController = new AbortController() - setTtsStreamingState((prev) => ({ ...prev, abortController })) - - const response = await fetch('/api/v1/text-to-speech/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-request-from': 'internal' - }, - credentials: 'include', - signal: abortController.signal, - body: JSON.stringify({ - chatflowId: chatflowid, - chatId: chatId, - chatMessageId: messageId, - text: messageText - }) - }) - - if (!response.ok) { - throw new Error(`TTS request failed: ${response.status}`) - } - - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = '' - - let done = false - while (!done) { - if (abortController.signal.aborted) { - break - } + // Check if TTS is configured + if (getChatflowConfig.data && getChatflowConfig.data.textToSpeech) { + try { + const ttsConfig = + typeof getChatflowConfig.data.textToSpeech === 'string' + ? JSON.parse(getChatflowConfig.data.textToSpeech) + : getChatflowConfig.data.textToSpeech; - const result = await reader.read() - done = result.done - if (done) { - break - } - const value = result.value - const chunk = decoder.decode(value, { stream: true }) - buffer += chunk - - const lines = buffer.split('\n\n') - buffer = lines.pop() || '' - - for (const eventBlock of lines) { - if (eventBlock.trim()) { - const event = parseSSEEvent(eventBlock) - if (event) { - switch (event.event) { - case 'tts_start': - break - case 'tts_data': - if (!abortController.signal.aborted) { - handleTTSDataChunk(event.data.audioChunk) - } - break - case 'tts_end': - if (!abortController.signal.aborted) { - handleTTSEnd() - } - break - } - } - } - } - } - } catch (error) { - if (error.name === 'AbortError') { - console.error('TTS request was aborted') - } else { - console.error('Error with TTS:', error) - enqueueSnackbar({ - message: `TTS failed: ${error.message}`, - options: { variant: 'error' } - }) + let isEnabled = false; + if (ttsConfig) { + Object.keys(ttsConfig).forEach((provider) => { + if (provider !== 'none' && ttsConfig?.[provider]?.status) { + isEnabled = true; } - } finally { - setIsTTSLoading((prev) => { - const newState = { ...prev } - delete newState[messageId] - return newState - }) - } - } - - const parseSSEEvent = (eventBlock) => { - const lines = eventBlock.split('\n') - const event = {} - - for (const line of lines) { - if (line.startsWith('event:')) { - event.event = line.substring(6).trim() - } else if (line.startsWith('data:')) { - const dataStr = line.substring(5).trim() - try { - const parsed = JSON.parse(dataStr) - if (parsed.data) { - event.data = parsed.data - } - } catch (e) { - console.error('Error parsing SSE data:', e, 'Raw data:', dataStr) - } + }); + } + setIsTTSEnabled(isEnabled); + } catch (error) { + setIsTTSEnabled(false); + } + } else { + setIsTTSEnabled(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatflowConfig.data]); + + useEffect(() => { + if (getChatflowConfig.error) { + setIsConfigLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatflowConfig.error]); + + useEffect(() => { + if (fullFileUpload) { + setIsChatFlowAvailableForFileUploads(true); + } else if (isChatFlowAvailableForRAGFileUploads) { + setIsChatFlowAvailableForFileUploads(true); + } else { + setIsChatFlowAvailableForFileUploads(false); + } + }, [isChatFlowAvailableForRAGFileUploads, fullFileUpload]); + + // Auto scroll chat to bottom (but not during TTS actions) + useEffect(() => { + if (!isTTSActionRef.current) { + scrollToBottom(); + } + }, [messages]); + + useEffect(() => { + if (isDialog && inputRef) { + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + } + }, [isDialog, inputRef]); + + useEffect(() => { + if (open && chatflowid) { + // API request + getChatmessageApi.request(chatflowid); + getIsChatflowStreamingApi.request(chatflowid); + getAllowChatFlowUploads.request(chatflowid); + getChatflowConfig.request(chatflowid); + + // Add a small delay to ensure content is rendered before scrolling + setTimeout(() => { + scrollToBottom(); + }, 100); + + setIsRecording(false); + setIsConfigLoading(true); + + // leads + const savedLead = getLocalStorageChatflow(chatflowid)?.lead; + if (savedLead) { + setIsLeadSaved(!!savedLead); + setLeadEmail(savedLead.email); + } + } + + return () => { + setUserInput(''); + setUploadedFiles([]); + setLoading(false); + setMessages([ + { + message: 'Hi there! How can I help?', + type: 'apiMessage', + }, + ]); + }; + + // 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]); + + useEffect(() => { + if (followUpPromptsStatus && messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + if (lastMessage.type === 'apiMessage' && lastMessage.followUpPrompts) { + if (Array.isArray(lastMessage.followUpPrompts)) { + setFollowUpPrompts(lastMessage.followUpPrompts); + } + if (typeof lastMessage.followUpPrompts === 'string') { + const followUpPrompts = JSON.parse(lastMessage.followUpPrompts); + setFollowUpPrompts(followUpPrompts); + } + } else if (lastMessage.type === 'userMessage') { + setFollowUpPrompts([]); + } + } + }, [followUpPromptsStatus, messages]); + + const copyMessageToClipboard = async (text) => { + try { + await navigator.clipboard.writeText(text || ''); + } catch (error) { + console.error('Error copying to clipboard:', error); + } + }; + + const onThumbsUpClick = async (messageId) => { + const body = { + chatflowid, + chatId, + messageId, + rating: 'THUMBS_UP', + content: '', + }; + const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body); + if (result.data) { + const data = result.data; + let id = ''; + if (data && data.id) id = data.id; + setMessages((prevMessages) => { + const allMessages = [...cloneDeep(prevMessages)]; + return allMessages.map((message) => { + if (message.id === messageId) { + message.feedback = { + rating: 'THUMBS_UP', + }; + } + return message; + }); + }); + setFeedbackId(id); + setShowFeedbackContentDialog(true); + } + }; + + const onThumbsDownClick = async (messageId) => { + const body = { + chatflowid, + chatId, + messageId, + rating: 'THUMBS_DOWN', + content: '', + }; + const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body); + if (result.data) { + const data = result.data; + let id = ''; + if (data && data.id) id = data.id; + setMessages((prevMessages) => { + const allMessages = [...cloneDeep(prevMessages)]; + return allMessages.map((message) => { + if (message.id === messageId) { + message.feedback = { + rating: 'THUMBS_DOWN', + }; + } + return message; + }); + }); + setFeedbackId(id); + setShowFeedbackContentDialog(true); + } + }; + + const submitFeedbackContent = async (text) => { + const body = { + content: text, + }; + const result = await chatmessagefeedbackApi.updateFeedback(feedbackId, body); + if (result.data) { + setFeedbackId(''); + setShowFeedbackContentDialog(false); + } + }; + + const handleLeadCaptureSubmit = async (event) => { + if (event) event.preventDefault(); + setIsLeadSaving(true); + + const body = { + chatflowid, + chatId, + name: leadName, + email: leadEmail, + phone: leadPhone, + }; + + const result = await leadsApi.addLead(body); + if (result.data) { + const data = result.data; + setChatId(data.chatId); + setLocalStorageChatflow(chatflowid, data.chatId, { lead: { name: leadName, email: leadEmail, phone: leadPhone } }); + setIsLeadSaved(true); + setLeadEmail(leadEmail); + setMessages((prevMessages) => { + let allMessages = [...cloneDeep(prevMessages)]; + if (allMessages[allMessages.length - 1].type !== 'leadCaptureMessage') return allMessages; + allMessages[allMessages.length - 1].message = leadsConfig.successMessage || 'Thank you for submitting your contact information.'; + return allMessages; + }); + } + + setIsLeadSaving(false); + }; + + const cleanupTTSForMessage = (messageId) => { + if (ttsAudio[messageId]) { + ttsAudio[messageId].pause(); + ttsAudio[messageId].currentTime = 0; + setTtsAudio((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + } + + if (ttsStreamingState.audio) { + ttsStreamingState.audio.pause(); + cleanupTTSStreaming(); + } + + setIsTTSPlaying((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + + setIsTTSLoading((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + }; + + const handleTTSStop = async (messageId) => { + setTTSAction(true); + await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }); + cleanupTTSForMessage(messageId); + }; + + const stopAllTTS = () => { + Object.keys(ttsAudio).forEach((messageId) => { + if (ttsAudio[messageId]) { + ttsAudio[messageId].pause(); + ttsAudio[messageId].currentTime = 0; + } + }); + setTtsAudio({}); + + if (ttsStreamingState.abortController) { + ttsStreamingState.abortController.abort(); + } + + if (ttsStreamingState.audio) { + ttsStreamingState.audio.pause(); + cleanupTTSStreaming(); + } + + setIsTTSPlaying({}); + setIsTTSLoading({}); + }; + + const handleTTSClick = async (messageId, messageText) => { + if (isTTSLoading[messageId]) return; + + if (isTTSPlaying[messageId] || ttsAudio[messageId]) { + handleTTSStop(messageId); + return; + } + + setTTSAction(true); + stopAllTTS(); + + handleTTSStart({ chatMessageId: messageId, format: 'mp3' }); + try { + const abortController = new AbortController(); + setTtsStreamingState((prev) => ({ ...prev, abortController })); + + const response = await fetch('/api/v1/text-to-speech/generate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-request-from': 'internal', + }, + credentials: 'include', + signal: abortController.signal, + body: JSON.stringify({ + chatflowId: chatflowid, + chatId: chatId, + chatMessageId: messageId, + text: messageText, + }), + }); + + if (!response.ok) { + throw new Error(`TTS request failed: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + let done = false; + while (!done) { + if (abortController.signal.aborted) { + break; + } + + const result = await reader.read(); + done = result.done; + if (done) { + break; + } + const value = result.value; + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const eventBlock of lines) { + if (eventBlock.trim()) { + const event = parseSSEEvent(eventBlock); + if (event) { + switch (event.event) { + case 'tts_start': + break; + case 'tts_data': + if (!abortController.signal.aborted) { + handleTTSDataChunk(event.data.audioChunk); + } + break; + case 'tts_end': + if (!abortController.signal.aborted) { + handleTTSEnd(); + } + break; + } } - } - - return event.event ? event : null - } - - const initializeTTSStreaming = (data) => { + } + } + } + } catch (error) { + if (error.name === 'AbortError') { + console.error('TTS request was aborted'); + } else { + console.error('Error with TTS:', error); + enqueueSnackbar({ + message: `TTS failed: ${error.message}`, + options: { variant: 'error' }, + }); + } + } finally { + setIsTTSLoading((prev) => { + const newState = { ...prev }; + delete newState[messageId]; + return newState; + }); + } + }; + + const parseSSEEvent = (eventBlock) => { + const lines = eventBlock.split('\n'); + const event = {}; + + for (const line of lines) { + if (line.startsWith('event:')) { + event.event = line.substring(6).trim(); + } else if (line.startsWith('data:')) { + const dataStr = line.substring(5).trim(); try { - const mediaSource = new MediaSource() - const audio = new Audio() - audio.src = URL.createObjectURL(mediaSource) - - mediaSource.addEventListener('sourceopen', () => { - try { - const mimeType = data.format === 'mp3' ? 'audio/mpeg' : 'audio/mpeg' - const sourceBuffer = mediaSource.addSourceBuffer(mimeType) - - setTtsStreamingState((prevState) => ({ - ...prevState, - mediaSource, - sourceBuffer, - audio - })) - - audio.play().catch((playError) => { - console.error('Error starting audio playback:', playError) - }) - } catch (error) { - console.error('Error setting up source buffer:', error) - console.error('MediaSource readyState:', mediaSource.readyState) - console.error('Requested MIME type:', mimeType) - } - }) - - audio.addEventListener('playing', () => { - setIsTTSLoading((prevState) => { - const newState = { ...prevState } - newState[data.chatMessageId] = false - return newState - }) - setIsTTSPlaying((prevState) => ({ - ...prevState, - [data.chatMessageId]: true - })) - }) - - audio.addEventListener('ended', () => { - setIsTTSPlaying((prevState) => { - const newState = { ...prevState } - delete newState[data.chatMessageId] - return newState - }) - cleanupTTSStreaming() - }) - } catch (error) { - console.error('Error initializing TTS streaming:', error) + const parsed = JSON.parse(dataStr); + if (parsed.data) { + event.data = parsed.data; + } + } catch (e) { + console.error('Error parsing SSE data:', e, 'Raw data:', dataStr); } + } } - const cleanupTTSStreaming = () => { - setTtsStreamingState((prevState) => { - if (prevState.abortController) { - prevState.abortController.abort() - } - - if (prevState.audio) { - prevState.audio.pause() - prevState.audio.removeAttribute('src') - if (prevState.audio.src) { - URL.revokeObjectURL(prevState.audio.src) - } - } - - if (prevState.mediaSource) { - if (prevState.mediaSource.readyState === 'open') { - try { - prevState.mediaSource.endOfStream() - } catch (e) { - // Ignore errors during cleanup - } - } - prevState.mediaSource.removeEventListener('sourceopen', () => {}) - } - - return { - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: null, - abortController: null - } - }) - } - - const processChunkQueue = () => { - setTtsStreamingState((prevState) => { - if (!prevState.sourceBuffer || prevState.sourceBuffer.updating || prevState.chunkQueue.length === 0) { - return prevState - } - - const chunk = prevState.chunkQueue.shift() - - try { - prevState.sourceBuffer.appendBuffer(chunk) - return { - ...prevState, - chunkQueue: [...prevState.chunkQueue], - isBuffering: true - } - } catch (error) { - console.error('Error appending chunk to buffer:', error) - return prevState - } - }) - } + return event.event ? event : null; + }; - const handleTTSStart = (data) => { - setTTSAction(true) + const initializeTTSStreaming = (data) => { + try { + const mediaSource = new MediaSource(); + const audio = new Audio(); + audio.src = URL.createObjectURL(mediaSource); - // Stop all existing TTS audio before starting new stream - stopAllTTS() + mediaSource.addEventListener('sourceopen', () => { + try { + const mimeType = data.format === 'mp3' ? 'audio/mpeg' : 'audio/mpeg'; + const sourceBuffer = mediaSource.addSourceBuffer(mimeType); - setIsTTSLoading((prevState) => ({ + setTtsStreamingState((prevState) => ({ ...prevState, - [data.chatMessageId]: true - })) - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)] - const lastMessage = allMessages[allMessages.length - 1] - if (lastMessage.type === 'userMessage') return allMessages - if (lastMessage.id) return allMessages - allMessages[allMessages.length - 1].id = data.chatMessageId - return allMessages - }) - setTtsStreamingState({ - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: data.format, - abortController: null - }) - - setTimeout(() => initializeTTSStreaming(data), 0) - } - - const handleTTSDataChunk = (base64Data) => { + mediaSource, + sourceBuffer, + audio, + })); + + audio.play().catch((playError) => { + console.error('Error starting audio playback:', playError); + }); + } catch (error) { + console.error('Error setting up source buffer:', error); + console.error('MediaSource readyState:', mediaSource.readyState); + console.error('Requested MIME type:', mimeType); + } + }); + + audio.addEventListener('playing', () => { + setIsTTSLoading((prevState) => { + const newState = { ...prevState }; + newState[data.chatMessageId] = false; + return newState; + }); + setIsTTSPlaying((prevState) => ({ + ...prevState, + [data.chatMessageId]: true, + })); + }); + + audio.addEventListener('ended', () => { + setIsTTSPlaying((prevState) => { + const newState = { ...prevState }; + delete newState[data.chatMessageId]; + return newState; + }); + cleanupTTSStreaming(); + }); + } catch (error) { + console.error('Error initializing TTS streaming:', error); + } + }; + + const cleanupTTSStreaming = () => { + setTtsStreamingState((prevState) => { + if (prevState.abortController) { + prevState.abortController.abort(); + } + + if (prevState.audio) { + prevState.audio.pause(); + prevState.audio.removeAttribute('src'); + if (prevState.audio.src) { + URL.revokeObjectURL(prevState.audio.src); + } + } + + if (prevState.mediaSource) { + if (prevState.mediaSource.readyState === 'open') { + try { + prevState.mediaSource.endOfStream(); + } catch (e) { + // Ignore errors during cleanup + } + } + prevState.mediaSource.removeEventListener('sourceopen', () => {}); + } + + return { + mediaSource: null, + sourceBuffer: null, + audio: null, + chunkQueue: [], + isBuffering: false, + audioFormat: null, + abortController: null, + }; + }); + }; + + const processChunkQueue = () => { + setTtsStreamingState((prevState) => { + if (!prevState.sourceBuffer || prevState.sourceBuffer.updating || prevState.chunkQueue.length === 0) { + return prevState; + } + + const chunk = prevState.chunkQueue.shift(); + + try { + prevState.sourceBuffer.appendBuffer(chunk); + return { + ...prevState, + chunkQueue: [...prevState.chunkQueue], + isBuffering: true, + }; + } catch (error) { + console.error('Error appending chunk to buffer:', error); + return prevState; + } + }); + }; + + const handleTTSStart = (data) => { + setTTSAction(true); + + // Stop all existing TTS audio before starting new stream + stopAllTTS(); + + setIsTTSLoading((prevState) => ({ + ...prevState, + [data.chatMessageId]: true, + })); + setMessages((prevMessages) => { + const allMessages = [...cloneDeep(prevMessages)]; + const lastMessage = allMessages[allMessages.length - 1]; + if (lastMessage.type === 'userMessage') return allMessages; + if (lastMessage.id) return allMessages; + allMessages[allMessages.length - 1].id = data.chatMessageId; + return allMessages; + }); + setTtsStreamingState({ + mediaSource: null, + sourceBuffer: null, + audio: null, + chunkQueue: [], + isBuffering: false, + audioFormat: data.format, + abortController: null, + }); + + setTimeout(() => initializeTTSStreaming(data), 0); + }; + + const handleTTSDataChunk = (base64Data) => { + try { + const audioBuffer = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)); + + setTtsStreamingState((prevState) => { + const newState = { + ...prevState, + chunkQueue: [...prevState.chunkQueue, audioBuffer], + }; + + if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { + setTimeout(() => processChunkQueue(), 0); + } + + return newState; + }); + } catch (error) { + console.error('Error handling TTS data chunk:', error); + } + }; + + const handleTTSEnd = () => { + setTtsStreamingState((prevState) => { + if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { try { - const audioBuffer = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)) - - setTtsStreamingState((prevState) => { - const newState = { - ...prevState, - chunkQueue: [...prevState.chunkQueue, audioBuffer] - } - + if (prevState.sourceBuffer && prevState.chunkQueue.length > 0 && !prevState.sourceBuffer.updating) { + const remainingChunks = [...prevState.chunkQueue]; + remainingChunks.forEach((chunk, index) => { + setTimeout(() => { if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - setTimeout(() => processChunkQueue(), 0) - } - - return newState - }) - } catch (error) { - console.error('Error handling TTS data chunk:', error) - } - } - - const handleTTSEnd = () => { - setTtsStreamingState((prevState) => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - try { - if (prevState.sourceBuffer && prevState.chunkQueue.length > 0 && !prevState.sourceBuffer.updating) { - const remainingChunks = [...prevState.chunkQueue] - remainingChunks.forEach((chunk, index) => { - setTimeout(() => { - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - try { - prevState.sourceBuffer.appendBuffer(chunk) - if (index === remainingChunks.length - 1) { - setTimeout(() => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - prevState.mediaSource.endOfStream() - } - }, 100) - } - } catch (error) { - console.error('Error appending remaining chunk:', error) - } - } - }, index * 50) - }) - return { - ...prevState, - chunkQueue: [] + try { + prevState.sourceBuffer.appendBuffer(chunk); + if (index === remainingChunks.length - 1) { + setTimeout(() => { + if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { + prevState.mediaSource.endOfStream(); } + }, 100); } - - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - prevState.mediaSource.endOfStream() - } else if (prevState.sourceBuffer) { - prevState.sourceBuffer.addEventListener( - 'updateend', - () => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - prevState.mediaSource.endOfStream() - } - }, - { once: true } - ) - } - } catch (error) { - console.error('Error ending TTS stream:', error) + } catch (error) { + console.error('Error appending remaining chunk:', error); + } } - } - return prevState - }) - } - - const handleTTSAbort = (data) => { - const messageId = data.chatMessageId - cleanupTTSForMessage(messageId) + }, index * 50); + }); + return { + ...prevState, + chunkQueue: [], + }; + } + + if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { + prevState.mediaSource.endOfStream(); + } else if (prevState.sourceBuffer) { + prevState.sourceBuffer.addEventListener( + 'updateend', + () => { + if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { + prevState.mediaSource.endOfStream(); + } + }, + { once: true }, + ); + } + } catch (error) { + console.error('Error ending TTS stream:', error); + } + } + return prevState; + }); + }; + + const handleTTSAbort = (data) => { + const messageId = data.chatMessageId; + cleanupTTSForMessage(messageId); + }; + + useEffect(() => { + if (ttsStreamingState.sourceBuffer) { + const sourceBuffer = ttsStreamingState.sourceBuffer; + + const handleUpdateEnd = () => { + setTtsStreamingState((prevState) => ({ + ...prevState, + isBuffering: false, + })); + setTimeout(() => processChunkQueue(), 0); + }; + + sourceBuffer.addEventListener('updateend', handleUpdateEnd); + + return () => { + sourceBuffer.removeEventListener('updateend', handleUpdateEnd); + }; + } + }, [ttsStreamingState.sourceBuffer]); + + useEffect(() => { + return () => { + cleanupTTSStreaming(); + // Cleanup TTS timeout on unmount + if (ttsTimeoutRef.current) { + clearTimeout(ttsTimeoutRef.current); + ttsTimeoutRef.current = null; + } + }; + }, []); + + const getInputDisabled = () => { + return ( + loading || + !chatflowid || + (leadsConfig?.status && !isLeadSaved) || + (messages[messages.length - 1].action && Object.keys(messages[messages.length - 1].action).length > 0) + ); + }; + + const previewDisplay = (item) => { + if (item.mime.startsWith('image/')) { + return ( + handleDeletePreview(item)} + > + + + + + + + ); + } else if (item.mime.startsWith('audio/')) { + return ( + + + handleDeletePreview(item)} size="small"> + + + + ); + } else { + return ( + handleDeletePreview(item)} /> + ); + } + }; + + const renderFileUploads = (item, index) => { + if (item?.mime?.startsWith('image/')) { + return ( + + + + ); + } else if (item?.mime?.startsWith('audio/')) { + return ( + /* eslint-disable jsx-a11y/media-has-caption */ + + ); + } else { + return ( + + + + {item.name} + + + ); + } + }; + + const agentReasoningArtifacts = (artifacts) => { + const newArtifacts = cloneDeep(artifacts); + for (let i = 0; i < newArtifacts.length; i++) { + const artifact = newArtifacts[i]; + if (artifact && (artifact.type === 'png' || artifact.type === 'jpeg')) { + const data = artifact.data; + newArtifacts[i].data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${data.replace( + 'FILE-STORAGE::', + '', + )}`; + } + } + return newArtifacts; + }; + + const renderArtifacts = (item, index, isAgentReasoning) => { + if (item.type === 'png' || item.type === 'jpeg') { + return ( + + + + ); + } else if (item.type === 'html') { + return ( +
+ +
+ ); + } else { + return ( + + {item.data} + + ); } + }; - useEffect(() => { - if (ttsStreamingState.sourceBuffer) { - const sourceBuffer = ttsStreamingState.sourceBuffer - - const handleUpdateEnd = () => { - setTtsStreamingState((prevState) => ({ - ...prevState, - isBuffering: false - })) - setTimeout(() => processChunkQueue(), 0) - } - - sourceBuffer.addEventListener('updateend', handleUpdateEnd) - - return () => { - sourceBuffer.removeEventListener('updateend', handleUpdateEnd) - } - } - }, [ttsStreamingState.sourceBuffer]) - - useEffect(() => { - return () => { - cleanupTTSStreaming() - // Cleanup TTS timeout on unmount - if (ttsTimeoutRef.current) { - clearTimeout(ttsTimeoutRef.current) - ttsTimeoutRef.current = null - } - } - }, []) - - const getInputDisabled = () => { - return ( - loading || - !chatflowid || - (leadsConfig?.status && !isLeadSaved) || - (messages[messages.length - 1].action && Object.keys(messages[messages.length - 1].action).length > 0) - ) - } + if (isConfigLoading) { + return ( + + + + + + ); + } - const previewDisplay = (item) => { - if (item.mime.startsWith('image/')) { - return ( - handleDeletePreview(item)} - > - - - - - - - ) - } else if (item.mime.startsWith('audio/')) { - return ( - - - handleDeletePreview(item)} size='small'> - - - - ) - } else { - return ( - handleDeletePreview(item)} - /> - ) - } - } + if (startInputType === 'formInput' && messages.length === 1) { + return ( + + + + + {formTitle || 'Please Fill Out The Form'} + + + {formDescription || 'Complete all fields below to continue'} + + + {/* Form inputs */} + + {formInputParams && + formInputParams.map((inputParam, index) => ( + + { + setFormInputsData((prev) => ({ + ...prev, + inputs: { + ...prev.inputs, + [inputParam.name]: newValue, + }, + })); + }} + /> + + ))} + - const renderFileUploads = (item, index) => { - if (item?.mime?.startsWith('image/')) { + + + + + ); + } + + return ( +
+ {isDragActive && ( +
+ )} + {isDragActive && (getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isRAGFileUploadAllowed) && ( + + Drop here to upload + {[...getAllowChatFlowUploads.data.imgUploadSizeAndTypes, ...getAllowChatFlowUploads.data.fileUploadSizeAndTypes].map((allowed) => { return ( - + {allowed.fileTypes?.join(', ')} + {allowed.maxUploadSize && Max Allowed Size: {allowed.maxUploadSize} MB} + + ); + })} + + )} +
+
+ {messages && + messages.map((message, index) => { + return ( + // The latest message sent by the user will be animated while waiting for a response + - - - ) - } else if (item?.mime?.startsWith('audio/')) { - return ( - /* eslint-disable jsx-a11y/media-has-caption */ - - ) - } else { - return ( - + ) : ( + Me + )} +
- - + {message.fileUploads && message.fileUploads.length > 0 && ( +
- {item.name} - - - ) - } - } - - const agentReasoningArtifacts = (artifacts) => { - const newArtifacts = cloneDeep(artifacts) - for (let i = 0; i < newArtifacts.length; i++) { - const artifact = newArtifacts[i] - if (artifact && (artifact.type === 'png' || artifact.type === 'jpeg')) { - const data = artifact.data - newArtifacts[i].data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${data.replace( - 'FILE-STORAGE::', - '' - )}` - } - } - return newArtifacts - } - - const renderArtifacts = (item, index, isAgentReasoning) => { - if (item.type === 'png' || item.type === 'jpeg') { - return ( - - + {message.fileUploads.map((item, index) => { + return <>{renderFileUploads(item, index)}; + })} +
+ )} + {message.agentReasoning && message.agentReasoning.length > 0 && ( +
+ {message.agentReasoning.map((agent, index) => ( + + ))} +
+ )} + {message.agentFlowExecutedData && Array.isArray(message.agentFlowExecutedData) && message.agentFlowExecutedData.length > 0 && ( + + )} + {message.usedTools && ( +
- - ) - } else if (item.type === 'html') { - return ( -
- -
- ) - } else { - return ( - - {item.data} - - ) - } - } - - if (isConfigLoading) { - return ( - - - - - - ) - } - - if (startInputType === 'formInput' && messages.length === 1) { - return ( - - - + {message.usedTools.map((tool, index) => { + return tool ? ( + } + onClick={() => onSourceDialogClick(tool, 'Used Tools')} + /> + ) : null; + })} +
+ )} + {message.artifacts && ( +
- - {formTitle || 'Please Fill Out The Form'} - - - {formDescription || 'Complete all fields below to continue'} - - - {/* Form inputs */} - - {formInputParams && - formInputParams.map((inputParam, index) => ( - - { - setFormInputsData((prev) => ({ - ...prev, - inputs: { - ...prev.inputs, - [inputParam.name]: newValue - } - })) - }} - /> - - ))} - - -
+ )} +
+ {message.type === 'leadCaptureMessage' && !getLocalStorageChatflow(chatflowid)?.lead && leadsConfig.status ? ( + - {loading ? 'Submitting...' : 'Submit'} - - - - - ) - } - - return ( -
- {isDragActive && ( -
- )} - {isDragActive && - (getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isRAGFileUploadAllowed) && ( - - Drop here to upload - {[ - ...getAllowChatFlowUploads.data.imgUploadSizeAndTypes, - ...getAllowChatFlowUploads.data.fileUploadSizeAndTypes - ].map((allowed) => { - return ( - <> - {allowed.fileTypes?.join(', ')} - {allowed.maxUploadSize && ( - Max Allowed Size: {allowed.maxUploadSize} MB - )} - - ) + + {leadsConfig.title || 'Let us know where we can reach you:'} + +
+ {leadsConfig.name && ( + setLeadName(e.target.value)} + /> + )} + {leadsConfig.email && ( + setLeadEmail(e.target.value)} + /> + )} + {leadsConfig.phone && ( + setLeadPhone(e.target.value)} + /> + )} + + + + +
+ ) : ( + <> + + {message.message} + + + )} +
+ {message.fileAnnotations && ( +
+ {message.fileAnnotations.map((fileAnnotation, index) => { + return ( + + ); + })} +
+ )} + {message.sourceDocuments && ( +
+ {removeDuplicateURL(message).map((source, index) => { + const URL = source.metadata && source.metadata.source ? isValidURL(source.metadata.source) : undefined; + return ( + (URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source))} + /> + ); })} - - )} -
-
- {messages && - messages.map((message, index) => { - return ( - // The latest message sent by the user will be animated while waiting for a response - + )} + {message.action && ( +
+ {(message.action.elements || []).map((elem, index) => { + return ( + <> + {(elem.type === 'approve-button' && elem.label === 'Yes') || elem.type === 'agentflowv2-approve-button' ? ( + + ) : (elem.type === 'reject-button' && elem.label === 'No') || elem.type === 'agentflowv2-reject-button' ? ( + + ) : ( + - - - - ) : ( - <> - - {message.message} - - - )} -
- {message.fileAnnotations && ( -
- {message.fileAnnotations.map((fileAnnotation, index) => { - return ( - - ) - })} -
- )} - {message.sourceDocuments && ( -
- {removeDuplicateURL(message).map((source, index) => { - const URL = - source.metadata && source.metadata.source - ? isValidURL(source.metadata.source) - : undefined - return ( - - URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source) - } - /> - ) - })} -
- )} - {message.action && ( -
- {(message.action.elements || []).map((elem, index) => { - return ( - <> - {(elem.type === 'approve-button' && elem.label === 'Yes') || - elem.type === 'agentflowv2-approve-button' ? ( - - ) : (elem.type === 'reject-button' && elem.label === 'No') || - elem.type === 'agentflowv2-reject-button' ? ( - - ) : ( - - )} - - ) - })} -
- )} - {message.type === 'apiMessage' && message.id ? ( - <> - - {isTTSEnabled && ( - - isTTSPlaying[message.id] - ? handleTTSStop(message.id) - : handleTTSClick(message.id, message.message) - } - disabled={isTTSLoading[message.id]} - sx={{ - backgroundColor: ttsAudio[message.id] ? 'primary.main' : 'transparent', - color: ttsAudio[message.id] ? 'white' : 'inherit', - '&:hover': { - backgroundColor: ttsAudio[message.id] ? 'primary.dark' : 'action.hover' - } - }} - > - {isTTSLoading[message.id] ? ( - - ) : isTTSPlaying[message.id] ? ( - - ) : ( - - )} - - )} - {chatFeedbackStatus && ( - <> - copyMessageToClipboard(message.message)} - /> - {!message.feedback || - message.feedback.rating === '' || - message.feedback.rating === 'THUMBS_UP' ? ( - onThumbsUpClick(message.id)} - /> - ) : null} - {!message.feedback || - message.feedback.rating === '' || - message.feedback.rating === 'THUMBS_DOWN' ? ( - onThumbsDownClick(message.id)} - /> - ) : null} - - )} - - - ) : null} -
- - ) + {elem.label} + + )} + + ); })} +
+ )} + {message.type === 'apiMessage' && message.id ? ( + <> + + {isTTSEnabled && ( + (isTTSPlaying[message.id] ? handleTTSStop(message.id) : handleTTSClick(message.id, message.message))} + disabled={isTTSLoading[message.id]} + sx={{ + backgroundColor: ttsAudio[message.id] ? 'primary.main' : 'transparent', + color: ttsAudio[message.id] ? 'white' : 'inherit', + '&:hover': { + backgroundColor: ttsAudio[message.id] ? 'primary.dark' : 'action.hover', + }, + }} + > + {isTTSLoading[message.id] ? ( + + ) : isTTSPlaying[message.id] ? ( + + ) : ( + + )} + + )} + {chatFeedbackStatus && ( + <> + copyMessageToClipboard(message.message)} /> + {!message.feedback || message.feedback.rating === '' || message.feedback.rating === 'THUMBS_UP' ? ( + onThumbsUpClick(message.id)} + /> + ) : null} + {!message.feedback || message.feedback.rating === '' || message.feedback.rating === 'THUMBS_DOWN' ? ( + onThumbsDownClick(message.id)} + /> + ) : null} + + )} + + + ) : null} +
+ + ); + })} +
+
+ + {messages && messages.length === 1 && starterPrompts.length > 0 && ( +
+ 0 ? 70 : 0 }} + starterPrompts={starterPrompts || []} + onPromptClick={handlePromptClick} + isGrid={isDialog} + /> +
+ )} + + {messages && messages.length > 2 && followUpPromptsStatus && followUpPrompts.length > 0 && ( + <> + + + + + + Try these prompts + + + 0 ? 70 : 0 }} + followUpPrompts={followUpPrompts || []} + onPromptClick={handleFollowUpPromptClick} + isGrid={isDialog} + /> + + + )} + + + +
+ {previews && previews.length > 0 && ( + + {previews.map((item, index) => ( + {previewDisplay(item)} + ))} + + )} + {isRecording ? ( + <> + {recordingNotSupported ? ( +
+
+ To record audio, use modern browsers like Chrome or Firefox that support audio recording. +
-
- - {messages && messages.length === 1 && starterPrompts.length > 0 && ( -
- 0 ? 70 : 0 }} - starterPrompts={starterPrompts || []} - onPromptClick={handlePromptClick} - isGrid={isDialog} - /> +
+ ) : ( + +
+ + + + 00:00 + {isLoadingRecording && Sending...}
+
+ + + + + + +
+
)} - - {messages && messages.length > 2 && followUpPromptsStatus && followUpPrompts.length > 0 && ( + + ) : ( +
+ - - - - - - Try these prompts - - - 0 ? 70 : 0 }} - followUpPrompts={followUpPrompts || []} - onPromptClick={handleFollowUpPromptClick} - isGrid={isDialog} - /> - + {isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && ( + + + + + + )} + {!isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( + + + + + + )} + {isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( + + + + + + + + + )} + {!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && } - )} - - - -
- {previews && previews.length > 0 && ( - - {previews.map((item, index) => ( - {previewDisplay(item)} - ))} - - )} - {isRecording ? ( - <> - {recordingNotSupported ? ( -
-
- - To record audio, use modern browsers like Chrome or Firefox that support audio recording. - - -
-
+ } + endAdornment={ + <> + {isChatFlowAvailableForSpeech && ( + + onMicrophonePressed()} type="button" disabled={getInputDisabled()} edge="end"> + + + + )} + {!isAgentCanvas && ( + + + {loading ? ( +
+ +
) : ( - -
- - - - 00:00 - {isLoadingRecording && Sending...} -
-
- - - - - - -
-
+ // Send icon SVG in input field + )} +
+
+ )} + {isAgentCanvas && ( + <> + {!loading && ( + + + + + + )} + {loading && ( + + handleAbort()} + disabled={isMessageStopping} + > + {isMessageStopping ? ( +
+ +
+ ) : ( + + )} +
+
+ )} - ) : ( - - - {isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && ( - - - - - - )} - {!isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( - - - - - - )} - {isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( - - - - - - - - - )} - {!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && } - - } - endAdornment={ - <> - {isChatFlowAvailableForSpeech && ( - - onMicrophonePressed()} - type='button' - disabled={getInputDisabled()} - edge='end' - > - - - - )} - {!isAgentCanvas && ( - - - {loading ? ( -
- -
- ) : ( - // Send icon SVG in input field - - )} -
-
- )} - {isAgentCanvas && ( - <> - {!loading && ( - - - - - - )} - {loading && ( - - handleAbort()} - disabled={isMessageStopping} - > - {isMessageStopping ? ( -
- -
- ) : ( - - )} -
-
- )} - - )} - - } - /> - {isChatFlowAvailableForImageUploads && ( - - )} - {isChatFlowAvailableForFileUploads && ( - - )} - - )} -
- setSourceDialogOpen(false)} /> - setShowFeedbackContentDialog(false)} - onConfirm={submitFeedbackContent} + )} + + } /> - { - setOpenFeedbackDialog(false) - setPendingActionData(null) - setFeedback('') - }} - > - Provide Feedback - - setFeedback(e.target.value)} - /> - - - - - - -
- ) -} + {isChatFlowAvailableForImageUploads && ( + + )} + {isChatFlowAvailableForFileUploads && ( + + )} + + )} +
+ setSourceDialogOpen(false)} /> + setShowFeedbackContentDialog(false)} + onConfirm={submitFeedbackContent} + /> + { + setOpenFeedbackDialog(false); + setPendingActionData(null); + setFeedback(''); + }} + > + Provide Feedback + + setFeedback(e.target.value)} + /> + + + + + + +
+ ); +}; ChatMessage.propTypes = { - open: PropTypes.bool, - chatflowid: PropTypes.string, - isAgentCanvas: PropTypes.bool, - isDialog: PropTypes.bool, - previews: PropTypes.array, - setPreviews: PropTypes.func -} - -export default memo(ChatMessage) + open: PropTypes.bool, + chatflowid: PropTypes.string, + isAgentCanvas: PropTypes.bool, + isDialog: PropTypes.bool, + previews: PropTypes.array, + setPreviews: PropTypes.func, +}; + +export default memo(ChatMessage); From d6afada767e243861108b7a7e43c591c4b90be9d Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 26 Sep 2025 14:51:45 +0530 Subject: [PATCH 08/10] Remove react component --- src/components/ChatMessage.jsx | 2946 -------------------------------- 1 file changed, 2946 deletions(-) delete mode 100644 src/components/ChatMessage.jsx diff --git a/src/components/ChatMessage.jsx b/src/components/ChatMessage.jsx deleted file mode 100644 index 7baa627ed..000000000 --- a/src/components/ChatMessage.jsx +++ /dev/null @@ -1,2946 +0,0 @@ -import { useState, useRef, useEffect, useCallback, Fragment, useContext, memo } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import PropTypes from 'prop-types'; -import { cloneDeep } from 'lodash'; -import axios from 'axios'; -import { v4 as uuidv4 } from 'uuid'; -import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'; - -import { - Box, - Button, - Card, - CardMedia, - Chip, - CircularProgress, - Divider, - IconButton, - InputAdornment, - OutlinedInput, - Typography, - Stack, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - TextField, -} from '@mui/material'; -import { darken, useTheme } from '@mui/material/styles'; -import { - IconCircleDot, - IconDownload, - IconSend, - IconMicrophone, - IconPhotoPlus, - IconTrash, - IconX, - IconTool, - IconSquareFilled, - IconCheck, - IconPaperclip, - IconSparkles, - IconVolume, -} from '@tabler/icons-react'; -import robotPNG from '@/assets/images/robot.png'; -import userPNG from '@/assets/images/account.png'; -import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'; -import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'; -import audioUploadSVG from '@/assets/images/wave-sound.jpg'; - -// project import -import NodeInputHandler from '@/views/canvas/NodeInputHandler'; -import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'; -import { SafeHTML } from '@/ui-component/safe/SafeHTML'; -import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'; -import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog'; -import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard'; -import AgentReasoningCard from './AgentReasoningCard'; -import AgentExecutedDataCard from './AgentExecutedDataCard'; -import { ImageButton, ImageSrc, ImageBackdrop, ImageMarked } from '@/ui-component/button/ImageButton'; -import CopyToClipboardButton from '@/ui-component/button/CopyToClipboardButton'; -import ThumbsUpButton from '@/ui-component/button/ThumbsUpButton'; -import ThumbsDownButton from '@/ui-component/button/ThumbsDownButton'; -import { cancelAudioRecording, startAudioRecording, stopAudioRecording } from './audio-recording'; -import './audio-recording.css'; -import './ChatMessage.css'; - -// api -import chatmessageApi from '@/api/chatmessage'; -import chatflowsApi from '@/api/chatflows'; -import predictionApi from '@/api/prediction'; -import vectorstoreApi from '@/api/vectorstore'; -import attachmentsApi from '@/api/attachments'; -import chatmessagefeedbackApi from '@/api/chatmessagefeedback'; -import leadsApi from '@/api/lead'; -import executionsApi from '@/api/executions'; -import ttsApi from '@/api/tts'; - -// Hooks -import useApi from '@/hooks/useApi'; -import { flowContext } from '@/store/context/ReactFlowContext'; - -// Const -import { baseURL, maxScroll } from '@/store/constant'; -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'; - -// Utils -import { isValidURL, removeDuplicateURL, setLocalStorageChatflow, getLocalStorageChatflow } from '@/utils/genericHelper'; -import useNotifier from '@/utils/useNotifier'; -import FollowUpPromptsCard from '@/ui-component/cards/FollowUpPromptsCard'; - -// History -import { ChatInputHistory } from './ChatInputHistory'; - -const messageImageStyle = { - width: '128px', - height: '128px', - objectFit: 'cover', -}; - -const CardWithDeleteOverlay = ({ item, disabled, customization, onDelete }) => { - const [isHovered, setIsHovered] = useState(false); - const defaultBackgroundColor = customization.isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'transparent'; - - return ( -
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ position: 'relative', display: 'inline-block' }}> - - - - {item.name} - - - {isHovered && !disabled && ( - - )} -
- ); -}; - -CardWithDeleteOverlay.propTypes = { - item: PropTypes.object, - customization: PropTypes.object, - disabled: PropTypes.bool, - onDelete: PropTypes.func, -}; - -const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setPreviews }) => { - const theme = useTheme(); - const customization = useSelector((state) => state.customization); - - const ps = useRef(); - - const dispatch = useDispatch(); - const { onAgentflowNodeStatusUpdate, clearAgentflowNodeStatus } = useContext(flowContext); - - useNotifier(); - const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)); - const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)); - - const [userInput, setUserInput] = useState(''); - const [loading, setLoading] = useState(false); - const [messages, setMessages] = useState([ - { - message: 'Hi there! How can I help?', - type: 'apiMessage', - }, - ]); - const [isChatFlowAvailableToStream, setIsChatFlowAvailableToStream] = useState(false); - const [isChatFlowAvailableForSpeech, setIsChatFlowAvailableForSpeech] = useState(false); - const [sourceDialogOpen, setSourceDialogOpen] = useState(false); - const [sourceDialogProps, setSourceDialogProps] = useState({}); - const [chatId, setChatId] = useState(uuidv4()); - const [isMessageStopping, setIsMessageStopping] = useState(false); - const [uploadedFiles, setUploadedFiles] = useState([]); - const [imageUploadAllowedTypes, setImageUploadAllowedTypes] = useState(''); - const [fileUploadAllowedTypes, setFileUploadAllowedTypes] = useState(''); - const [inputHistory] = useState(new ChatInputHistory(10)); - - const inputRef = useRef(null); - const getChatmessageApi = useApi(chatmessageApi.getInternalChatmessageFromChatflow); - const getAllExecutionsApi = useApi(executionsApi.getAllExecutions); - const getIsChatflowStreamingApi = useApi(chatflowsApi.getIsChatflowStreaming); - const getAllowChatFlowUploads = useApi(chatflowsApi.getAllowChatflowUploads); - const getChatflowConfig = useApi(chatflowsApi.getSpecificChatflow); - - const [starterPrompts, setStarterPrompts] = useState([]); - - // full file upload - const [fullFileUpload, setFullFileUpload] = useState(false); - const [fullFileUploadAllowedTypes, setFullFileUploadAllowedTypes] = useState('*'); - - // feedback - const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false); - const [feedbackId, setFeedbackId] = useState(''); - const [showFeedbackContentDialog, setShowFeedbackContentDialog] = useState(false); - - // leads - const [leadsConfig, setLeadsConfig] = useState(null); - const [leadName, setLeadName] = useState(''); - const [leadEmail, setLeadEmail] = useState(''); - const [leadPhone, setLeadPhone] = useState(''); - const [isLeadSaving, setIsLeadSaving] = useState(false); - const [isLeadSaved, setIsLeadSaved] = useState(false); - - // follow-up prompts - const [followUpPromptsStatus, setFollowUpPromptsStatus] = useState(false); - const [followUpPrompts, setFollowUpPrompts] = useState([]); - - // drag & drop and file input - const imgUploadRef = useRef(null); - const fileUploadRef = useRef(null); - const [isChatFlowAvailableForImageUploads, setIsChatFlowAvailableForImageUploads] = useState(false); - const [isChatFlowAvailableForFileUploads, setIsChatFlowAvailableForFileUploads] = useState(false); - const [isChatFlowAvailableForRAGFileUploads, setIsChatFlowAvailableForRAGFileUploads] = useState(false); - const [isDragActive, setIsDragActive] = useState(false); - - // recording - const [isRecording, setIsRecording] = useState(false); - const [recordingNotSupported, setRecordingNotSupported] = useState(false); - const [isLoadingRecording, setIsLoadingRecording] = useState(false); - - const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false); - const [feedback, setFeedback] = useState(''); - const [pendingActionData, setPendingActionData] = useState(null); - const [feedbackType, setFeedbackType] = useState(''); - - // start input type - const [startInputType, setStartInputType] = useState(''); - const [formTitle, setFormTitle] = useState(''); - const [formDescription, setFormDescription] = useState(''); - const [formInputsData, setFormInputsData] = useState({}); - const [formInputParams, setFormInputParams] = useState([]); - - const [isConfigLoading, setIsConfigLoading] = useState(true); - - // TTS state - const [isTTSLoading, setIsTTSLoading] = useState({}); - const [isTTSPlaying, setIsTTSPlaying] = useState({}); - const [ttsAudio, setTtsAudio] = useState({}); - const [isTTSEnabled, setIsTTSEnabled] = useState(false); - - // TTS streaming state - const [ttsStreamingState, setTtsStreamingState] = useState({ - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: null, - abortController: null, - }); - - // Ref to prevent auto-scroll during TTS actions (using ref to avoid re-renders) - const isTTSActionRef = useRef(false); - const ttsTimeoutRef = useRef(null); - - const isFileAllowedForUpload = (file) => { - const constraints = getAllowChatFlowUploads.data; - /** - * {isImageUploadAllowed: boolean, imgUploadSizeAndTypes: Array<{ fileTypes: string[], maxUploadSize: number }>} - */ - let acceptFile = false; - - // Early return if constraints are not available yet - if (!constraints) { - console.warn('Upload constraints not loaded yet'); - return false; - } - - if (constraints.isImageUploadAllowed) { - const fileType = file.type; - const sizeInMB = file.size / 1024 / 1024; - if (constraints.imgUploadSizeAndTypes && Array.isArray(constraints.imgUploadSizeAndTypes)) { - constraints.imgUploadSizeAndTypes.forEach((allowed) => { - if (allowed.fileTypes && allowed.fileTypes.includes(fileType) && sizeInMB <= allowed.maxUploadSize) { - acceptFile = true; - } - }); - } - } - - if (fullFileUpload) { - return true; - } else if (constraints.isRAGFileUploadAllowed) { - const fileExt = file.name.split('.').pop(); - if (fileExt && constraints.fileUploadSizeAndTypes && Array.isArray(constraints.fileUploadSizeAndTypes)) { - constraints.fileUploadSizeAndTypes.forEach((allowed) => { - if (allowed.fileTypes && allowed.fileTypes.length === 1 && allowed.fileTypes[0] === '*') { - acceptFile = true; - } else if (allowed.fileTypes && allowed.fileTypes.includes(`.${fileExt}`)) { - 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 (!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads) { - return; - } - e.preventDefault(); - setIsDragActive(false); - let files = []; - let uploadedFiles = []; - - 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; - // Only add files - if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { - uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }); - } - 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 { - 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); - setUploadedFiles(uploadedFiles); - setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]); - } - - if (e.dataTransfer.items) { - //TODO set files - 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 ? 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 ? s.substring(s.indexOf('href') + 6) : ''; - let hrefStr = start.substring(0, start.indexOf('"')); - - let upload = { - data: hrefStr, - preview: hrefStr, - type: 'url', - name: hrefStr ? 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 = []; - let uploadedFiles = []; - for (const file of event.target.files) { - if (isFileAllowedForUpload(file) === false) { - return; - } - // Only add files - if (!file.type || !imageUploadAllowedTypes.includes(file.type)) { - uploadedFiles.push({ file, type: fullFileUpload ? 'file:full' : 'file:rag' }); - } - 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); - setUploadedFiles(uploadedFiles); - setPreviews((prevPreviews) => [...prevPreviews, ...newFiles]); - // 👇️ reset file input - event.target.value = null; - }; - - const addRecordingToPreviews = (blob) => { - let mimeType = ''; - const pos = blob.type.indexOf(';'); - if (pos === -1) { - mimeType = blob.type; - } else { - mimeType = blob.type ? blob.type.substring(0, pos) : ''; - } - // 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_${Date.now()}.wav`, - mime: mimeType, - }; - setPreviews((prevPreviews) => [...prevPreviews, upload]); - }; - }; - - const handleDrag = (e) => { - if (isChatFlowAvailableForImageUploads || isChatFlowAvailableForFileUploads) { - e.preventDefault(); - e.stopPropagation(); - if (e.type === 'dragenter' || e.type === 'dragover') { - setIsDragActive(true); - } else if (e.type === 'dragleave') { - setIsDragActive(false); - } - } - }; - - const handleAbort = async () => { - setIsMessageStopping(true); - try { - // Stop all TTS streams first - stopAllTTS(); - - // Abort TTS for any active streams - const activeTTSMessages = Object.keys(isTTSLoading).concat(Object.keys(isTTSPlaying)); - for (const messageId of activeTTSMessages) { - await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }); - } - - await chatmessageApi.abortMessage(chatflowid, chatId); - } catch (error) { - setIsMessageStopping(false); - enqueueSnackbar({ - message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - persist: true, - action: (key) => ( - - ), - }, - }); - } - }; - - const handleDeletePreview = (itemToDelete) => { - if (itemToDelete.type === 'file') { - URL.revokeObjectURL(itemToDelete.preview); // Clean up for file - } - setPreviews(previews.filter((item) => item !== itemToDelete)); - }; - - const handleFileUploadClick = () => { - // 👇️ open file input box on click of another element - fileUploadRef.current.click(); - }; - - const handleImageUploadClick = () => { - // 👇️ open file input box on click of another element - imgUploadRef.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); - }; - - const onURLClick = (data) => { - window.open(data, '_blank'); - }; - - const scrollToBottom = () => { - if (ps.current) { - ps.current.scrollTo({ top: maxScroll }); - } - }; - - // Helper function to manage TTS action flag - const setTTSAction = (isActive) => { - isTTSActionRef.current = isActive; - if (ttsTimeoutRef.current) { - clearTimeout(ttsTimeoutRef.current); - ttsTimeoutRef.current = null; - } - if (isActive) { - // Reset the flag after a longer delay to ensure all state changes are complete - ttsTimeoutRef.current = setTimeout(() => { - isTTSActionRef.current = false; - ttsTimeoutRef.current = null; - }, 300); - } - }; - - const onChange = useCallback((e) => setUserInput(e.target.value), [setUserInput]); - - const updateLastMessage = (text) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].message += text; - allMessages[allMessages.length - 1].feedback = null; - return allMessages; - }); - }; - - const updateErrorMessage = (errorMessage) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - allMessages.push({ message: errorMessage, type: 'apiMessage' }); - return allMessages; - }); - }; - - const updateLastMessageSourceDocuments = (sourceDocuments) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].sourceDocuments = sourceDocuments; - return allMessages; - }); - }; - - const updateLastMessageAgentReasoning = (agentReasoning) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].agentReasoning = agentReasoning; - return allMessages; - }); - }; - - const updateAgentFlowEvent = (event) => { - if (event === 'INPROGRESS') { - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage', agentFlowEventStatus: event }]); - } else { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].agentFlowEventStatus = event; - return allMessages; - }); - } - }; - - const updateAgentFlowExecutedData = (agentFlowExecutedData) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].agentFlowExecutedData = agentFlowExecutedData; - return allMessages; - }); - }; - - const updateLastMessageAction = (action) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].action = action; - return allMessages; - }); - }; - - const updateLastMessageArtifacts = (artifacts) => { - artifacts.forEach((artifact) => { - if (artifact.type === 'png' || artifact.type === 'jpeg') { - artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( - 'FILE-STORAGE::', - '', - )}`; - } - }); - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].artifacts = artifacts; - return allMessages; - }); - }; - - const updateLastMessageNextAgent = (nextAgent) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning; - if (lastAgentReasoning && lastAgentReasoning.length > 0) { - lastAgentReasoning.push({ nextAgent }); - } - allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning; - return allMessages; - }); - }; - - const updateLastMessageNextAgentFlow = (nextAgentFlow) => { - onAgentflowNodeStatusUpdate(nextAgentFlow); - }; - - const updateLastMessageUsedTools = (usedTools) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].usedTools = usedTools; - return allMessages; - }); - }; - - const updateLastMessageFileAnnotations = (fileAnnotations) => { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].fileAnnotations = fileAnnotations; - return allMessages; - }); - }; - - const abortMessage = () => { - setIsMessageStopping(false); - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - const lastAgentReasoning = allMessages[allMessages.length - 1].agentReasoning; - if (lastAgentReasoning && lastAgentReasoning.length > 0) { - allMessages[allMessages.length - 1].agentReasoning = lastAgentReasoning.filter((reasoning) => !reasoning.nextAgent); - } - return allMessages; - }); - setTimeout(() => { - inputRef.current?.focus(); - }, 100); - enqueueSnackbar({ - message: 'Message stopped', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ), - }, - }); - }; - - const handleError = (message = 'Oops! There seems to be an error. Please try again.') => { - message = message.replace(`Unable to parse JSON response from chat agent.\n\n`, ''); - setMessages((prevMessages) => [...prevMessages, { message, type: 'apiMessage' }]); - setLoading(false); - setUserInput(''); - setUploadedFiles([]); - setTimeout(() => { - inputRef.current?.focus(); - }, 100); - }; - - const handlePromptClick = async (promptStarterInput) => { - setUserInput(promptStarterInput); - handleSubmit(undefined, promptStarterInput); - }; - - const handleFollowUpPromptClick = async (promptStarterInput) => { - setUserInput(promptStarterInput); - setFollowUpPrompts([]); - handleSubmit(undefined, promptStarterInput); - }; - - const onSubmitResponse = (actionData, feedback = '', type = '') => { - let fbType = feedbackType; - if (type) { - fbType = type; - } - const question = feedback ? feedback : fbType.charAt(0).toUpperCase() + fbType.slice(1); - handleSubmit(undefined, question, undefined, { - type: fbType, - startNodeId: actionData?.nodeId, - feedback, - }); - }; - - const handleSubmitFeedback = () => { - if (pendingActionData) { - onSubmitResponse(pendingActionData, feedback); - setOpenFeedbackDialog(false); - setFeedback(''); - setPendingActionData(null); - setFeedbackType(''); - } - }; - - const handleActionClick = async (elem, action) => { - setUserInput(elem.label); - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'userMessage') return allMessages; - allMessages[allMessages.length - 1].action = null; - return allMessages; - }); - if (elem.type.includes('agentflowv2')) { - const type = elem.type.includes('approve') ? 'proceed' : 'reject'; - setFeedbackType(type); - - if (action.data && action.data.input && action.data.input.humanInputEnableFeedback) { - setPendingActionData(action.data); - setOpenFeedbackDialog(true); - } else { - onSubmitResponse(action.data, '', type); - } - } else { - handleSubmit(undefined, elem.label, action); - } - }; - - const updateMetadata = (data, input) => { - // set message id that is needed for feedback - if (data.chatMessageId) { - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type === 'apiMessage') { - allMessages[allMessages.length - 1].id = data.chatMessageId; - } - return allMessages; - }); - } - - if (data.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 (data.followUpPrompts) { - const followUpPrompts = JSON.parse(data.followUpPrompts); - if (typeof followUpPrompts === 'string') { - setFollowUpPrompts(JSON.parse(followUpPrompts)); - } else { - setFollowUpPrompts(followUpPrompts); - } - } - }; - - const handleFileUploads = async (uploads) => { - if (!uploadedFiles.length) return uploads; - - if (fullFileUpload) { - const filesWithFullUploadType = uploadedFiles.filter((file) => file.type === 'file:full'); - if (filesWithFullUploadType.length > 0) { - const formData = new FormData(); - for (const file of filesWithFullUploadType) { - formData.append('files', file.file); - } - formData.append('chatId', chatId); - - const response = await attachmentsApi.createAttachment(chatflowid, chatId, formData); - const data = response.data; - - for (const extractedFileData of data) { - const content = extractedFileData.content; - const fileName = extractedFileData.name; - - // find matching name in previews and replace data with content - const uploadIndex = uploads.findIndex((upload) => upload.name === fileName); - - if (uploadIndex !== -1) { - uploads[uploadIndex] = { - ...uploads[uploadIndex], - data: content, - name: fileName, - type: 'file:full', - }; - } - } - } - } else if (isChatFlowAvailableForRAGFileUploads) { - const filesWithRAGUploadType = uploadedFiles.filter((file) => file.type === 'file:rag'); - - if (filesWithRAGUploadType.length > 0) { - const formData = new FormData(); - for (const file of filesWithRAGUploadType) { - formData.append('files', file.file); - } - formData.append('chatId', chatId); - - await vectorstoreApi.upsertVectorStoreWithFormData(chatflowid, formData); - - // delay for vector store to be updated - const delay = (delayInms) => { - return new Promise((resolve) => setTimeout(resolve, delayInms)); - }; - await delay(2500); //TODO: check if embeddings can be retrieved using file name as metadata filter - - uploads = uploads.map((upload) => { - return { - ...upload, - type: 'file:rag', - }; - }); - } - } - return uploads; - }; - - // Handle form submission - const handleSubmit = async (e, selectedInput, action, humanInput) => { - if (e) e.preventDefault(); - - if (!selectedInput && userInput.trim() === '') { - const containsFile = previews.filter((item) => !item.mime.startsWith('image') && item.type !== 'audio').length > 0; - if (!previews.length || (previews.length && containsFile)) { - return; - } - } - - let input = userInput; - - if (typeof selectedInput === 'string') { - if (selectedInput !== undefined && selectedInput.trim() !== '') input = selectedInput; - - if (input.trim()) { - inputHistory.addToHistory(input); - } - } else if (typeof selectedInput === 'object') { - input = Object.entries(selectedInput) - .map(([key, value]) => `${key}: ${value}`) - .join('\n'); - } - - setLoading(true); - clearAgentflowNodeStatus(); - - let uploads = previews.map((item) => { - return { - data: item.data, - type: item.type, - name: item.name, - mime: item.mime, - }; - }); - - try { - uploads = await handleFileUploads(uploads); - } catch (error) { - handleError('Unable to upload documents'); - return; - } - - clearPreviews(); - setMessages((prevMessages) => [...prevMessages, { message: input, type: 'userMessage', fileUploads: uploads }]); - - // Send user question to Prediction Internal API - try { - const params = { - question: input, - chatId, - }; - if (typeof selectedInput === 'object') { - params.form = selectedInput; - delete params.question; - } - if (uploads && uploads.length > 0) params.uploads = uploads; - if (leadEmail) params.leadEmail = leadEmail; - if (action) params.action = action; - if (humanInput) params.humanInput = humanInput; - - if (isChatFlowAvailableToStream) { - fetchResponseFromEventStream(chatflowid, params); - } else { - const response = await predictionApi.sendMessageAndGetPrediction(chatflowid, params); - if (response.data) { - const data = response.data; - - updateMetadata(data, input); - - let text = ''; - if (data.text) text = data.text; - else if (data.json) text = '```json\n' + JSON.stringify(data.json, null, 2); - else text = JSON.stringify(data, null, 2); - - setMessages((prevMessages) => [ - ...prevMessages, - { - message: text, - id: data?.chatMessageId, - sourceDocuments: data?.sourceDocuments, - usedTools: data?.usedTools, - calledTools: data?.calledTools, - fileAnnotations: data?.fileAnnotations, - agentReasoning: data?.agentReasoning, - agentFlowExecutedData: data?.agentFlowExecutedData, - action: data?.action, - artifacts: data?.artifacts, - type: 'apiMessage', - feedback: null, - }, - ]); - - setLocalStorageChatflow(chatflowid, data.chatId); - setLoading(false); - setUserInput(''); - setUploadedFiles([]); - - setTimeout(() => { - inputRef.current?.focus(); - scrollToBottom(); - }, 100); - } - } - } catch (error) { - handleError(error.response.data.message); - return; - } - }; - - const fetchResponseFromEventStream = async (chatflowid, params) => { - const chatId = params.chatId; - const input = params.question; - params.streaming = true; - await fetchEventSource(`${baseURL}/api/v1/internal-prediction/${chatflowid}`, { - openWhenHidden: true, - method: 'POST', - body: JSON.stringify(params), - headers: { - 'Content-Type': 'application/json', - 'x-request-from': 'internal', - }, - async onopen(response) { - if (response.ok && response.headers.get('content-type') === EventStreamContentType) { - //console.log('EventSource Open') - } - }, - async onmessage(ev) { - const payload = JSON.parse(ev.data); - switch (payload.event) { - case 'start': - setMessages((prevMessages) => [...prevMessages, { message: '', type: 'apiMessage' }]); - break; - case 'token': - updateLastMessage(payload.data); - break; - case 'sourceDocuments': - updateLastMessageSourceDocuments(payload.data); - break; - case 'usedTools': - updateLastMessageUsedTools(payload.data); - break; - case 'fileAnnotations': - updateLastMessageFileAnnotations(payload.data); - break; - case 'agentReasoning': - updateLastMessageAgentReasoning(payload.data); - break; - case 'agentFlowEvent': - updateAgentFlowEvent(payload.data); - break; - case 'agentFlowExecutedData': - updateAgentFlowExecutedData(payload.data); - break; - case 'artifacts': - updateLastMessageArtifacts(payload.data); - break; - case 'action': - updateLastMessageAction(payload.data); - break; - case 'nextAgent': - updateLastMessageNextAgent(payload.data); - break; - case 'nextAgentFlow': - updateLastMessageNextAgentFlow(payload.data); - break; - case 'metadata': - updateMetadata(payload.data, input); - break; - case 'error': - updateErrorMessage(payload.data); - break; - case 'abort': - abortMessage(payload.data); - closeResponse(); - break; - case 'tts_start': - handleTTSStart(payload.data); - break; - case 'tts_data': - handleTTSDataChunk(payload.data.audioChunk); - break; - case 'tts_end': - handleTTSEnd(); - break; - case 'tts_abort': - handleTTSAbort(payload.data); - break; - case 'end': - setLocalStorageChatflow(chatflowid, chatId); - closeResponse(); - break; - } - }, - async onclose() { - closeResponse(); - }, - async onerror(err) { - console.error('EventSource Error: ', err); - closeResponse(); - throw err; - }, - }); - }; - - const closeResponse = () => { - setLoading(false); - setUserInput(''); - setUploadedFiles([]); - setTimeout(() => { - inputRef.current?.focus(); - scrollToBottom(); - }, 100); - }; - // Prevent blank submissions and allow for multiline input - const handleEnter = (e) => { - // Check if IME composition is in progress - const isIMEComposition = e.isComposing || e.keyCode === 229; - if (e.key === 'ArrowUp' && !isIMEComposition) { - e.preventDefault(); - const previousInput = inputHistory.getPreviousInput(userInput); - setUserInput(previousInput); - } else if (e.key === 'ArrowDown' && !isIMEComposition) { - e.preventDefault(); - const nextInput = inputHistory.getNextInput(); - setUserInput(nextInput); - } else if (e.key === 'Enter' && userInput && !isIMEComposition) { - if (!e.shiftKey && userInput) { - handleSubmit(e); - } - } else if (e.key === 'Enter') { - e.preventDefault(); - } - }; - - const getLabel = (URL, source) => { - if (URL && typeof URL === 'object') { - if (URL.pathname && typeof URL.pathname === 'string') { - if (URL.pathname.substring(0, 15) === '/') { - return URL.host || ''; - } else { - return `${URL.pathname.substring(0, 15)}...`; - } - } else if (URL.host) { - return URL.host; - } - } - - if (source && source.pageContent && typeof source.pageContent === 'string') { - return `${source.pageContent.substring(0, 15)}...`; - } - - return ''; - }; - - const getFileUploadAllowedTypes = () => { - if (fullFileUpload) { - return fullFileUploadAllowedTypes === '' ? '*' : fullFileUploadAllowedTypes; - } - return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*'; - }; - - const downloadFile = async (fileAnnotation) => { - try { - const response = await axios.post( - `${baseURL}/api/v1/openai-assistants-file/download`, - { fileName: fileAnnotation.fileName, chatflowId: chatflowid, chatId: chatId }, - { responseType: 'blob' }, - ); - const blob = new Blob([response.data], { type: response.headers['content-type'] }); - const downloadUrl = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = downloadUrl; - link.download = fileAnnotation.fileName; - document.body.appendChild(link); - link.click(); - link.remove(); - } catch (error) { - console.error('Download failed:', error); - } - }; - - const getAgentIcon = (nodeName, instructions) => { - if (nodeName) { - return `${baseURL}/api/v1/node-icon/${nodeName}`; - } else if (instructions) { - return multiagent_supervisorPNG; - } else { - return multiagent_workerPNG; - } - }; - - // Get chatmessages successful - useEffect(() => { - if (getChatmessageApi.data?.length) { - const chatId = getChatmessageApi.data[0]?.chatId; - setChatId(chatId); - const loadedMessages = getChatmessageApi.data.map((message) => { - const obj = { - id: message.id, - message: message.content, - feedback: message.feedback, - type: message.role, - }; - if (message.sourceDocuments) obj.sourceDocuments = message.sourceDocuments; - if (message.usedTools) obj.usedTools = message.usedTools; - if (message.fileAnnotations) obj.fileAnnotations = message.fileAnnotations; - if (message.agentReasoning) obj.agentReasoning = message.agentReasoning; - if (message.action) obj.action = message.action; - if (message.artifacts) { - obj.artifacts = message.artifacts; - obj.artifacts.forEach((artifact) => { - if (artifact.type === 'png' || artifact.type === 'jpeg') { - artifact.data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${artifact.data.replace( - 'FILE-STORAGE::', - '', - )}`; - } - }); - } - if (message.fileUploads) { - obj.fileUploads = 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}`; - } - }); - } - if (message.followUpPrompts) obj.followUpPrompts = JSON.parse(message.followUpPrompts); - if (message.role === 'apiMessage' && message.execution && message.execution.executionData) - obj.agentFlowExecutedData = JSON.parse(message.execution.executionData); - return obj; - }); - setMessages((prevMessages) => [...prevMessages, ...loadedMessages]); - setLocalStorageChatflow(chatflowid, chatId); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatmessageApi.data]); - - useEffect(() => { - if (getAllExecutionsApi.data?.length) { - const chatId = getAllExecutionsApi.data[0]?.sessionId; - setChatId(chatId); - const loadedMessages = getAllExecutionsApi.data.map((execution) => { - const executionData = typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData; - const obj = { - id: execution.id, - agentFlow: executionData, - }; - return obj; - }); - setMessages((prevMessages) => [...prevMessages, ...loadedMessages]); - setLocalStorageChatflow(chatflowid, chatId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllExecutionsApi.data]); - - // Get chatflow streaming capability - useEffect(() => { - if (getIsChatflowStreamingApi.data) { - setIsChatFlowAvailableToStream(getIsChatflowStreamingApi.data?.isStreaming ?? false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getIsChatflowStreamingApi.data]); - - // Get chatflow uploads capability - useEffect(() => { - if (getAllowChatFlowUploads.data) { - setIsChatFlowAvailableForImageUploads(getAllowChatFlowUploads.data?.isImageUploadAllowed ?? false); - setIsChatFlowAvailableForRAGFileUploads(getAllowChatFlowUploads.data?.isRAGFileUploadAllowed ?? false); - setIsChatFlowAvailableForSpeech(getAllowChatFlowUploads.data?.isSpeechToTextEnabled ?? false); - setImageUploadAllowedTypes(getAllowChatFlowUploads.data?.imgUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')); - setFileUploadAllowedTypes(getAllowChatFlowUploads.data?.fileUploadSizeAndTypes.map((allowed) => allowed.fileTypes).join(',')); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllowChatFlowUploads.data]); - - useEffect(() => { - if (getChatflowConfig.data) { - setIsConfigLoading(false); - if (getChatflowConfig.data?.flowData) { - let nodes = JSON.parse(getChatflowConfig.data?.flowData).nodes ?? []; - const startNode = nodes.find((node) => node.data.name === 'startAgentflow'); - if (startNode) { - const startInputType = startNode.data.inputs?.startInputType; - setStartInputType(startInputType); - - const formInputTypes = startNode.data.inputs?.formInputTypes; - if (startInputType === 'formInput' && formInputTypes && formInputTypes.length > 0) { - for (const formInputType of formInputTypes) { - if (formInputType.type === 'options') { - formInputType.options = formInputType.addOptions.map((option) => ({ - label: option.option, - name: option.option, - })); - } - } - setFormInputParams(formInputTypes); - setFormInputsData({ - id: 'formInput', - inputs: {}, - inputParams: formInputTypes, - }); - setFormTitle(startNode.data.inputs?.formTitle); - setFormDescription(startNode.data.inputs?.formDescription); - } - - getAllExecutionsApi.request({ agentflowId: chatflowid }); - } - } - - if (getChatflowConfig.data?.chatbotConfig && JSON.parse(getChatflowConfig.data?.chatbotConfig)) { - let config = JSON.parse(getChatflowConfig.data?.chatbotConfig); - if (config.starterPrompts) { - let inputFields = []; - Object.getOwnPropertyNames(config.starterPrompts).forEach((key) => { - if (config.starterPrompts[key]) { - inputFields.push(config.starterPrompts[key]); - } - }); - setStarterPrompts(inputFields.filter((field) => field.prompt !== '')); - } - if (config.chatFeedback) { - setChatFeedbackStatus(config.chatFeedback.status); - } - - if (config.leads) { - setLeadsConfig(config.leads); - if (config.leads.status && !getLocalStorageChatflow(chatflowid).lead) { - setMessages((prevMessages) => { - const leadCaptureMessage = { - message: '', - type: 'leadCaptureMessage', - }; - - return [...prevMessages, leadCaptureMessage]; - }); - } - } - - if (config.followUpPrompts) { - setFollowUpPromptsStatus(config.followUpPrompts.status); - } - - if (config.fullFileUpload) { - setFullFileUpload(config.fullFileUpload.status); - if (config.fullFileUpload?.allowedUploadFileTypes) { - setFullFileUploadAllowedTypes(config.fullFileUpload?.allowedUploadFileTypes); - } - } - } - } - - // Check if TTS is configured - if (getChatflowConfig.data && getChatflowConfig.data.textToSpeech) { - try { - const ttsConfig = - typeof getChatflowConfig.data.textToSpeech === 'string' - ? JSON.parse(getChatflowConfig.data.textToSpeech) - : getChatflowConfig.data.textToSpeech; - - let isEnabled = false; - if (ttsConfig) { - Object.keys(ttsConfig).forEach((provider) => { - if (provider !== 'none' && ttsConfig?.[provider]?.status) { - isEnabled = true; - } - }); - } - setIsTTSEnabled(isEnabled); - } catch (error) { - setIsTTSEnabled(false); - } - } else { - setIsTTSEnabled(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatflowConfig.data]); - - useEffect(() => { - if (getChatflowConfig.error) { - setIsConfigLoading(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatflowConfig.error]); - - useEffect(() => { - if (fullFileUpload) { - setIsChatFlowAvailableForFileUploads(true); - } else if (isChatFlowAvailableForRAGFileUploads) { - setIsChatFlowAvailableForFileUploads(true); - } else { - setIsChatFlowAvailableForFileUploads(false); - } - }, [isChatFlowAvailableForRAGFileUploads, fullFileUpload]); - - // Auto scroll chat to bottom (but not during TTS actions) - useEffect(() => { - if (!isTTSActionRef.current) { - scrollToBottom(); - } - }, [messages]); - - useEffect(() => { - if (isDialog && inputRef) { - setTimeout(() => { - inputRef.current?.focus(); - }, 100); - } - }, [isDialog, inputRef]); - - useEffect(() => { - if (open && chatflowid) { - // API request - getChatmessageApi.request(chatflowid); - getIsChatflowStreamingApi.request(chatflowid); - getAllowChatFlowUploads.request(chatflowid); - getChatflowConfig.request(chatflowid); - - // Add a small delay to ensure content is rendered before scrolling - setTimeout(() => { - scrollToBottom(); - }, 100); - - setIsRecording(false); - setIsConfigLoading(true); - - // leads - const savedLead = getLocalStorageChatflow(chatflowid)?.lead; - if (savedLead) { - setIsLeadSaved(!!savedLead); - setLeadEmail(savedLead.email); - } - } - - return () => { - setUserInput(''); - setUploadedFiles([]); - setLoading(false); - setMessages([ - { - message: 'Hi there! How can I help?', - type: 'apiMessage', - }, - ]); - }; - - // 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]); - - useEffect(() => { - if (followUpPromptsStatus && messages.length > 0) { - const lastMessage = messages[messages.length - 1]; - if (lastMessage.type === 'apiMessage' && lastMessage.followUpPrompts) { - if (Array.isArray(lastMessage.followUpPrompts)) { - setFollowUpPrompts(lastMessage.followUpPrompts); - } - if (typeof lastMessage.followUpPrompts === 'string') { - const followUpPrompts = JSON.parse(lastMessage.followUpPrompts); - setFollowUpPrompts(followUpPrompts); - } - } else if (lastMessage.type === 'userMessage') { - setFollowUpPrompts([]); - } - } - }, [followUpPromptsStatus, messages]); - - const copyMessageToClipboard = async (text) => { - try { - await navigator.clipboard.writeText(text || ''); - } catch (error) { - console.error('Error copying to clipboard:', error); - } - }; - - const onThumbsUpClick = async (messageId) => { - const body = { - chatflowid, - chatId, - messageId, - rating: 'THUMBS_UP', - content: '', - }; - const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body); - if (result.data) { - const data = result.data; - let id = ''; - if (data && data.id) id = data.id; - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)]; - return allMessages.map((message) => { - if (message.id === messageId) { - message.feedback = { - rating: 'THUMBS_UP', - }; - } - return message; - }); - }); - setFeedbackId(id); - setShowFeedbackContentDialog(true); - } - }; - - const onThumbsDownClick = async (messageId) => { - const body = { - chatflowid, - chatId, - messageId, - rating: 'THUMBS_DOWN', - content: '', - }; - const result = await chatmessagefeedbackApi.addFeedback(chatflowid, body); - if (result.data) { - const data = result.data; - let id = ''; - if (data && data.id) id = data.id; - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)]; - return allMessages.map((message) => { - if (message.id === messageId) { - message.feedback = { - rating: 'THUMBS_DOWN', - }; - } - return message; - }); - }); - setFeedbackId(id); - setShowFeedbackContentDialog(true); - } - }; - - const submitFeedbackContent = async (text) => { - const body = { - content: text, - }; - const result = await chatmessagefeedbackApi.updateFeedback(feedbackId, body); - if (result.data) { - setFeedbackId(''); - setShowFeedbackContentDialog(false); - } - }; - - const handleLeadCaptureSubmit = async (event) => { - if (event) event.preventDefault(); - setIsLeadSaving(true); - - const body = { - chatflowid, - chatId, - name: leadName, - email: leadEmail, - phone: leadPhone, - }; - - const result = await leadsApi.addLead(body); - if (result.data) { - const data = result.data; - setChatId(data.chatId); - setLocalStorageChatflow(chatflowid, data.chatId, { lead: { name: leadName, email: leadEmail, phone: leadPhone } }); - setIsLeadSaved(true); - setLeadEmail(leadEmail); - setMessages((prevMessages) => { - let allMessages = [...cloneDeep(prevMessages)]; - if (allMessages[allMessages.length - 1].type !== 'leadCaptureMessage') return allMessages; - allMessages[allMessages.length - 1].message = leadsConfig.successMessage || 'Thank you for submitting your contact information.'; - return allMessages; - }); - } - - setIsLeadSaving(false); - }; - - const cleanupTTSForMessage = (messageId) => { - if (ttsAudio[messageId]) { - ttsAudio[messageId].pause(); - ttsAudio[messageId].currentTime = 0; - setTtsAudio((prev) => { - const newState = { ...prev }; - delete newState[messageId]; - return newState; - }); - } - - if (ttsStreamingState.audio) { - ttsStreamingState.audio.pause(); - cleanupTTSStreaming(); - } - - setIsTTSPlaying((prev) => { - const newState = { ...prev }; - delete newState[messageId]; - return newState; - }); - - setIsTTSLoading((prev) => { - const newState = { ...prev }; - delete newState[messageId]; - return newState; - }); - }; - - const handleTTSStop = async (messageId) => { - setTTSAction(true); - await ttsApi.abortTTS({ chatflowId: chatflowid, chatId, chatMessageId: messageId }); - cleanupTTSForMessage(messageId); - }; - - const stopAllTTS = () => { - Object.keys(ttsAudio).forEach((messageId) => { - if (ttsAudio[messageId]) { - ttsAudio[messageId].pause(); - ttsAudio[messageId].currentTime = 0; - } - }); - setTtsAudio({}); - - if (ttsStreamingState.abortController) { - ttsStreamingState.abortController.abort(); - } - - if (ttsStreamingState.audio) { - ttsStreamingState.audio.pause(); - cleanupTTSStreaming(); - } - - setIsTTSPlaying({}); - setIsTTSLoading({}); - }; - - const handleTTSClick = async (messageId, messageText) => { - if (isTTSLoading[messageId]) return; - - if (isTTSPlaying[messageId] || ttsAudio[messageId]) { - handleTTSStop(messageId); - return; - } - - setTTSAction(true); - stopAllTTS(); - - handleTTSStart({ chatMessageId: messageId, format: 'mp3' }); - try { - const abortController = new AbortController(); - setTtsStreamingState((prev) => ({ ...prev, abortController })); - - const response = await fetch('/api/v1/text-to-speech/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-request-from': 'internal', - }, - credentials: 'include', - signal: abortController.signal, - body: JSON.stringify({ - chatflowId: chatflowid, - chatId: chatId, - chatMessageId: messageId, - text: messageText, - }), - }); - - if (!response.ok) { - throw new Error(`TTS request failed: ${response.status}`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - let done = false; - while (!done) { - if (abortController.signal.aborted) { - break; - } - - const result = await reader.read(); - done = result.done; - if (done) { - break; - } - const value = result.value; - const chunk = decoder.decode(value, { stream: true }); - buffer += chunk; - - const lines = buffer.split('\n\n'); - buffer = lines.pop() || ''; - - for (const eventBlock of lines) { - if (eventBlock.trim()) { - const event = parseSSEEvent(eventBlock); - if (event) { - switch (event.event) { - case 'tts_start': - break; - case 'tts_data': - if (!abortController.signal.aborted) { - handleTTSDataChunk(event.data.audioChunk); - } - break; - case 'tts_end': - if (!abortController.signal.aborted) { - handleTTSEnd(); - } - break; - } - } - } - } - } - } catch (error) { - if (error.name === 'AbortError') { - console.error('TTS request was aborted'); - } else { - console.error('Error with TTS:', error); - enqueueSnackbar({ - message: `TTS failed: ${error.message}`, - options: { variant: 'error' }, - }); - } - } finally { - setIsTTSLoading((prev) => { - const newState = { ...prev }; - delete newState[messageId]; - return newState; - }); - } - }; - - const parseSSEEvent = (eventBlock) => { - const lines = eventBlock.split('\n'); - const event = {}; - - for (const line of lines) { - if (line.startsWith('event:')) { - event.event = line.substring(6).trim(); - } else if (line.startsWith('data:')) { - const dataStr = line.substring(5).trim(); - try { - const parsed = JSON.parse(dataStr); - if (parsed.data) { - event.data = parsed.data; - } - } catch (e) { - console.error('Error parsing SSE data:', e, 'Raw data:', dataStr); - } - } - } - - return event.event ? event : null; - }; - - const initializeTTSStreaming = (data) => { - try { - const mediaSource = new MediaSource(); - const audio = new Audio(); - audio.src = URL.createObjectURL(mediaSource); - - mediaSource.addEventListener('sourceopen', () => { - try { - const mimeType = data.format === 'mp3' ? 'audio/mpeg' : 'audio/mpeg'; - const sourceBuffer = mediaSource.addSourceBuffer(mimeType); - - setTtsStreamingState((prevState) => ({ - ...prevState, - mediaSource, - sourceBuffer, - audio, - })); - - audio.play().catch((playError) => { - console.error('Error starting audio playback:', playError); - }); - } catch (error) { - console.error('Error setting up source buffer:', error); - console.error('MediaSource readyState:', mediaSource.readyState); - console.error('Requested MIME type:', mimeType); - } - }); - - audio.addEventListener('playing', () => { - setIsTTSLoading((prevState) => { - const newState = { ...prevState }; - newState[data.chatMessageId] = false; - return newState; - }); - setIsTTSPlaying((prevState) => ({ - ...prevState, - [data.chatMessageId]: true, - })); - }); - - audio.addEventListener('ended', () => { - setIsTTSPlaying((prevState) => { - const newState = { ...prevState }; - delete newState[data.chatMessageId]; - return newState; - }); - cleanupTTSStreaming(); - }); - } catch (error) { - console.error('Error initializing TTS streaming:', error); - } - }; - - const cleanupTTSStreaming = () => { - setTtsStreamingState((prevState) => { - if (prevState.abortController) { - prevState.abortController.abort(); - } - - if (prevState.audio) { - prevState.audio.pause(); - prevState.audio.removeAttribute('src'); - if (prevState.audio.src) { - URL.revokeObjectURL(prevState.audio.src); - } - } - - if (prevState.mediaSource) { - if (prevState.mediaSource.readyState === 'open') { - try { - prevState.mediaSource.endOfStream(); - } catch (e) { - // Ignore errors during cleanup - } - } - prevState.mediaSource.removeEventListener('sourceopen', () => {}); - } - - return { - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: null, - abortController: null, - }; - }); - }; - - const processChunkQueue = () => { - setTtsStreamingState((prevState) => { - if (!prevState.sourceBuffer || prevState.sourceBuffer.updating || prevState.chunkQueue.length === 0) { - return prevState; - } - - const chunk = prevState.chunkQueue.shift(); - - try { - prevState.sourceBuffer.appendBuffer(chunk); - return { - ...prevState, - chunkQueue: [...prevState.chunkQueue], - isBuffering: true, - }; - } catch (error) { - console.error('Error appending chunk to buffer:', error); - return prevState; - } - }); - }; - - const handleTTSStart = (data) => { - setTTSAction(true); - - // Stop all existing TTS audio before starting new stream - stopAllTTS(); - - setIsTTSLoading((prevState) => ({ - ...prevState, - [data.chatMessageId]: true, - })); - setMessages((prevMessages) => { - const allMessages = [...cloneDeep(prevMessages)]; - const lastMessage = allMessages[allMessages.length - 1]; - if (lastMessage.type === 'userMessage') return allMessages; - if (lastMessage.id) return allMessages; - allMessages[allMessages.length - 1].id = data.chatMessageId; - return allMessages; - }); - setTtsStreamingState({ - mediaSource: null, - sourceBuffer: null, - audio: null, - chunkQueue: [], - isBuffering: false, - audioFormat: data.format, - abortController: null, - }); - - setTimeout(() => initializeTTSStreaming(data), 0); - }; - - const handleTTSDataChunk = (base64Data) => { - try { - const audioBuffer = Uint8Array.from(atob(base64Data), (c) => c.charCodeAt(0)); - - setTtsStreamingState((prevState) => { - const newState = { - ...prevState, - chunkQueue: [...prevState.chunkQueue, audioBuffer], - }; - - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - setTimeout(() => processChunkQueue(), 0); - } - - return newState; - }); - } catch (error) { - console.error('Error handling TTS data chunk:', error); - } - }; - - const handleTTSEnd = () => { - setTtsStreamingState((prevState) => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - try { - if (prevState.sourceBuffer && prevState.chunkQueue.length > 0 && !prevState.sourceBuffer.updating) { - const remainingChunks = [...prevState.chunkQueue]; - remainingChunks.forEach((chunk, index) => { - setTimeout(() => { - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - try { - prevState.sourceBuffer.appendBuffer(chunk); - if (index === remainingChunks.length - 1) { - setTimeout(() => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - prevState.mediaSource.endOfStream(); - } - }, 100); - } - } catch (error) { - console.error('Error appending remaining chunk:', error); - } - } - }, index * 50); - }); - return { - ...prevState, - chunkQueue: [], - }; - } - - if (prevState.sourceBuffer && !prevState.sourceBuffer.updating) { - prevState.mediaSource.endOfStream(); - } else if (prevState.sourceBuffer) { - prevState.sourceBuffer.addEventListener( - 'updateend', - () => { - if (prevState.mediaSource && prevState.mediaSource.readyState === 'open') { - prevState.mediaSource.endOfStream(); - } - }, - { once: true }, - ); - } - } catch (error) { - console.error('Error ending TTS stream:', error); - } - } - return prevState; - }); - }; - - const handleTTSAbort = (data) => { - const messageId = data.chatMessageId; - cleanupTTSForMessage(messageId); - }; - - useEffect(() => { - if (ttsStreamingState.sourceBuffer) { - const sourceBuffer = ttsStreamingState.sourceBuffer; - - const handleUpdateEnd = () => { - setTtsStreamingState((prevState) => ({ - ...prevState, - isBuffering: false, - })); - setTimeout(() => processChunkQueue(), 0); - }; - - sourceBuffer.addEventListener('updateend', handleUpdateEnd); - - return () => { - sourceBuffer.removeEventListener('updateend', handleUpdateEnd); - }; - } - }, [ttsStreamingState.sourceBuffer]); - - useEffect(() => { - return () => { - cleanupTTSStreaming(); - // Cleanup TTS timeout on unmount - if (ttsTimeoutRef.current) { - clearTimeout(ttsTimeoutRef.current); - ttsTimeoutRef.current = null; - } - }; - }, []); - - const getInputDisabled = () => { - return ( - loading || - !chatflowid || - (leadsConfig?.status && !isLeadSaved) || - (messages[messages.length - 1].action && Object.keys(messages[messages.length - 1].action).length > 0) - ); - }; - - const previewDisplay = (item) => { - if (item.mime.startsWith('image/')) { - return ( - handleDeletePreview(item)} - > - - - - - - - ); - } else if (item.mime.startsWith('audio/')) { - return ( - - - handleDeletePreview(item)} size="small"> - - - - ); - } else { - return ( - handleDeletePreview(item)} /> - ); - } - }; - - const renderFileUploads = (item, index) => { - if (item?.mime?.startsWith('image/')) { - return ( - - - - ); - } else if (item?.mime?.startsWith('audio/')) { - return ( - /* eslint-disable jsx-a11y/media-has-caption */ - - ); - } else { - return ( - - - - {item.name} - - - ); - } - }; - - const agentReasoningArtifacts = (artifacts) => { - const newArtifacts = cloneDeep(artifacts); - for (let i = 0; i < newArtifacts.length; i++) { - const artifact = newArtifacts[i]; - if (artifact && (artifact.type === 'png' || artifact.type === 'jpeg')) { - const data = artifact.data; - newArtifacts[i].data = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowid}&chatId=${chatId}&fileName=${data.replace( - 'FILE-STORAGE::', - '', - )}`; - } - } - return newArtifacts; - }; - - const renderArtifacts = (item, index, isAgentReasoning) => { - if (item.type === 'png' || item.type === 'jpeg') { - return ( - - - - ); - } else if (item.type === 'html') { - return ( -
- -
- ); - } else { - return ( - - {item.data} - - ); - } - }; - - if (isConfigLoading) { - return ( - - - - - - ); - } - - if (startInputType === 'formInput' && messages.length === 1) { - return ( - - - - - {formTitle || 'Please Fill Out The Form'} - - - {formDescription || 'Complete all fields below to continue'} - - - {/* Form inputs */} - - {formInputParams && - formInputParams.map((inputParam, index) => ( - - { - setFormInputsData((prev) => ({ - ...prev, - inputs: { - ...prev.inputs, - [inputParam.name]: newValue, - }, - })); - }} - /> - - ))} - - - - - - - ); - } - - return ( -
- {isDragActive && ( -
- )} - {isDragActive && (getAllowChatFlowUploads.data?.isImageUploadAllowed || getAllowChatFlowUploads.data?.isRAGFileUploadAllowed) && ( - - Drop here to upload - {[...getAllowChatFlowUploads.data.imgUploadSizeAndTypes, ...getAllowChatFlowUploads.data.fileUploadSizeAndTypes].map((allowed) => { - return ( - <> - {allowed.fileTypes?.join(', ')} - {allowed.maxUploadSize && Max Allowed Size: {allowed.maxUploadSize} MB} - - ); - })} - - )} -
-
- {messages && - messages.map((message, index) => { - return ( - // The latest message sent by the user will be animated while waiting for a response - - {/* Display the correct icon depending on the message type */} - {message.type === 'apiMessage' || message.type === 'leadCaptureMessage' ? ( - AI - ) : ( - Me - )} -
- {message.fileUploads && message.fileUploads.length > 0 && ( -
- {message.fileUploads.map((item, index) => { - return <>{renderFileUploads(item, index)}; - })} -
- )} - {message.agentReasoning && message.agentReasoning.length > 0 && ( -
- {message.agentReasoning.map((agent, index) => ( - - ))} -
- )} - {message.agentFlowExecutedData && Array.isArray(message.agentFlowExecutedData) && message.agentFlowExecutedData.length > 0 && ( - - )} - {message.usedTools && ( -
- {message.usedTools.map((tool, index) => { - return tool ? ( - } - onClick={() => onSourceDialogClick(tool, 'Used Tools')} - /> - ) : null; - })} -
- )} - {message.artifacts && ( -
- {message.artifacts.map((item, index) => { - return item !== null ? <>{renderArtifacts(item, index)} : null; - })} -
- )} -
- {message.type === 'leadCaptureMessage' && !getLocalStorageChatflow(chatflowid)?.lead && leadsConfig.status ? ( - - - {leadsConfig.title || 'Let us know where we can reach you:'} - -
- {leadsConfig.name && ( - setLeadName(e.target.value)} - /> - )} - {leadsConfig.email && ( - setLeadEmail(e.target.value)} - /> - )} - {leadsConfig.phone && ( - setLeadPhone(e.target.value)} - /> - )} - - - - -
- ) : ( - <> - - {message.message} - - - )} -
- {message.fileAnnotations && ( -
- {message.fileAnnotations.map((fileAnnotation, index) => { - return ( - - ); - })} -
- )} - {message.sourceDocuments && ( -
- {removeDuplicateURL(message).map((source, index) => { - const URL = source.metadata && source.metadata.source ? isValidURL(source.metadata.source) : undefined; - return ( - (URL ? onURLClick(source.metadata.source) : onSourceDialogClick(source))} - /> - ); - })} -
- )} - {message.action && ( -
- {(message.action.elements || []).map((elem, index) => { - return ( - <> - {(elem.type === 'approve-button' && elem.label === 'Yes') || elem.type === 'agentflowv2-approve-button' ? ( - - ) : (elem.type === 'reject-button' && elem.label === 'No') || elem.type === 'agentflowv2-reject-button' ? ( - - ) : ( - - )} - - ); - })} -
- )} - {message.type === 'apiMessage' && message.id ? ( - <> - - {isTTSEnabled && ( - (isTTSPlaying[message.id] ? handleTTSStop(message.id) : handleTTSClick(message.id, message.message))} - disabled={isTTSLoading[message.id]} - sx={{ - backgroundColor: ttsAudio[message.id] ? 'primary.main' : 'transparent', - color: ttsAudio[message.id] ? 'white' : 'inherit', - '&:hover': { - backgroundColor: ttsAudio[message.id] ? 'primary.dark' : 'action.hover', - }, - }} - > - {isTTSLoading[message.id] ? ( - - ) : isTTSPlaying[message.id] ? ( - - ) : ( - - )} - - )} - {chatFeedbackStatus && ( - <> - copyMessageToClipboard(message.message)} /> - {!message.feedback || message.feedback.rating === '' || message.feedback.rating === 'THUMBS_UP' ? ( - onThumbsUpClick(message.id)} - /> - ) : null} - {!message.feedback || message.feedback.rating === '' || message.feedback.rating === 'THUMBS_DOWN' ? ( - onThumbsDownClick(message.id)} - /> - ) : null} - - )} - - - ) : null} -
-
- ); - })} -
-
- - {messages && messages.length === 1 && starterPrompts.length > 0 && ( -
- 0 ? 70 : 0 }} - starterPrompts={starterPrompts || []} - onPromptClick={handlePromptClick} - isGrid={isDialog} - /> -
- )} - - {messages && messages.length > 2 && followUpPromptsStatus && followUpPrompts.length > 0 && ( - <> - - - - - - Try these prompts - - - 0 ? 70 : 0 }} - followUpPrompts={followUpPrompts || []} - onPromptClick={handleFollowUpPromptClick} - isGrid={isDialog} - /> - - - )} - - - -
- {previews && previews.length > 0 && ( - - {previews.map((item, index) => ( - {previewDisplay(item)} - ))} - - )} - {isRecording ? ( - <> - {recordingNotSupported ? ( -
-
- To record audio, use modern browsers like Chrome or Firefox that support audio recording. - -
-
- ) : ( - -
- - - - 00:00 - {isLoadingRecording && Sending...} -
-
- - - - - - -
-
- )} - - ) : ( -
- - {isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && ( - - - - - - )} - {!isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( - - - - - - )} - {isChatFlowAvailableForImageUploads && isChatFlowAvailableForFileUploads && ( - - - - - - - - - )} - {!isChatFlowAvailableForImageUploads && !isChatFlowAvailableForFileUploads && } - - } - endAdornment={ - <> - {isChatFlowAvailableForSpeech && ( - - onMicrophonePressed()} type="button" disabled={getInputDisabled()} edge="end"> - - - - )} - {!isAgentCanvas && ( - - - {loading ? ( -
- -
- ) : ( - // Send icon SVG in input field - - )} -
-
- )} - {isAgentCanvas && ( - <> - {!loading && ( - - - - - - )} - {loading && ( - - handleAbort()} - disabled={isMessageStopping} - > - {isMessageStopping ? ( -
- -
- ) : ( - - )} -
-
- )} - - )} - - } - /> - {isChatFlowAvailableForImageUploads && ( - - )} - {isChatFlowAvailableForFileUploads && ( - - )} - - )} -
- setSourceDialogOpen(false)} /> - setShowFeedbackContentDialog(false)} - onConfirm={submitFeedbackContent} - /> - { - setOpenFeedbackDialog(false); - setPendingActionData(null); - setFeedback(''); - }} - > - Provide Feedback - - setFeedback(e.target.value)} - /> - - - - - - -
- ); -}; - -ChatMessage.propTypes = { - open: PropTypes.bool, - chatflowid: PropTypes.string, - isAgentCanvas: PropTypes.bool, - isDialog: PropTypes.bool, - previews: PropTypes.array, - setPreviews: PropTypes.func, -}; - -export default memo(ChatMessage); From ffeea7f327723098fa678dc539ce2d9376e52e83 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 26 Sep 2025 14:52:36 +0530 Subject: [PATCH 09/10] New build --- dist/web.js | 2 +- dist/web.umd.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/web.js b/dist/web.js index 7fdbc81b4..b1c9548b8 100644 --- a/dist/web.js +++ b/dist/web.js @@ -1 +1 @@ -function e(e){return Object.keys(e).reduce(((r,o)=>{var a=e[o];return r[o]=Object.assign({},a),!t(a.value)||function(e){return"[object Function]"===Object.prototype.toString.call(e)}(a.value)||Array.isArray(a.value)||(r[o].value=Object.assign({},a.value)),Array.isArray(a.value)&&(r[o].value=a.value.slice(0)),r}),{})}function r(e){if(e)try{return JSON.parse(e)}catch(r){return e}}function o(e,r,o){if(null==o||!1===o)return e.removeAttribute(r);let t=JSON.stringify(o);e.__updating[r]=!0,"true"===t&&(t=""),e.setAttribute(r,t),Promise.resolve().then((()=>delete e.__updating[r]))}function t(e){return null!=e&&("object"==typeof e||"function"==typeof e)}let a;function l(t,l){const i=Object.keys(l);return class extends t{static get observedAttributes(){return i.map((e=>l[e].attribute))}constructor(){super(),this.__initialized=!1,this.__released=!1,this.__releaseCallbacks=[],this.__propertyChangedCallbacks=[],this.__updating={},this.props={}}connectedCallback(){if(!this.__initialized){this.__releaseCallbacks=[],this.__propertyChangedCallbacks=[],this.__updating={},this.props=function(t,a){const l=e(a);return Object.keys(a).forEach((e=>{const a=l[e],i=t.getAttribute(a.attribute),n=t[e];i&&(a.value=a.parse?r(i):i),null!=n&&(a.value=Array.isArray(n)?n.slice(0):n),a.reflect&&o(t,a.attribute,a.value),Object.defineProperty(t,e,{get:()=>a.value,set(r){var t=a.value;a.value=r,a.reflect&&o(this,a.attribute,a.value);for(let o=0,a=this.__propertyChangedCallbacks.length;o(r[o]=e[o].value,r)),{})}(this.props),i=this.Component,n=a;try{(a=this).__initialized=!0,function(e){return"function"==typeof e&&0===e.toString().indexOf("class")}(i)?new i(t,{element:this}):i(t,{element:this})}finally{a=n}}}async disconnectedCallback(){if(await Promise.resolve(),!this.isConnected){this.__propertyChangedCallbacks.length=0;for(var e=null;e=this.__releaseCallbacks.pop();)e(this);delete this.__initialized,this.__released=!0}}attributeChangedCallback(e,o,t){!this.__initialized||this.__updating[e]||(e=this.lookupProp(e))in l&&(null==t&&!this[e]||(this[e]=l[e].parse?r(t):t))}lookupProp(e){if(l)return i.find((r=>e===r||e===l[r].attribute))}get renderRoot(){return this.shadowRoot||this.attachShadow({mode:"open"})}addReleaseCallback(e){this.__releaseCallbacks.push(e)}addPropertyChangedCallback(e){this.__propertyChangedCallbacks.push(e)}}}function i(e,r={},o={}){const{BaseElement:a=HTMLElement,extension:i}=o;return o=>{if(!e)throw new Error("tag is required to register a Component");let n=customElements.get(e);return n?n.prototype.Component=o:((n=l(a,function(e){return e?Object.keys(e).reduce(((r,o)=>{var a=e[o];return r[o]=t(a)&&"value"in a?a:{value:a},r[o].attribute||(r[o].attribute=function(e){return e.replace(/\.?([A-Z]+)/g,((e,r)=>"-"+r.toLowerCase())).replace("_","-").replace(/^-/,"")}(o)),r[o].parse="parse"in r[o]?r[o].parse:"string"!=typeof r[o].value,r}),{}):{}}(r))).prototype.Component=o,n.prototype.registeredTag=e,customElements.define(e,n,i)),n}}const n=Symbol("solid-proxy"),d=Symbol("solid-track"),s=Symbol("solid-dev-component"),m={equals:(e,r)=>e===r};let g=X;const c=1,x=2,u={owned:null,cleanups:null,context:null,owner:null};var p=null;let h=null,S=null,v=null,$=null,b=0;function A(e,r){const o=S,t=p,a=0===e.length,l=a?u:{owned:null,cleanups:null,context:null,owner:void 0===r?t:r},i=a?e:()=>e((()=>_((()=>V(l)))));p=l,S=null;try{return k(i,!0)}finally{S=o,p=t}}function f(e,r){const o={value:e,observers:null,observerSlots:null,comparator:(r=r?Object.assign({},m,r):m).equals||void 0};return[B.bind(o),e=>("function"==typeof e&&(e=e(o.value)),E(o,e))]}function M(e,r,o){N(O(e,r,!1,c))}function P(e,r,o){g=D,(e=O(e,r,!1,c)).user=!0,$?$.push(e):N(e)}function T(e,r,o){return o=o?Object.assign({},m,o):m,(e=O(e,r,!0,0)).observers=null,e.observerSlots=null,e.comparator=o.equals||void 0,N(e),B.bind(e)}function _(e){if(null===S)return e();var r=S;S=null;try{return e()}finally{S=r}}function y(e){P((()=>_(e)))}function w(e){return null!==p&&(null===p.cleanups?p.cleanups=[e]:p.cleanups.push(e)),e}function G(){return S}function L(e){var r;return void 0!==(r=U(p,e.id))?r:e.defaultValue}function C(e){const r=T(e),o=T((()=>K(r())));return o.toArray=()=>{var e=o();return Array.isArray(e)?e:null!=e?[e]:[]},o}function B(){var e;return this.sources&&this.state&&(this.state===c?N(this):(e=v,v=null,k((()=>R(this)),!1),v=e)),S&&(e=this.observers?this.observers.length:0,S.sources?(S.sources.push(this),S.sourceSlots.push(e)):(S.sources=[this],S.sourceSlots=[e]),this.observers?(this.observers.push(S),this.observerSlots.push(S.sources.length-1)):(this.observers=[S],this.observerSlots=[S.sources.length-1])),this.value}function E(e,r,o){var t=e.value;return e.comparator&&e.comparator(t,r)||(e.value=r,e.observers&&e.observers.length&&k((()=>{for(let t=0;tR(e,o[0])),!1),v=r)}}}function k(e,r){if(v)return e();let o=!1;r||(v=[]),$?o=!0:$=[],b++;try{var t=e();return function(e){if(v&&(X(v),v=null),!e){const e=$;$=null,e.length&&k((()=>g(e)),!1)}}(o),t}catch(e){o||($=null),v=null,H(e)}}function X(e){for(let r=0;ro=_((()=>(p.context={[e]:r.value},C((()=>r.children)))))),void 0),o}}const Z=Symbol("fallback");function Q(e){for(let r=0;re(r||{})))}function Y(){return!0}const J={get:(e,r,o)=>r===n?o:e.get(r),has:(e,r)=>r===n||e.has(r),set:Y,deleteProperty:Y,getOwnPropertyDescriptor:(e,r)=>({configurable:!0,enumerable:!0,get:()=>e.get(r),set:Y,deleteProperty:Y}),ownKeys:e=>e.keys()};function j(e){return(e="function"==typeof e?e():e)||{}}function q(...e){let r=!1;for(let t=0;tnew Proxy({get:o=>r.includes(o)?e[o]:void 0,has:o=>r.includes(o)&&o in e,keys:()=>r.filter((r=>r in e))},J)))).push(new Proxy({get:r=>o.has(r)?void 0:e[r],has:r=>!o.has(r)&&r in e,keys:()=>Object.keys(e).filter((e=>!o.has(e)))},J)),t;const a=Object.getOwnPropertyDescriptors(e);return r.push(Object.keys(a).filter((e=>!o.has(e)))),r.map((r=>{var o={};for(let t=0;te[l],set:()=>!0,enumerable:!0})}return o}))}function re(e){var r="fallback"in e&&{fallback:()=>e.fallback};return T(function(e,r,o={}){let t=[],a=[],l=[],i=0,n=1Q(l))),()=>{let s,m,g=e()||[];return g[d],_((()=>{let e,r,d,x,u,p,h,S,v,$=g.length;if(0===$)0!==i&&(Q(l),l=[],t=[],a=[],i=0,n=n&&[]),o.fallback&&(t=[Z],a[0]=A((e=>(l[0]=e,o.fallback()))),i=1);else if(0===i){for(a=new Array($),m=0;m<$;m++)t[m]=g[m],a[m]=A(c);i=$}else{for(d=new Array($),x=new Array($),n&&(u=new Array($)),p=0,h=Math.min(i,$);p=p&&S>=p&&t[h]===g[S];h--,S--)d[S]=a[h],x[S]=l[h],n&&(u[S]=n[h]);for(e=new Map,r=new Array(S+1),m=S;m>=p;m--)v=g[m],s=e.get(v),r[m]=void 0===s?-1:s,e.set(v,m);for(s=p;s<=h;s++)v=t[s],void 0!==(m=e.get(v))&&-1!==m?(d[m]=a[s],x[m]=l[s],n&&(u[m]=n[s]),m=r[m],e.set(v,m)):l[s]();for(m=p;m<$;m++)m in d?(a[m]=d[m],l[m]=x[m],n&&(n[m]=u[m],n[m](m))):a[m]=A(c);a=a.slice(0,i=$),t=g.slice(0)}return a}));function c(e){var o;return l[m]=e,n?([e,o]=f(m),n[m]=o,r(g[m],e)):r(g[m])}}}((()=>e.each),e.children,r||void 0))}function oe(e){const r=e.keyed,o=T((()=>e.when),void 0,{equals:(e,o)=>r?e===o:!e==!o});return T((()=>{const t=o();if(t){const a=e.children;return"function"==typeof a&&0a(r?t:()=>{if(_(o))return e.when;throw(e=>`Stale read from <${e}>.`)("Show")}))):a}return e.fallback}),void 0,void 0)}const te=new Set(["className","value","readOnly","formNoValidate","isMap","noModule","playsInline","allowfullscreen","async","autofocus","autoplay","checked","controls","default","disabled","formnovalidate","hidden","indeterminate","ismap","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","seamless","selected"]),ae=new Set(["innerHTML","textContent","innerText","children"]),le=Object.assign(Object.create(null),{className:"class",htmlFor:"for"}),ie=Object.assign(Object.create(null),{class:"className",formnovalidate:{$:"formNoValidate",BUTTON:1,INPUT:1},ismap:{$:"isMap",IMG:1},nomodule:{$:"noModule",SCRIPT:1},playsinline:{$:"playsInline",VIDEO:1},readonly:{$:"readOnly",INPUT:1,TEXTAREA:1}});const ne=new Set(["beforeinput","click","dblclick","contextmenu","focusin","focusout","input","keydown","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","pointerdown","pointermove","pointerout","pointerover","pointerup","touchend","touchmove","touchstart"]),de=new Set(["altGlyph","altGlyphDef","altGlyphItem","animate","animateColor","animateMotion","animateTransform","circle","clipPath","color-profile","cursor","defs","desc","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","font","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignObject","g","glyph","glyphRef","hkern","image","line","linearGradient","marker","mask","metadata","missing-glyph","mpath","path","pattern","polygon","polyline","radialGradient","rect","set","stop","svg","switch","symbol","text","textPath","tref","tspan","use","view","vkern"]),se={xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace"};const me="_$DX_DELEGATE";function ge(e,r,o){let t;const a=()=>{var r=document.createElement("template");return r.innerHTML=e,(o?r.content.firstChild:r.content).firstChild};return(r=r?()=>(t=t||a()).cloneNode(!0):()=>_((()=>document.importNode(t=t||a(),!0)))).cloneNode=r}function ce(e,r=window.document){var o=r[me]||(r[me]=new Set);for(let a=0,l=e.length;at.call(e,o[1],r))}else e.addEventListener(r,o)}function he(e,r,o){if(!r)return o?xe(e,"style"):r;var t=e.style;if("string"==typeof r)return t.cssText=r;let a,l;for(l in"string"==typeof o&&(t.cssText=o=void 0),r=r||{},o=o||{})null==r[l]&&t.removeProperty(l),delete o[l];for(l in r)(a=r[l])!==o[l]&&(t.setProperty(l,a),o[l]=a);return o}function Se(e,r={},o,t){const a={};return t||M((()=>a.children=Me(e,r.children,a.children))),M((()=>r.ref&&r.ref(e))),M((()=>function(e,r,o,t,a={},l=!1){r=r||{};for(const t in a)t in r||"children"!==t&&(a[t]=Ae(e,t,null,a[t],o,l));for(const n in r){var i;"children"===n?t||Me(e,r.children):(i=r[n],a[n]=Ae(e,n,i,a[n],o,l))}}(e,r,o,!0,a,!0))),a}function ve(e,r,o){return _((()=>e(r,o)))}function $e(e,r,o,t){if(void 0!==o&&(t=t||[]),"function"!=typeof r)return Me(e,r,t,o);M((t=>Me(e,r(),t,o)),t)}function be(e,r,o){var t=r.trim().split(/\s+/);for(let r=0,a=t.length;rr.toUpperCase()))}(r)]=o):(t=a&&-1o||document});o;){var t=o[r];if(t&&!o.disabled){var a=o[r+"Data"];if(void 0!==a?t.call(o,a,e):t.call(o,e),e.cancelBubble)return}o=o._$host||o.parentNode||o.host}}function Me(e,r,o,t,a){for(;"function"==typeof o;)o=o();if(r!==o){var l=typeof r,i=void 0!==t;if(e=i&&o[0]&&o[0].parentNode||e,"string"==l||"number"==l)if("number"==l&&(r=r.toString()),i){let a=o[0];a&&3===a.nodeType?a.data=r:a=document.createTextNode(r),o=_e(e,o,t,a)}else o=""!==o&&"string"==typeof o?e.firstChild.data=r:e.textContent=r;else if(null==r||"boolean"==l)o=_e(e,o,t);else{if("function"==l)return M((()=>{let a=r();for(;"function"==typeof a;)a=a();o=Me(e,a,o,t)})),()=>o;if(Array.isArray(r)){const n=[];if(l=o&&Array.isArray(o),Pe(n,r,o,a))return M((()=>o=Me(e,n,o,t,!0))),()=>o;if(0===n.length){if(o=_e(e,o,t),i)return o}else l?0===o.length?Te(e,n,t):function(e,r,o){let t=o.length,a=r.length,l=t,i=0,n=0,d=r[a-1].nextSibling,s=null;for(;ic-n)for(var x=r[i];nr.component));return T((()=>{const e=t();switch(typeof e){case"function":return Object.assign(e,{[s]:!0}),_((()=>e(o)));case"string":var r=de.has(e),a=function(e,r=!1){return r?document.createElementNS(ye,e):document.createElement(e)}(e,r);return Se(a,o,r),a}}))}function Ge(e){return(r,o)=>{const t=o.element;return A((a=>{const l=function(e){var r=Object.keys(e),o={};for(let t=0;te))}})}return o}(r);t.addPropertyChangedCallback(((e,r)=>l[e]=r)),t.addReleaseCallback((()=>{t.renderRoot.textContent="",a()}));var i=e(l,o);return $e(t.renderRoot,i)}),function(e){if(e.assignedSlot&&e.assignedSlot._$owner)return e.assignedSlot._$owner;let r=e.parentNode;for(;r&&!r._$owner&&(!r.assignedSlot||!r.assignedSlot._$owner);)r=r.parentNode;return(r&&r.assignedSlot?r.assignedSlot:e)._$owner}(t))}}function Le(e,r,o){return 2===arguments.length&&(o=r,r={}),i(e,r)(Ge(o))}const Ce={chatflowid:"",apiHost:void 0,onRequest:void 0,chatflowConfig:void 0,theme:void 0,observersConfig:void 0};var Be='/*! tailwindcss v3.3.1 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-bottom:1.2em;margin-top:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-bottom:1.25em;margin-top:1.25em;padding-left:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-bottom:3em;margin-top:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){border-left-color:var(--tw-prose-quote-borders);border-left-width:.25rem;color:var(--tw-prose-quotes);font-style:italic;font-weight:500;margin-bottom:1.6em;margin-top:1.6em;padding-left:1em;quotes:"\\201C""\\201D""\\2018""\\2019"}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:2.25em;font-weight:800;line-height:1.1111111;margin-bottom:.8888889em;margin-top:0}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.5em;font-weight:700;line-height:1.3333333;margin-bottom:1em;margin-top:2em}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-size:1.25em;font-weight:600;line-height:1.6;margin-bottom:.6em;margin-top:1.6em}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;line-height:1.5;margin-bottom:.5em;margin-top:1.5em}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-bottom:2em;margin-top:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.3125rem;box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);color:var(--tw-prose-kbd);font-family:inherit;font-size:.875em;font-weight:500;padding:.1875em .375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:var(--tw-prose-pre-bg);border-radius:.375rem;color:var(--tw-prose-pre-code);font-size:.875em;font-weight:400;line-height:1.7142857;margin-bottom:1.7142857em;margin-top:1.7142857em;overflow-x:auto;padding:.8571429em 1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-radius:0;border-width:0;color:inherit;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.875em;line-height:1.7142857;margin-bottom:2em;margin-top:2em;table-layout:auto;text-align:left;width:100%}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-color:var(--tw-prose-th-borders);border-bottom-width:1px}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;padding-bottom:.5714286em;padding-left:.5714286em;padding-right:.5714286em;vertical-align:bottom}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-color:var(--tw-prose-td-borders);border-bottom-width:1px}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-color:var(--tw-prose-th-borders);border-top-width:1px}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0;margin-top:0}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.5em;margin-top:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(.prose>ul>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:.75em;margin-top:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em;margin-top:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-left:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-right:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:2em;margin-top:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.right-\\[-8px\\]{right:-8px}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.z-\\[1001\\]{z-index:1001}.z-\\[1002\\]{z-index:1002}.float-right{float:right}.m-0{margin:0}.m-\\[6px\\]{margin:6px}.m-auto{margin:auto}.mx-4{margin-left:16px;margin-right:16px}.my-2{margin-bottom:8px;margin-top:8px}.my-6{margin-bottom:24px;margin-top:24px}.-ml-1{margin-left:-4px}.mb-1{margin-bottom:4px}.mb-2{margin-bottom:8px}.mb-3{margin-bottom:12px}.mb-4{margin-bottom:16px}.mb-6{margin-bottom:24px}.ml-1{margin-left:4px}.ml-1\\.5{margin-left:6px}.ml-10{margin-left:40px}.ml-2{margin-left:8px}.ml-auto{margin-left:auto}.mr-1{margin-right:4px}.mr-2{margin-right:8px}.mr-3{margin-right:12px}.mr-\\[10px\\]{margin-right:10px}.mt-2{margin-top:8px}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-10{height:40px}.h-12{height:48px}.h-14{height:56px}.h-2{height:8px}.h-4{height:16px}.h-5{height:20px}.h-6{height:24px}.h-7{height:28px}.h-\\[50px\\]{height:50px}.h-\\[58px\\]{height:58px}.h-auto{height:auto}.h-full{height:100%}.max-h-60{max-height:240px}.max-h-\\[128px\\]{max-height:128px}.max-h-\\[192px\\]{max-height:192px}.max-h-\\[704px\\]{max-height:704px}.min-h-\\[56px\\]{min-height:56px}.min-h-full{min-height:100%}.w-10{width:40px}.w-12{width:48px}.w-2{width:8px}.w-4{width:16px}.w-5{width:20px}.w-6{width:24px}.w-64{width:256px}.w-7{width:28px}.w-\\[200px\\]{width:200px}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-3xl{max-width:768px}.max-w-\\[128px\\]{max-width:128px}.max-w-full{max-width:100%}.max-w-max{max-width:-moz-max-content;max-width:max-content}.max-w-md{max-width:448px}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-none{flex:none}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.flex-grow-0{flex-grow:0}.basis-auto{flex-basis:auto}.-rotate-180{--tw-rotate:-180deg}.-rotate-180,.rotate-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-0{--tw-rotate:0deg}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.animate-fade-in{animation:fade-in .3s ease-out}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(8px*(1 - var(--tw-space-x-reverse)));margin-right:calc(8px*var(--tw-space-x-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(16px*(1 - var(--tw-space-x-reverse)));margin-right:calc(16px*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(8px*var(--tw-space-y-reverse));margin-top:calc(8px*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(16px*var(--tw-space-y-reverse));margin-top:calc(16px*(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-scroll{overflow-y:scroll}.scroll-smooth{scroll-behavior:smooth}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:4px}.rounded-\\[10px\\]{border-radius:10px}.rounded-\\[6px\\]{border-radius:6px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:8px}.rounded-md{border-radius:6px}.rounded-none{border-radius:0}.rounded-xl{border-radius:12px}.rounded-b{border-bottom-left-radius:4px;border-bottom-right-radius:4px}.rounded-t{border-top-left-radius:4px;border-top-right-radius:4px}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-t-4{border-top-width:4px}.border-solid{border-style:solid}.border-dashed{border-style:dashed}.border-\\[\\#eeeeee\\]{--tw-border-opacity:1;border-color:rgb(238 238 238/var(--tw-border-opacity))}.border-current{border-color:currentColor}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgb(22 163 74/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity))}.border-red-600{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity))}.border-yellow-300{--tw-border-opacity:1;border-color:rgb(253 224 71/var(--tw-border-opacity))}.border-t-transparent{border-top-color:transparent}.border-t-white{--tw-border-opacity:1;border-top-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-\\[rgba\\(0\\2c 0\\2c 0\\2c 0\\.3\\)\\]{background-color:rgba(0,0,0,.3)}.bg-\\[rgba\\(0\\2c 0\\2c 0\\2c 0\\.4\\)\\]{background-color:rgba(0,0,0,.4)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-black\\/10{background-color:rgba(0,0,0,.1)}.bg-black\\/60{background-color:rgba(0,0,0,.6)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.bg-cover{background-size:cover}.bg-center{background-position:50%}.fill-transparent{fill:transparent}.stroke-2{stroke-width:2}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:4px}.p-10{padding:40px}.p-2{padding:8px}.p-2\\.5{padding:10px}.p-3{padding:12px}.p-4{padding:16px}.p-5{padding:20px}.p-6{padding:24px}.px-1{padding-left:4px;padding-right:4px}.px-12{padding-left:48px;padding-right:48px}.px-2{padding-left:8px;padding-right:8px}.px-3{padding-left:12px;padding-right:12px}.px-4{padding-left:16px;padding-right:16px}.px-5{padding-left:20px;padding-right:20px}.px-6{padding-left:24px;padding-right:24px}.px-\\[10px\\]{padding-left:10px;padding-right:10px}.py-1{padding-bottom:4px;padding-top:4px}.py-2{padding-bottom:8px;padding-top:8px}.py-4{padding-bottom:16px;padding-top:16px}.py-8{padding-bottom:32px;padding-top:32px}.py-\\[10px\\]{padding-bottom:10px;padding-top:10px}.pb-1{padding-bottom:4px}.pb-2{padding-bottom:8px}.pb-\\[10px\\]{padding-bottom:10px}.pl-4{padding-left:16px}.pr-0{padding-right:0}.pr-3{padding-right:12px}.pt-2{padding-top:8px}.pt-4{padding-top:16px}.pt-\\[6px\\]{padding-top:6px}.pt-\\[70px\\]{padding-top:70px}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:24px;line-height:32px}.text-\\[13px\\]{font-size:13px}.text-base{font-size:16px;line-height:24px}.text-sm{font-size:14px;line-height:20px}.text-xl{font-size:20px;line-height:28px}.text-xs{font-size:12px;line-height:16px}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.italic{font-style:italic}.leading-none{line-height:1}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-inherit{color:inherit}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-transparent{color:transparent}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.blur-\\[2px\\]{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-\\[2px\\]{--tw-blur:blur(2px)}.blur-none{--tw-blur:blur(0)}.blur-none,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-linear{transition-timing-function:linear}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}:host{--chatbot-container-bg-image:none;--chatbot-container-bg-color:transparent;--chatbot-container-font-family:"Open Sans";--chatbot-button-bg-color:#0042da;--chatbot-button-color:#fff;--chatbot-host-bubble-bg-color:#f7f8ff;--chatbot-host-bubble-color:#303235;--chatbot-guest-bubble-bg-color:#3b81f6;--chatbot-guest-bubble-color:#fff;--chatbot-input-bg-color:#fff;--chatbot-input-color:#303235;--chatbot-input-placeholder-color:#9095a0;--chatbot-header-bg-color:#fff;--chatbot-header-color:#303235;--chatbot-border-radius:6px;--PhoneInputCountryFlag-borderColor:transparent;--PhoneInput-color--focus:transparent}a{color:#16bed7;font-weight:500}a:hover{text-decoration:underline}pre{word-wrap:break-word;font-size:13px;margin:5px;overflow:auto;padding:5px;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;width:auto}.string{color:green}.number{color:#ff8c00}.boolean{color:blue}.null{color:#f0f}.key{color:#002b36}.scrollable-container::-webkit-scrollbar{display:none}.scrollable-container{-ms-overflow-style:none;scrollbar-width:none}.text-fade-in{transition:opacity .4s ease-in .2s}.bubble-typing{transition:width .4s ease-out,height .4s ease-out}.bubble1,.bubble2,.bubble3{background-color:var(--chatbot-host-bubble-color);opacity:.5}.bubble1,.bubble2{animation:chatBubbles 1s ease-in-out infinite}.bubble2{animation-delay:.3s}.bubble3{animation:chatBubbles 1s ease-in-out infinite;animation-delay:.5s}@keyframes chatBubbles{0%{transform:translateY(0)}50%{transform:translateY(-5px)}to{transform:translateY(0)}}button,input,textarea{font-weight:300}.slate-a{text-decoration:underline}.slate-html-container>div{min-height:24px}.slate-bold{font-weight:700}.slate-italic{font-style:oblique}.slate-underline{text-decoration:underline}.text-input::-moz-placeholder{color:#9095a0!important;opacity:1!important}.text-input::placeholder{color:#9095a0!important;opacity:1!important}.chatbot-container{background-color:var(--chatbot-container-bg-color);background-image:var(--chatbot-container-bg-image);font-family:Open Sans,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.file-annotation-button{background-color:#02a0a0c2;border:1px solid #02a0a0c2;border-radius:var(--chatbot-border-radius);color:var(--chatbot-button-color)}.chatbot-button{background-color:#0042da;border:1px solid #0042da;border-radius:var(--chatbot-border-radius);color:var(--chatbot-button-color)}.chatbot-button.selectable{border:1px solid #0042da}.chatbot-button.selectable,.chatbot-host-bubble{background-color:#f7f8ff;color:var(--chatbot-host-bubble-color)}.chatbot-host-bubble{word-wrap:break-word;overflow-wrap:break-word;white-space:normal;word-break:break-word}.chatbot-host-bubble>.bubble-typing{background-color:#f7f8ff;border:var(--chatbot-host-bubble-border);border-radius:6px}.chatbot-host-bubble iframe,.chatbot-host-bubble img,.chatbot-host-bubble video{border-radius:var(--chatbot-border-radius)}.chatbot-guest-bubble{word-wrap:break-word;background-color:#3b81f6;border-radius:6px;color:var(--chatbot-guest-bubble-color);overflow-wrap:break-word;white-space:normal;word-break:break-word}.chatbot-input,.feedback-input{background-color:#fff;border-radius:var(--chatbot-border-radius);box-shadow:0 2px 6px -1px rgba(0,0,0,.1);color:#303235}.chatbot-input-error-message{color:#303235}.chatbot-button>.send-icon{fill:var(--chatbot-button-color);stroke:var(--chatbot-button-color)}.chatbot-chat-view{max-width:800px}.ping span{background-color:#0042da}.rating-icon-container svg{stroke:#0042da;fill:#f7f8ff;height:42px;transition:fill .1s ease-out;width:42px}.rating-icon-container.selected svg{fill:#0042da}.rating-icon-container:hover svg{filter:brightness(.9)}.rating-icon-container:active svg{filter:brightness(.75)}.upload-progress-bar{background-color:#0042da;border-radius:var(--chatbot-border-radius)}.total-files-indicator{background-color:#0042da;color:var(--chatbot-button-color);font-size:10px}.chatbot-upload-input{transition:border-color .1s ease-out}.chatbot-upload-input.dragging-over{border-color:#0042da}.secondary-button{background-color:#f7f8ff;border-radius:var(--chatbot-border-radius);color:var(--chatbot-host-bubble-color)}.chatbot-country-select{color:#303235}.chatbot-country-select,.chatbot-date-input{background-color:#fff;border-radius:var(--chatbot-border-radius)}.chatbot-date-input{color:#303235;color-scheme:light}.chatbot-popup-blocked-toast{border-radius:var(--chatbot-border-radius)}.messagelist{border-radius:.5rem;height:100%;overflow-y:scroll;width:100%}.messagelistloading{display:flex;justify-content:center;margin-top:1rem;width:100%}.usermessage{padding:1rem 1.5rem}.usermessagewaiting-light{background:linear-gradient(270deg,#ede7f6,#e3f2fd,#ede7f6);background-position:-100% 0;background-size:200% 200%}.usermessagewaiting-dark,.usermessagewaiting-light{animation:loading-gradient 2s ease-in-out infinite;animation-direction:alternate;animation-name:loading-gradient;padding:1rem 1.5rem}.usermessagewaiting-dark{background:linear-gradient(270deg,#2e2352,#1d3d60,#2e2352);background-position:-100% 0;background-size:200% 200%;color:#ececf1}@keyframes loading-gradient{0%{background-position:-100% 0}to{background-position:100% 0}}.apimessage{animation:fadein .5s;padding:1rem 1.5rem}@keyframes fadein{0%{opacity:0}to{opacity:1}}.apimessage,.usermessage,.usermessagewaiting{display:flex}.markdownanswer{line-height:1.75}.markdownanswer a:hover{opacity:.8}.markdownanswer a{color:#16bed7;font-weight:500}.markdownanswer code{color:#15cb19;font-weight:500;white-space:pre-wrap!important}.markdownanswer ol,.markdownanswer ul{margin:1rem}.boticon,.usericon{border-radius:1rem;margin-right:1rem}.markdownanswer h1,.markdownanswer h2,.markdownanswer h3{font-size:inherit}.center{flex-direction:column;padding:10px;position:relative}.center,.cloud{align-items:center;display:flex;justify-content:center}.cloud{border-radius:.5rem;height:calc(100% - 50px);width:400px}input,textarea{background-color:transparent;border:none;font-family:Poppins,sans-serif;padding:10px}@media (max-width:640px){div[part=bot]{height:100%!important;left:0!important;max-height:unset!important;max-width:unset!important;overflow:auto;overflow-x:hidden;position:fixed!important;top:0!important;width:100%!important}.chatbot-container,.rounded-lg,div[class="flex flex-row items-center w-full h-[50px] absolute top-0 left-0 z-10"],div[part=button]{border-radius:0!important}button{cursor:default!important}}.tooltip{background:var(--tooltip-background-color,#000);border-radius:5px;color:var(--tooltip-text-color,#fff);font-size:var(--tooltip-font-size,12px);max-width:calc(100vw - 20px);padding:5px 10px;position:fixed;transition:opacity .3s ease-in-out;white-space:pre-wrap;word-break:break-word;z-index:42424242}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.spinner{animation:spin 1s linear infinite;border:4px solid hsla(0,0%,100%,.3);border-radius:50%;border-top-color:#fff;height:24px;width:24px}.hover\\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.hover\\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.hover\\:bg-transparent:hover{background-color:transparent}.hover\\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\\:brightness-90:hover{--tw-brightness:brightness(.9);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.focus\\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity))}.active\\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\\:bg-emerald-600:active{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity))}.active\\:brightness-75:active{--tw-brightness:brightness(.75);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.disabled\\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\\:opacity-50:disabled{opacity:.5}.disabled\\:brightness-100:disabled{--tw-brightness:brightness(1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.group:hover .group-hover\\:flex{display:flex}@media (min-width:640px){.sm\\:right-5{right:20px}.sm\\:my-8{margin-bottom:32px;margin-top:32px}.sm\\:w-\\[400px\\]{width:400px}.sm\\:w-full{width:100%}.sm\\:max-w-lg{max-width:512px}.sm\\:p-0{padding:0}}@media (min-width:768px){.md\\:mb-0{margin-bottom:0}.md\\:flex-row{flex-direction:row}}';const Ee=e=>null==e,Ne=e=>null!=e,Oe=async e=>{try{var r="string"==typeof e?e:e.url,o="string"!=typeof e&&Ne(e.body)?{"Content-Type":"application/json",...e.headers}:void 0;let i="string"!=typeof e&&Ne(e.body)?JSON.stringify(e.body):void 0;"string"!=typeof e&&e.formData&&(i=e.formData);var t={method:"string"==typeof e?"GET":e.method,mode:"cors",headers:o,body:i,signal:"string"!=typeof e?e.signal:void 0},a=("string"!=typeof e&&e.onRequest&&await e.onRequest(t),await fetch(r,t));let n;var l=a.headers.get("Content-Type");if(n=l&&l.includes("application/json")?await a.json():"string"!=typeof e&&"blob"===e.type?await a.blob():await a.text(),a.ok)return{data:n};{let e;throw e="object"==typeof n&&"error"in n?n.error:n||a.statusText}}catch(e){return console.error(e),{error:e}}},Ie=(e,r,o={})=>{var t=localStorage.getItem(e+"_EXTERNAL");o={...o};if(r&&(o.chatId=r),t)try{var a=JSON.parse(t);localStorage.setItem(e+"_EXTERNAL",JSON.stringify({...a,...o}))}catch(a){const r=t;o.chatId=r,localStorage.setItem(e+"_EXTERNAL",JSON.stringify(o))}else localStorage.setItem(e+"_EXTERNAL",JSON.stringify(o))},ke=e=>{if(!(e=localStorage.getItem(e+"_EXTERNAL")))return{};try{return JSON.parse(e)}catch(e){return{}}},Xe=e=>e?"number"==typeof e?e:"small"===e?32:"medium"!==e&&"large"===e?64:48:48,De=ge(''),Re=ge('Bubble button icon'),Fe=ge('