Untitled
import { LoadingIcon, SoundIcon } from '@/assets/icons/Icons'; import { TPromptSchema, promptSchema } from '@/utils/validations/chat'; import { zodResolver } from '@hookform/resolvers/zod'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import * as storage from '@/utils/storage'; import SendIcon from '@mui/icons-material/Send'; import { Box, CircularProgress, IconButton, Stack, Tooltip, Typography, styled } from '@mui/material'; import { useEffect, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { FormInputText } from '../forms/FormInputText'; import Message from './message'; import { useGetProjectSettings } from '@/hooks/query/useProjectSettings'; import { useParams } from 'react-router-dom'; import { Close } from '@mui/icons-material'; import { storageKey } from '@/constants/storageKey'; import { useGetApiToken } from '@/hooks/query/useApiKey'; import { useAudioChat, useCreateConversation } from '@/hooks/query/useConversation'; import { useAuthTenantId, useAuthUserName } from '@/hooks/useAuthDetail'; const ChatStyled = styled(Box)` flex: 1; overflow: hidden; p { font-size: 14px; } .chat-styled-wrapper { flex-grow: 1; height: 100%; overflow: auto; display: flex; flex-direction: column; justify-content: space-between; } .chat-scrollbar { margin-bottom: 2rem; overflow: auto; flex: 1; } .bot { background: #f1f5fe; padding: 1rem; border-radius: 9px; margin-right: 4px !important; box-shadow: inset 1px 0px 5px 0px #abbeeb7a; margin-top: 0; } .user { padding: 0 1rem 1rem; /* margin-bottom: 10px; */ } .chatbox { position: relative; max-width: 928px; width: 100%; margin: 0 auto; background: white; padding-right: 0.3rem; } .icon-button { position: absolute; top: 8px; right: 10px; } .close-button{ position: absolute; top: 8px; right: 48px; } .scroll-to-bottom { position: absolute; top: -35px; right: -25px; .MuiButtonBase-root { padding: 6px; border: 1px solid ${(props) => props.theme.palette.grey[400]}; } } `; export default function Chat({ messages, onQuestionSubmit, isLoading, isError, queryResponseId, showSource, }: any) { const { projectId, ingestId } = useParams(); const { control, handleSubmit, setValue } = useForm({ defaultValues: { prompt: "", }, resolver: zodResolver(promptSchema), }); const { mutateAsync: conversationCreate } = useCreateConversation(); const [conversationId, setConversationId] = useState(''); const [promptData, setPromptData] = useState<string>(""); const [responseTimeTaken, setResponseTimeTaken] = useState<any>([]); const [elapsedTime, setElapsedTime] = useState<number>(0); const [showOverlay, setShowOverlay] = useState(false); const [audioQueryResponseValue, setAudioQueryResponseValue] = useState<boolean>(false); const [isRecording, setIsRecording] = useState(false); const [reachedTimeLimit, setReachedTimeLimit] = useState(false); // Added state for time limit const mediaStream = useRef<MediaStream | null>(null); const mediaRecorder = useRef<MediaRecorder | null>(null); const chunks = useRef<Blob[]>([]); const inputRef = useRef(null); const messagesRef = useRef<HTMLDivElement | null>(null); const recordingTimeout = useRef<NodeJS.Timeout | null>(null); const { data } = useGetProjectSettings((projectId as string) || ""); const knowledgeBaseIdentifier = storage.get(storageKey.PROJECT_IDENTIFIER); const authTenantId = useAuthTenantId(); const [history, setHistory] = useState<any>([]); const { mutateAsync: audioConversation } = useAudioChat(); const { data: apiToken } = useGetApiToken(authTenantId); const { clientId, clientSecret } = (apiToken as any)?.tokens?.[0] || {}; const authUserName = useAuthUserName(); let apiKey: string = ''; if (apiToken) { apiKey = btoa(clientId?.concat(':', clientSecret)); } const createConversation = async (apiKey: string) => { try { const response = await conversationCreate({ projectId, api_key: apiKey, projectIngestIdentifier: ingestId, username: btoa(authUserName), userType: 'botChat' }); console.log("response", response) setConversationId(response.conversation_id as string); } catch (err: any) { console.log(err); } }; console.log('conversationId', conversationId) useEffect(() => { if (apiKey) { createConversation(apiKey); } }, [conversationCreate, projectId, apiKey, ingestId]); const scrollToBottom = () => { if (messagesRef.current) { messagesRef.current.scrollTop = messagesRef.current.scrollHeight; } }; useEffect(() => { scrollToBottom(); }, [messages]); useEffect(() => { if (!isLoading) { const audioQueryResponse = data?.data?.find( (setting: any) => setting.key === "audioQueryResponse" )?.value; setAudioQueryResponseValue(audioQueryResponse === "enabled"); } }, [data, isLoading]); useEffect(() => { if (isLoading) { setValue("prompt", ""); } }, [isLoading, setValue]); useEffect(() => { inputRef.current && (inputRef.current as HTMLInputElement).focus(); }, [messages, showOverlay]); const onSubmit = (data: TPromptSchema) => { const trimmedPrompt = data.prompt.trim(); if (!trimmedPrompt) return; setPromptData(trimmedPrompt); onQuestionSubmit(trimmedPrompt); }; const toggleOverlay = () => setShowOverlay(!showOverlay); useEffect(() => { if (promptData && isLoading) { const intervalId = setInterval(() => { setElapsedTime((prevTime) => prevTime + 1); }, 1000); return () => clearInterval(intervalId); } else if (promptData && !isLoading) { setResponseTimeTaken((prev: any) => [ ...prev, { promptQuestion: promptData, resultTime: elapsedTime }, ]); setElapsedTime(0); } }, [isLoading, promptData]); const updateHistory = (prompt: string, response: string) => { const chatHistory = [...history]; chatHistory.push([prompt, response]); setHistory(chatHistory); }; const handleStartRecording = async () => { if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === "undefined") { console.error("Browser does not support audio recording."); return; } setReachedTimeLimit(false); // Reset the time limit flag try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaStream.current = stream; mediaRecorder.current = new MediaRecorder(stream); chunks.current = []; // Reset chunks mediaRecorder.current.ondataavailable = (e) => { if (e.data.size > 0) { chunks.current.push(e.data); console.log("Chunk added, size:", e.data.size); } }; mediaRecorder.current.start(100); // Start recording and fire ondataavailable every 100ms setIsRecording(true); setShowOverlay(true); console.log("Recording started"); // Set a timeout to stop recording after 10 seconds recordingTimeout.current = setTimeout(() => { setReachedTimeLimit(true); // Update reachedTimeLimit state handleStopRecording(); }, 10000); } catch (error) { console.error("Error starting recording:", error); alert("Failed to access microphone. Please check permissions."); } }; const stopAllTracks = () => { if (mediaStream.current) { mediaStream.current.getTracks().forEach((track) => track.stop()); mediaStream.current = null; } if (mediaRecorder.current) { mediaRecorder.current = null; } if (recordingTimeout.current) { clearTimeout(recordingTimeout.current); recordingTimeout.current = null; } }; const handleStopRecording = async () => { if (mediaRecorder.current?.state === "recording") { mediaRecorder.current.stop(); await new Promise(resolve => mediaRecorder.current!.addEventListener("stop", resolve)); } stopAllTracks(); setIsRecording(false); setShowOverlay(false); console.log("reachedTimeLimit", reachedTimeLimit); if (reachedTimeLimit) { console.log("Recording reached time limit. API call cancelled."); return; } if (chunks.current.length === 0) { console.error("No audio chunks were recorded."); return; } try { const audioBlob = new Blob(chunks.current, { type: "audio/wav" }); console.log("Audio Blob size:", audioBlob.size); if (audioBlob.size === 0) { console.error("Audio Blob is empty."); return; } const fileName = `audio_${knowledgeBaseIdentifier}_${conversationId}.wav`; const file = new File([audioBlob], fileName, { type: "audio/wav" }); console.log("File created:", file); const response = await audioConversation({ projectId, file, api_key: apiKey, conversation_id: conversationId, }); console.log("Audio API Response:", response); } catch (error) { console.error("Error uploading audio:", error); alert("Error uploading audio. Please try again."); } }; const handleCancel = () => { stopAllTracks(); chunks.current = []; setIsRecording(false); setShowOverlay(false); setReachedTimeLimit(false); // Reset reachedTimeLimit state }; const lastMsgIndex = messages.length - 1; return ( <ChatStyled> <Box className="chat-styled-wrapper"> <Stack className="chat-scrollbar" spacing={2} ref={messagesRef}> <Box className="bot"> <Message message={{ text: "Hi there! How can I assist you today?", sender: "bot", }} hideFeedback={true} /> </Box> {messages?.map((message: any, index: number) => ( <Box key={index} className={message.sender === "bot" ? "bot" : "user"}> <Message index={index} lastMsgIndex={lastMsgIndex} message={message} showSource={showSource} queryResponseId={queryResponseId} timer={elapsedTime} responseDataTime={responseTimeTaken} /> </Box> ))} </Stack> {isLoading && ( <Box sx={{ pb: 4, textAlign: "left", display: "flex", alignItems: "center", paddingLeft: "1rem" }}> <Box sx={{ height: "20px", width: "20px", marginRight: "0.5rem" }}> <LoadingIcon /> </Box> <Typography color="#ccc">Generating answers, Please wait.</Typography> </Box> )} {isError && !isLoading && ( <Box sx={{ pb: 2 }}> <Typography color="error">Something went wrong.</Typography> </Box> )} <Box> <form onSubmit={(e) => { e.preventDefault(); if (isRecording) { handleStopRecording(); } else { setReachedTimeLimit(false); handleSubmit(onSubmit)(); } }} > <Box className="chatbox"> {!isRecording ? ( <FormInputText size="large" fullWidth placeholder="Type a message" variant="outlined" sx={{ input: { width: "calc(100% - 64px)" } }} name="prompt" control={control} type="text" displayError={false} disabled={isLoading} autofocus={true} multiline={true} maxRows={4} minRows={1} handleMultiLineEnter={handleSubmit(onSubmit)} inputRef={inputRef} /> ) : ( <Box> <Box borderRadius={16} bgcolor="" borderColor="red" border={1} py={2} px={1}> <Typography variant="h6"> {reachedTimeLimit ? "Recording stopped after 10 seconds. Please try again." : "Recording... (Max 10 seconds)"} </Typography> <IconButton className="close-button" onClick={handleCancel}> <Close /> </IconButton> </Box> </Box> )} <Box sx={{ position: "absolute", top: 7, right: 60, display: "flex", alignItems: "center", }} > {audioQueryResponseValue && !isRecording && ( <Tooltip title="Use Voice Mode" placement="top"> <IconButton onClick={handleStartRecording} > <SoundIcon /> </IconButton> </Tooltip> )} </Box> <IconButton type="submit" className={`icon-button ${!isLoading && "icon-button-hover"}`} > {isLoading ? ( <CircularProgress sx={{ height: "20px !important", width: "20px !important" }} /> ) : ( <SendIcon /> )} </IconButton> <Box className="scroll-to-bottom" component="span" onClick={scrollToBottom}> <Tooltip title="Scroll to Bottom"> <IconButton> <KeyboardArrowDownIcon /> </IconButton> </Tooltip> </Box> </Box> </form> </Box> </Box> </ChatStyled> ); } // const [showFeedback, setShowFeedback] = useState(false); // const [showVote, setShowVote] = useState(true); // const [voteType, setVoteType] = useState(""); // const [voteTypeName, setVoteTypeName] = useState(""); // const [voted, setVoted] = useState(false); // const [enabledFeedbackButton, setEnabledFeedbackButton] = useState(false); // const { data: voteTypeData } = useVoteType(); // const { mutateAsync: voteConversation } = useVoteConversation(); // const { mutateAsync: feedbackConversation } = useFeedbackConversation(); // const handleScroll = () => { // if (messagesRef.current) { // const messagesCo ntainer = messagesRef.current; // const atBottom = messagesContainer.scrollTop + messagesContainer.clientHeight >= messagesContainer.scrollHeight; // setShowScrollToBottom(!atBottom); // } // }; // const voteFeedback = async (vote: string) => { // try { // const filtered = (voteTypeData as any).filter( // (vType: any) => vType.systemName === vote // ); // setVoteType(filtered[0].id); // setVoteTypeName(filtered[0].systemName); // await voteConversation({ // queryResponseId: queryResponseId, // responseVoteTypeId: voteType, // }); // toast.success("Vote has been added successfully."); // setShowFeedback(true); // setShowVote(false); // } catch (error: any) { // if (error?.status === 500) { // toast.error("Internal Server Error"); // } // } // }; // const sendFeedback = async () => { // try { // setShowFeedback(false); // // await voteConversation({ // // queryResponseId: queryResponseId, // // responseVoteTypeId: voteType, // // }); // await feedbackConversation({ // queryResponseId: queryResponseId, // feedback: (getValues() as any).feedback, // }); // toast.success("Feedback has been added successfully."); // setVoted(true); // setShowVote(true); // } catch (error: any) { // if (error?.status === 500) { // toast.error("Internal Server Error"); // } // } // }; { /* {messages.length - 1 === index && !isLoading && !voted && ( <> {showVote && ( <> <Tooltip title="UpVote"> <IconButton onClick={() => { voteFeedback("upvote"); }} > {showFeedback && voteTypeName === "upvote" ? ( <ThumbUpAltIcon /> ) : ( <ThumbUpOffAltIcon /> )} </IconButton> </Tooltip> <Tooltip title="DownVote"> <IconButton onClick={() => { voteFeedback("downvote"); }} > {showFeedback && voteTypeName === "downvote" ? ( <ThumbDownAltIcon /> ) : ( <ThumbDownOffAltIcon /> )} </IconButton> </Tooltip> </> )} {showFeedback && ( <Box className="feedback-form-wrapper"> <Typography variant="subtitle2" className="feedback-form-title" > Feedback Form </Typography> <Box className="feedback-form" sx={{ display: "flex", flexDirection: "column", alignItems: "end", gap: "8px", }} > <FormInputText name={"feedback"} control={control} fullWidth sx={{ background: "white" }} typingCheck={setEnabledFeedbackButton} ></FormInputText> <Button disabled={!enabledFeedbackButton} onClick={() => { sendFeedback(); }} > Send Feedback </Button> </Box> </Box> )} </> )} */ } { // showScrollToBottom && // <Box // className="scroll-to-bottom" // component={"span"} // onClick={scrollToBottom} // > // <Tooltip title="Scroll to Bottom"> // <IconButton> // <KeyboardArrowDownIcon /> // </IconButton> // </Tooltip> // </Box> } { /* <Grid container spacing={2}> <Grid item xs={9}> <FormInputText size="small" fullWidth placeholder="Type a message" variant="outlined" sx={{ input: { color: "white", background: "#262626" } }} name="prompt" control={control} type="text" displayError={false} disabled={isLoading} /> </Grid> <Grid item xs={3}> <LoadingButton fullWidth color="primary" variant="contained" endIcon={isLoading ? null : <SendIcon />} loading={isLoading} loadingPosition="start" type="submit" onClick={handleSubmit(onSubmit)} > {isLoading ? "Generating" : "Ask"} </LoadingButton> </Grid> </Grid> */ } // import { useVoteConversation } from "@/hooks/query/useVoteController"; // import { useFeedbackConversation } from "@/hooks/query/useFeedbackController"; // import { useVoteType } from "@/hooks/query/useTypeFeedback"; // import ThumbUpOffAltIcon from "@mui/icons-material/ThumbUpOffAlt"; // import ThumbDownOffAltIcon from "@mui/icons-material/ThumbDownOffAlt"; // import ThumbDownAltIcon from "@mui/icons-material/ThumbDownAlt"; // import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"; // import { toast } from "react-toastify";
Leave a Comment