Skip to content

Commit

Permalink
Add file upload functionality to chat window
Browse files Browse the repository at this point in the history
Fixes anthropics#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).
  • Loading branch information
Stevo-G-Swag committed Nov 2, 2024
1 parent bbff506 commit cad3807
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 4 deletions.
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

0 comments on commit cad3807

Please sign in to comment.