diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx index 1e3c0c31..be008758 100644 --- a/src/interface/web/app/chat/page.tsx +++ b/src/interface/web/app/chat/page.tsx @@ -27,14 +27,14 @@ interface ChatBodyDataProps { setUploadedFiles: (files: string[]) => void; isMobileWidth?: boolean; isLoggedIn: boolean; - setImage64: (image64: string) => void; + setImages: (images: string[]) => void; } function ChatBodyData(props: ChatBodyDataProps) { const searchParams = useSearchParams(); const conversationId = searchParams.get("conversationId"); const [message, setMessage] = useState(""); - const [image, setImage] = useState(null); + const [images, setImages] = useState([]); const [processingMessage, setProcessingMessage] = useState(false); const [agentMetadata, setAgentMetadata] = useState(null); @@ -44,17 +44,20 @@ function ChatBodyData(props: ChatBodyDataProps) { const chatHistoryCustomClassName = props.isMobileWidth ? "w-full" : "w-4/6"; useEffect(() => { - if (image) { - props.setImage64(encodeURIComponent(image)); + if (images.length > 0) { + const encodedImages = images.map((image) => encodeURIComponent(image)); + props.setImages(encodedImages); } - }, [image, props.setImage64]); + }, [images, props.setImages]); useEffect(() => { - const storedImage = localStorage.getItem("image"); - if (storedImage) { - setImage(storedImage); - props.setImage64(encodeURIComponent(storedImage)); - localStorage.removeItem("image"); + const storedImages = localStorage.getItem("images"); + if (storedImages) { + const parsedImages: string[] = JSON.parse(storedImages); + setImages(parsedImages); + const encodedImages = parsedImages.map((img: string) => encodeURIComponent(img)); + props.setImages(encodedImages); + localStorage.removeItem("images"); } const storedMessage = localStorage.getItem("message"); @@ -62,7 +65,7 @@ function ChatBodyData(props: ChatBodyDataProps) { setProcessingMessage(true); setQueryToProcess(storedMessage); } - }, [setQueryToProcess]); + }, [setQueryToProcess, props.setImages]); useEffect(() => { if (message) { @@ -84,6 +87,7 @@ function ChatBodyData(props: ChatBodyDataProps) { props.streamedMessages[props.streamedMessages.length - 1].completed ) { setProcessingMessage(false); + setImages([]); // Reset images after processing } else { setMessage(""); } @@ -113,7 +117,7 @@ function ChatBodyData(props: ChatBodyDataProps) { agentColor={agentMetadata?.color} isLoggedIn={props.isLoggedIn} sendMessage={(message) => setMessage(message)} - sendImage={(image) => setImage(image)} + sendImage={(image) => setImages((prevImages) => [...prevImages, image])} sendDisabled={processingMessage} chatOptionsData={props.chatOptionsData} conversationId={conversationId} @@ -135,7 +139,7 @@ export default function Chat() { const [queryToProcess, setQueryToProcess] = useState(""); const [processQuerySignal, setProcessQuerySignal] = useState(false); const [uploadedFiles, setUploadedFiles] = useState([]); - const [image64, setImage64] = useState(""); + const [images, setImages] = useState([]); const locationData = useIPLocationData() || { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, @@ -171,7 +175,7 @@ export default function Chat() { completed: false, timestamp: new Date().toISOString(), rawQuery: queryToProcess || "", - uploadedImageData: decodeURIComponent(image64), + images: images, }; setMessages((prevMessages) => [...prevMessages, newStreamMessage]); setProcessQuerySignal(true); @@ -202,7 +206,7 @@ export default function Chat() { if (done) { setQueryToProcess(""); setProcessQuerySignal(false); - setImage64(""); + setImages([]); break; } @@ -250,7 +254,7 @@ export default function Chat() { country_code: locationData.countryCode, timezone: locationData.timezone, }), - ...(image64 && { image: image64 }), + ...(images.length > 0 && { images: images }), }; const response = await fetch(chatAPI, { @@ -264,7 +268,8 @@ export default function Chat() { try { await readChatStream(response); } catch (err) { - console.error(err); + const apiError = await response.json(); + console.error(apiError); // Retrieve latest message being processed const currentMessage = messages.find((message) => !message.completed); if (!currentMessage) return; @@ -273,7 +278,11 @@ export default function Chat() { const errorMessage = (err as Error).message; if (errorMessage.includes("Error in input stream")) currentMessage.rawResponse = `Woops! The connection broke while I was writing my thoughts down. Maybe try again in a bit or dislike this message if the issue persists?`; - else + else if (response.status === 429) { + "detail" in apiError + ? (currentMessage.rawResponse = `${apiError.detail}`) + : (currentMessage.rawResponse = `I'm a bit overwhelmed at the moment. Could you try again in a bit or dislike this message if the issue persists?`); + } else currentMessage.rawResponse = `Umm, not sure what just happened. I see this error message: ${errorMessage}. Could you try again or dislike this message if the issue persists?`; // Complete message streaming teardown properly @@ -332,7 +341,7 @@ export default function Chat() { setUploadedFiles={setUploadedFiles} isMobileWidth={isMobileWidth} onConversationIdChange={handleConversationIdChange} - setImage64={setImage64} + setImages={setImages} /> diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 9342ccc8..944aed9f 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -299,7 +299,7 @@ export default function ChatHistory(props: ChatHistoryProps) { created: message.timestamp, by: "you", automationId: "", - uploadedImageData: message.uploadedImageData, + images: message.images, }} customClassName="fullHistory" borderLeftColor={`${data?.agent?.color}-500`} @@ -348,7 +348,6 @@ export default function ChatHistory(props: ChatHistoryProps) { created: new Date().getTime().toString(), by: "you", automationId: "", - uploadedImageData: props.pendingMessage, }} customClassName="fullHistory" borderLeftColor={`${data?.agent?.color}-500`} diff --git a/src/interface/web/app/components/chatInputArea/chatInputArea.tsx b/src/interface/web/app/components/chatInputArea/chatInputArea.tsx index d85d6a54..944348ad 100644 --- a/src/interface/web/app/components/chatInputArea/chatInputArea.tsx +++ b/src/interface/web/app/components/chatInputArea/chatInputArea.tsx @@ -3,23 +3,7 @@ import React, { useEffect, useRef, useState } from "react"; import DOMPurify from "dompurify"; import "katex/dist/katex.min.css"; -import { - ArrowRight, - ArrowUp, - Browser, - ChatsTeardrop, - GlobeSimple, - Gps, - Image, - Microphone, - Notebook, - Paperclip, - X, - Question, - Robot, - Shapes, - Stop, -} from "@phosphor-icons/react"; +import { ArrowUp, Microphone, Paperclip, X, Stop } from "@phosphor-icons/react"; import { Command, @@ -78,10 +62,11 @@ export default function ChatInputArea(props: ChatInputProps) { const [loginRedirectMessage, setLoginRedirectMessage] = useState(null); const [showLoginPrompt, setShowLoginPrompt] = useState(false); - const [recording, setRecording] = useState(false); const [imageUploaded, setImageUploaded] = useState(false); - const [imagePath, setImagePath] = useState(""); - const [imageData, setImageData] = useState(null); + const [imagePaths, setImagePaths] = useState([]); + const [imageData, setImageData] = useState([]); + + const [recording, setRecording] = useState(false); const [mediaRecorder, setMediaRecorder] = useState(null); const [progressValue, setProgressValue] = useState(0); @@ -106,27 +91,31 @@ export default function ChatInputArea(props: ChatInputProps) { useEffect(() => { async function fetchImageData() { - if (imagePath) { - const response = await fetch(imagePath); - const blob = await response.blob(); - const reader = new FileReader(); - reader.onload = function () { - const base64data = reader.result; - setImageData(base64data as string); - }; - reader.readAsDataURL(blob); + if (imagePaths.length > 0) { + const newImageData = await Promise.all( + imagePaths.map(async (path) => { + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsDataURL(blob); + }); + }), + ); + setImageData(newImageData); } setUploading(false); } setUploading(true); fetchImageData(); - }, [imagePath]); + }, [imagePaths]); function onSendMessage() { if (imageUploaded) { setImageUploaded(false); - setImagePath(""); - props.sendImage(imageData || ""); + setImagePaths([]); + imageData.forEach((data) => props.sendImage(data)); } if (!message.trim()) return; @@ -172,18 +161,23 @@ export default function ChatInputArea(props: ChatInputProps) { setShowLoginPrompt(true); return; } - // check for image file + // check for image files const image_endings = ["jpg", "jpeg", "png", "webp"]; + const newImagePaths: string[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; const file_extension = file.name.split(".").pop(); if (image_endings.includes(file_extension || "")) { - setImageUploaded(true); - setImagePath(DOMPurify.sanitize(URL.createObjectURL(file))); - return; + newImagePaths.push(DOMPurify.sanitize(URL.createObjectURL(file))); } } + if (newImagePaths.length > 0) { + setImageUploaded(true); + setImagePaths((prevPaths) => [...prevPaths, ...newImagePaths]); + return; + } + uploadDataForIndexing( files, setWarning, @@ -288,9 +282,12 @@ export default function ChatInputArea(props: ChatInputProps) { setIsDragAndDropping(false); } - function removeImageUpload() { - setImageUploaded(false); - setImagePath(""); + function removeImageUpload(index: number) { + setImagePaths((prevPaths) => prevPaths.filter((_, i) => i !== index)); + setImageData((prevData) => prevData.filter((_, i) => i !== index)); + if (imagePaths.length === 1) { + setImageUploaded(false); + } } return ( @@ -407,24 +404,11 @@ export default function ChatInputArea(props: ChatInputProps) { )}
- {imageUploaded && ( -
-
- img -
-
- -
-
- )} - -
+
+ +
+
+
+ {imageUploaded && + imagePaths.map((path, index) => ( +
+ {`img-${index}`} + +
+ ))} +