From cad3807d6c73afdfa9ad278f5cb86098569d94e6 Mon Sep 17 00:00:00 2001 From: MarBeanAI Date: Fri, 1 Nov 2024 20:19:08 -0500 Subject: [PATCH] Add file upload functionality to chat window Fixes #19 Add file upload functionality to the chat window and display uploaded files. * **File Upload Handling:** - Add file upload functionality to `customer-support-agent/app/page.tsx`. - Implement file handling logic for text, image, and PDF files. - Display uploaded files and their contents in the chat window. - Add progress indicators for large file uploads. * **Chat Area Updates:** - Update `customer-support-agent/components/ChatArea.tsx` to handle file uploads and display file previews inline. - Integrate file preview components from `financial-data-analyst/components/FilePreview.tsx`. - Add expandable file preview functionality. * **Right Sidebar Updates:** - Add a separate file section for uploaded files in `customer-support-agent/components/RightSidebar.tsx`. - Display thumbnails or icons for each uploaded file. - Open a modal with a preview when clicking on a file. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/anthropics/anthropic-quickstarts/issues/19?shareId=XXXX-XXXX-XXXX-XXXX). --- customer-support-agent/app/page.tsx | 104 +++++++++++++++++- .../components/ChatArea.tsx | 53 ++++++++- .../components/RightSidebar.tsx | 29 ++++- 3 files changed, 182 insertions(+), 4 deletions(-) diff --git a/customer-support-agent/app/page.tsx b/customer-support-agent/app/page.tsx index 77baa8e7..b6e119ec 100644 --- a/customer-support-agent/app/page.tsx +++ b/customer-support-agent/app/page.tsx @@ -1,8 +1,11 @@ -import React from "react"; +import React, { useState, useRef } from "react"; import dynamic from "next/dynamic"; import TopNavBar from "@/components/TopNavBar"; import ChatArea from "@/components/ChatArea"; import config from "@/config"; +import { readFileAsText, readFileAsBase64, readFileAsPDFText } from "@/utils/fileHandling"; +import { toast } from "@/hooks/use-toast"; +import FilePreview from "@/components/FilePreview"; const LeftSidebar = dynamic(() => import("@/components/LeftSidebar"), { ssr: false, @@ -12,12 +15,109 @@ const RightSidebar = dynamic(() => import("@/components/RightSidebar"), { }); export default function Home() { + const [currentUpload, setCurrentUpload] = useState(null); + const fileInputRef = useRef(null); + const [isUploading, setIsUploading] = useState(false); + + const handleFileSelect = async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + + setIsUploading(true); + + let loadingToastRef; + + if (file.type === "application/pdf") { + loadingToastRef = toast({ + title: "Processing PDF", + description: "Extracting text content...", + duration: Infinity, + }); + } + + try { + const isImage = file.type.startsWith("image/"); + const isPDF = file.type === "application/pdf"; + let base64Data = ""; + let isText = false; + + if (isImage) { + base64Data = await readFileAsBase64(file); + isText = false; + } else if (isPDF) { + try { + const pdfText = await readFileAsPDFText(file); + base64Data = btoa(encodeURIComponent(pdfText)); + isText = true; + } catch (error) { + console.error("Failed to parse PDF:", error); + toast({ + title: "PDF parsing failed", + description: "Unable to extract text from the PDF", + variant: "destructive", + }); + return; + } + } else { + try { + const textContent = await readFileAsText(file); + base64Data = btoa(encodeURIComponent(textContent)); + isText = true; + } catch (error) { + console.error("Failed to read as text:", error); + toast({ + title: "Invalid file type", + description: "File must be readable as text, PDF, or be an image", + variant: "destructive", + }); + return; + } + } + + setCurrentUpload({ + base64: base64Data, + fileName: file.name, + mediaType: isText ? "text/plain" : file.type, + isText, + }); + + toast({ + title: "File uploaded", + description: `${file.name} ready to analyze`, + }); + } catch (error) { + console.error("Error processing file:", error); + toast({ + title: "Upload failed", + description: "Failed to process the file", + variant: "destructive", + }); + } finally { + setIsUploading(false); + if (loadingToastRef) { + loadingToastRef.dismiss(); + if (file.type === "application/pdf") { + toast({ + title: "PDF Processed", + description: "Text extracted successfully", + }); + } + } + } + }; + return (
{config.includeLeftSidebar && } - + {config.includeRightSidebar && }
diff --git a/customer-support-agent/components/ChatArea.tsx b/customer-support-agent/components/ChatArea.tsx index 12b47538..bd9f7a11 100644 --- a/customer-support-agent/components/ChatArea.tsx +++ b/customer-support-agent/components/ChatArea.tsx @@ -14,6 +14,7 @@ import { BookOpenText, ChevronDown, Send, + Paperclip, } from "lucide-react"; import "highlight.js/styles/atom-one-dark.css"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; @@ -25,6 +26,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import FilePreview from "@/components/FilePreview"; +import { toast } from "@/hooks/use-toast"; const TypedText = ({ text = "", delay = 5 }) => { const [displayedText, setDisplayedText] = useState(""); @@ -200,6 +203,12 @@ interface Message { id: string; role: string; content: string; + file?: { + base64: string; + fileName: string; + mediaType: string; + isText?: boolean; + }; } // Define the props interface for ConversationHeader @@ -297,7 +306,19 @@ const ConversationHeader: React.FC = ({ ); -function ChatArea() { +function ChatArea({ + currentUpload, + setCurrentUpload, + fileInputRef, + isUploading, + handleFileSelect, +}: { + currentUpload: any; + setCurrentUpload: any; + fileInputRef: any; + isUploading: boolean; + handleFileSelect: any; +}) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -415,6 +436,7 @@ function ChatArea() { id: crypto.randomUUID(), role: "user", content: typeof event === "string" ? event : input, + file: currentUpload || undefined, }; const placeholderMessage = { @@ -656,6 +678,11 @@ function ChatArea() { content={message.content} role={message.role} /> + {message.file && ( +
+ +
+ )} {message.role === "assistant" && ( @@ -690,6 +717,30 @@ function ChatArea() { rows={1} />
+
+ + + {currentUpload && ( + setCurrentUpload(null)} + /> + )} +
{ const [shouldShowSources, setShouldShowSources] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedSource, setSelectedSource] = useState(null); + const [uploadedFiles, setUploadedFiles] = useState([]); useEffect(() => { const updateRAGSources = ( @@ -99,6 +101,11 @@ const RightSidebar: React.FC = () => { setShouldShowSources(shouldShow); }; + const handleFileUpload = (event: CustomEvent) => { + const file = event.detail; + setUploadedFiles((prevFiles) => [...prevFiles, file]); + }; + window.addEventListener( "updateRagSources" as any, updateRAGSources as EventListener, @@ -107,6 +114,10 @@ const RightSidebar: React.FC = () => { "updateSidebar" as any, updateDebug as EventListener, ); + window.addEventListener( + "fileUpload" as any, + handleFileUpload as EventListener, + ); return () => { window.removeEventListener( @@ -117,6 +128,10 @@ const RightSidebar: React.FC = () => { "updateSidebar" as any, updateDebug as EventListener, ); + window.removeEventListener( + "fileUpload" as any, + handleFileUpload as EventListener, + ); }; }, []); @@ -144,7 +159,7 @@ const RightSidebar: React.FC = () => { - {ragHistory.length === 0 && ( + {ragHistory.length === 0 && uploadedFiles.length === 0 && (
The assistant will display sources here once finding them
@@ -196,6 +211,18 @@ const RightSidebar: React.FC = () => { ))}
))} + {uploadedFiles.length > 0 && ( +
+

+ Uploaded Files +

+ {uploadedFiles.map((file, index) => ( +
+ +
+ ))} +
+ )}