Untitled

 avatar
unknown
plain_text
2 months ago
21 kB
2
Indexable
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