Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file upload functionality to chat window #149

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 102 additions & 2 deletions customer-support-agent/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
<div className="flex flex-col h-screen w-full">
<TopNavBar />
<div className="flex flex-1 overflow-hidden h-screen w-full">
{config.includeLeftSidebar && <LeftSidebar />}
<ChatArea />
<ChatArea
currentUpload={currentUpload}
setCurrentUpload={setCurrentUpload}
fileInputRef={fileInputRef}
isUploading={isUploading}
handleFileSelect={handleFileSelect}
/>
{config.includeRightSidebar && <RightSidebar />}
</div>
</div>
Expand Down
53 changes: 52 additions & 1 deletion customer-support-agent/components/ChatArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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("");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -297,7 +306,19 @@ const ConversationHeader: React.FC<ConversationHeaderProps> = ({
</div>
);

function ChatArea() {
function ChatArea({
currentUpload,
setCurrentUpload,
fileInputRef,
isUploading,
handleFileSelect,
}: {
currentUpload: any;
setCurrentUpload: any;
fileInputRef: any;
isUploading: boolean;
handleFileSelect: any;
}) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -415,6 +436,7 @@ function ChatArea() {
id: crypto.randomUUID(),
role: "user",
content: typeof event === "string" ? event : input,
file: currentUpload || undefined,
};

const placeholderMessage = {
Expand Down Expand Up @@ -656,6 +678,11 @@ function ChatArea() {
content={message.content}
role={message.role}
/>
{message.file && (
<div className="mt-1.5">
<FilePreview file={message.file} size="small" />
</div>
)}
</div>
</div>
{message.role === "assistant" && (
Expand Down Expand Up @@ -690,6 +717,30 @@ function ChatArea() {
rows={1}
/>
<div className="flex justify-between items-center p-3">
<div className="flex items-center">
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => fileInputRef.current?.click()}
disabled={isLoading || isUploading}
className="h-8 w-8"
>
<Paperclip className="h-5 w-5" />
</Button>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileSelect}
/>
{currentUpload && (
<FilePreview
file={currentUpload}
onRemove={() => setCurrentUpload(null)}
/>
)}
</div>
<div>
<Image
src="/claude-icon.svg"
Expand Down
29 changes: 28 additions & 1 deletion customer-support-agent/components/RightSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { FileIcon, MessageCircleIcon } from "lucide-react";
import FullSourceModal from "./FullSourceModal";
import FilePreview from "@/components/FilePreview";

interface RAGSource {
id: string;
Expand Down Expand Up @@ -47,6 +48,7 @@ const RightSidebar: React.FC = () => {
const [shouldShowSources, setShouldShowSources] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedSource, setSelectedSource] = useState<RAGSource | null>(null);
const [uploadedFiles, setUploadedFiles] = useState<any[]>([]);

useEffect(() => {
const updateRAGSources = (
Expand Down Expand Up @@ -99,6 +101,11 @@ const RightSidebar: React.FC = () => {
setShouldShowSources(shouldShow);
};

const handleFileUpload = (event: CustomEvent<any>) => {
const file = event.detail;
setUploadedFiles((prevFiles) => [...prevFiles, file]);
};

window.addEventListener(
"updateRagSources" as any,
updateRAGSources as EventListener,
Expand All @@ -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(
Expand All @@ -117,6 +128,10 @@ const RightSidebar: React.FC = () => {
"updateSidebar" as any,
updateDebug as EventListener,
);
window.removeEventListener(
"fileUpload" as any,
handleFileUpload as EventListener,
);
};
}, []);

Expand Down Expand Up @@ -144,7 +159,7 @@ const RightSidebar: React.FC = () => {
</CardTitle>
</CardHeader>
<CardContent className="overflow-y-auto h-[calc(100%-45px)]">
{ragHistory.length === 0 && (
{ragHistory.length === 0 && uploadedFiles.length === 0 && (
<div className="text-sm text-muted-foreground">
The assistant will display sources here once finding them
</div>
Expand Down Expand Up @@ -196,6 +211,18 @@ const RightSidebar: React.FC = () => {
))}
</div>
))}
{uploadedFiles.length > 0 && (
<div className="mt-4">
<h3 className="text-sm font-medium leading-none mb-2">
Uploaded Files
</h3>
{uploadedFiles.map((file, index) => (
<div key={index} className="mb-2">
<FilePreview file={file} size="small" />
</div>
))}
</div>
)}
</CardContent>
</Card>
<FullSourceModal
Expand Down