Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 4 additions & 4 deletions frontend/src/components/AttachmentGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,16 @@ export const AttachmentGallery: React.FC<AttachmentGalleryProps> = ({

{/* Other Files */}
{otherFiles.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
<div className="space-y-1">
<h4 className="flex flex-wrap justify-end text-xs font-medium text-gray-700 dark:text-gray-300">
Files ({otherFiles.length})
</h4>
<div className="space-y-2">
<div className="flex justify-end flex-wrap gap-2">
{otherFiles.map((file) => (
<AttachmentPreview
key={file.fileId}
attachment={file}
className="w-full"
className="w-[40%]"
/>
))}
</div>
Expand Down
98 changes: 17 additions & 81 deletions frontend/src/components/AttachmentPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import React, { useState } from "react"
import { AttachmentMetadata } from "shared/types"
import {
Download,
Eye,
FileText,
Image,
Video,
Music,
Archive,
File,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -17,27 +10,14 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { authFetch } from "@/utils/authFetch"
import { getFileType } from "@/utils/fileUtils"
import { getFileIcon } from "@/components/ChatBox"

interface AttachmentPreviewProps {
attachment: AttachmentMetadata
className?: string
}

const getFileIcon = (fileType: string) => {
if (fileType.startsWith("image/")) return Image
if (fileType.startsWith("video/")) return Video
if (fileType.startsWith("audio/")) return Music
if (fileType.includes("pdf") || fileType.includes("document")) return FileText
if (
fileType.includes("zip") ||
fileType.includes("tar") ||
fileType.includes("gz")
)
return Archive
return File
}

const formatFileSize = (bytes: number): string => {
if (bytes === 0) return "0 Bytes"
const k = 1024
Expand All @@ -53,46 +33,11 @@ export const AttachmentPreview: React.FC<AttachmentPreviewProps> = ({
const [showImageModal, setShowImageModal] = useState(false)
const [imageError, setImageError] = useState(false)

const FileIcon = getFileIcon(attachment.fileType)
const isImage = attachment.isImage && !imageError
const thumbnailUrl = attachment.thumbnailPath
? `/api/v1/attachments/${attachment.fileId}/thumbnail`
: null

const handleDownload = async () => {
let url: string | null = null
try {
const response = await authFetch(
`/api/v1/attachments/${attachment.fileId}`,
{
credentials: "include",
},
)
if (!response.ok) {
if (response.status === 401) {
throw new Error("Please log in to download attachments")
}
throw new Error(`Download failed: ${response.statusText}`)
}

const blob = await response.blob()
url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = attachment.fileName
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
} catch (error) {
console.error("Download failed:", error)
alert(error instanceof Error ? error.message : "Download failed")
} finally {
if (url) {
window.URL.revokeObjectURL(url)
}
}
}

const handleImageView = () => {
if (isImage) {
setShowImageModal(true)
Expand Down Expand Up @@ -126,7 +71,7 @@ export const AttachmentPreview: React.FC<AttachmentPreviewProps> = ({
</div>
) : (
<div className="w-12 h-12 flex items-center justify-center bg-gray-200 dark:bg-gray-700 rounded-md">
<FileIcon className="w-6 h-6 text-gray-600 dark:text-gray-400" />
{getFileIcon(getFileType({type: attachment.fileType, name: attachment.fileName}))}
</div>
)}
</div>
Expand All @@ -137,33 +82,24 @@ export const AttachmentPreview: React.FC<AttachmentPreviewProps> = ({
{attachment.fileName}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatFileSize(attachment.fileSize)} • {attachment.fileType}
{formatFileSize(attachment.fileSize)} • {getFileType({type: attachment.fileType, name: attachment.fileName})}
</p>
</div>

{/* Actions */}
<div className="flex items-center space-x-2">
{isImage && (
<Button
variant="ghost"
size="sm"
onClick={handleImageView}
className="p-2"
aria-label={`Preview ${attachment.fileName}`}
>
<Eye className="w-4 h-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={handleDownload}
className="p-2"
aria-label={`Download ${attachment.fileName}`}
>
<Download className="w-4 h-4" />
</Button>
</div>
{isImage && (
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={handleImageView}
className="p-2"
aria-label={`Preview ${attachment.fileName}`}
>
<Eye className="w-4 h-4" />
</Button>
</div>
)}

{/* Image Modal */}
{isImage && (
Expand Down
75 changes: 57 additions & 18 deletions frontend/src/components/ChatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
X,
File,
Loader2,
FileText,
FileSpreadsheet,
Presentation,
FileImage,
} from "lucide-react"
import Attach from "@/assets/attach.svg?react"
import {
Expand Down Expand Up @@ -72,6 +76,7 @@ import {
createToastNotifier,
createImagePreview,
cleanupPreviewUrls,
getFileType,
} from "@/utils/fileUtils"
import { authFetch } from "@/utils/authFetch"

Expand All @@ -82,6 +87,26 @@ interface SelectedFile {
uploading?: boolean
uploadError?: string
preview?: string // URL for image preview
fileType?: string
}

export const getFileIcon = (fileType: string | undefined) => {
switch (fileType) {
case "Image":
return <FileImage size={24} className="text-blue-500 dark:text-blue-400 flex-shrink-0" />
case "Document":
return <FileText size={24} className="text-blue-600 dark:text-blue-400 flex-shrink-0" />
case "Spreadsheet":
return <FileSpreadsheet size={24} className="text-green-600 dark:text-green-400 flex-shrink-0" />
case "Presentation":
return <Presentation size={24} className="text-orange-600 dark:text-orange-400 flex-shrink-0" />
case "PDF":
return <FileText size={24} className="text-red-600 dark:text-red-400 flex-shrink-0" />
case "Text":
return <FileText size={24} className="text-gray-600 dark:text-gray-400 flex-shrink-0" />
default:
return <File size={24} className="text-gray-500 dark:text-gray-400 flex-shrink-0" />
}
}

// Add attachment limit constant
Expand Down Expand Up @@ -498,6 +523,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
id: generateFileId(),
uploading: false,
preview: createImagePreview(file),
fileType: getFileType(file),
}))

setSelectedFiles((prev) => {
Expand Down Expand Up @@ -2097,10 +2123,10 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(

const items = e.clipboardData?.items
if (items) {
// Handle image paste
// Handle file paste (all supported file types)
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (item.type.indexOf("image") !== -1) {
if (item.kind === "file") {
// Check attachment limit before processing
if (selectedFiles.length >= MAX_ATTACHMENTS) {
showToast(
Expand All @@ -2113,15 +2139,27 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(

const file = item.getAsFile()
if (file) {
// Process the pasted image file directly
// The file will be processed with a default name by the processFiles function
processFiles([file])
showToast(
"Image pasted",
"Image has been added to your message.",
false,
)
return // Exit early since we handled the image
// Check if the file type is supported
const isValid = validateAndDeduplicateFiles([file], showToast)
if (isValid.length > 0) {
// Process the pasted file
processFiles([file])
const fileType = getFileType(file)

showToast(
"File pasted",
`${fileType} has been added to your message.`,
false,
)
return // Exit early since we handled the file
} else {
showToast(
"Unsupported file type",
"This file type is not supported for attachments.",
true,
)
return
}
}
}
}
Expand Down Expand Up @@ -2450,14 +2488,14 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
? `${selectedFile.file.name.substring(0, 9)}...`
: selectedFile.file.name}
</span>
<span className="text-white text-xs opacity-80 block">
{selectedFile.fileType}
</span>
</div>
</div>
) : (
<div className="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500 transition-colors min-w-0">
<File
size={24}
className="text-gray-500 dark:text-gray-400 flex-shrink-0"
/>
{getFileIcon(selectedFile.fileType)}
<div className="flex-1 min-w-0">
<span
className="text-sm text-gray-700 dark:text-gray-300 truncate block max-w-[120px]"
Expand All @@ -2469,7 +2507,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
className="text-xs text-gray-500 dark:text-gray-400 truncate block max-w-[120px]"
title={getExtension(selectedFile.file)}
>
{getExtension(selectedFile.file)}
{selectedFile.fileType}
</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
Expand Down Expand Up @@ -2533,7 +2571,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
title={
selectedFiles.length >= MAX_ATTACHMENTS
? `Maximum ${MAX_ATTACHMENTS} attachments allowed`
: "Attach files"
: "Attach files (images, documents, spreadsheets, presentations, PDFs, text files)"
}
/>
{showAdvancedOptions && (
Expand Down Expand Up @@ -3299,7 +3337,8 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
multiple
className="hidden"
onChange={handleFileChange}
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp"
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp,text/plain,text/csv,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,text/markdown,.txt,.csv,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.md"
title="Supported file types: Images (JPEG, PNG, GIF, WebP), Documents (DOC, DOCX), Spreadsheets (XLS, XLSX, CSV), Presentations (PPT, PPTX), PDFs, and Text files (TXT, MD)"
/>
</div>
)
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/utils/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,41 @@ const isImageFile = (file: File): boolean => {
)
}

// Get file type category for display purposes
export const getFileType = (file: File | { type: string, name: string }): string => {
if (file instanceof File && isImageFile(file)) {
return "Image"
}

// Check for document types
if (file.type.includes("word") || file.name.toLowerCase().match(/\.(doc|docx)$/)) {
return "Document"
}

// Check for spreadsheet types
if (file.type.includes("excel") || file.type.includes("spreadsheet") || file.name.toLowerCase().match(/\.(xls|xlsx|csv)$/)) {
return "Spreadsheet"
}

// Check for presentation types
if (file.type.includes("powerpoint") || file.type.includes("presentation") || file.name.toLowerCase().match(/\.(ppt|pptx)$/)) {
return "Presentation"
}

// Check for PDF
if (file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf")) {
return "PDF"
}

// Check for text files
if (file.type.startsWith("text/") || file.name.toLowerCase().match(/\.(txt|md)$/)) {
return "Text"
}

// Default fallback
return "File"
}

// Create preview URL for image files
export const createImagePreview = (file: File): string | undefined => {
if (isImageFile(file)) {
Expand Down
Loading