feat(25-03): create ChatFilePreview and ChatFileCard components
- ChatFileCard: icon, filename, size, download button with theme-aware bg-muted styling - ChatFilePreview: inline image rendering with constrained max-h-[300px], ChatFileCard for all other types - formatFileSize helper (B, KB, MB) - lucide icons: ImageIcon, FileCode, FileText, File per category
This commit is contained in:
parent
261db60d1d
commit
3e13ac88dc
2 changed files with 96 additions and 0 deletions
67
ui/src/components/ChatFileCard.tsx
Normal file
67
ui/src/components/ChatFileCard.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Download, File, FileCode, FileText, ImageIcon } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import type { ChatFile } from "@paperclipai/shared";
|
||||
|
||||
interface ChatFileCardProps {
|
||||
file: ChatFile;
|
||||
contentPath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function FileIcon({ category }: { category: ChatFile["category"] }) {
|
||||
const cls = "h-5 w-5 shrink-0 text-muted-foreground";
|
||||
switch (category) {
|
||||
case "image":
|
||||
return <ImageIcon className={cls} />;
|
||||
case "code":
|
||||
return <FileCode className={cls} />;
|
||||
case "document":
|
||||
return <FileText className={cls} />;
|
||||
default:
|
||||
return <File className={cls} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function ChatFileCard({ file, contentPath, className }: ChatFileCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg border border-border bg-muted p-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<FileIcon category={file.category} />
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span
|
||||
className="truncate text-sm font-medium text-foreground"
|
||||
title={file.originalFilename}
|
||||
>
|
||||
{file.originalFilename}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatFileSize(file.sizeBytes)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={contentPath}
|
||||
download={file.originalFilename}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
aria-label={`Download ${file.originalFilename}`}
|
||||
title="Download"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
ui/src/components/ChatFilePreview.tsx
Normal file
29
ui/src/components/ChatFilePreview.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { ChatFileCard } from "./ChatFileCard";
|
||||
import type { ChatFile } from "@paperclipai/shared";
|
||||
|
||||
interface ChatFilePreviewProps {
|
||||
file: ChatFile;
|
||||
contentPath: string;
|
||||
}
|
||||
|
||||
export function ChatFilePreview({ file, contentPath }: ChatFilePreviewProps) {
|
||||
if (file.category === "image") {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<a href={contentPath} target="_blank" rel="noreferrer" className="block w-fit">
|
||||
<img
|
||||
src={contentPath}
|
||||
alt={file.originalFilename}
|
||||
loading="lazy"
|
||||
className="max-h-[300px] rounded-lg object-contain"
|
||||
/>
|
||||
</a>
|
||||
{/* Always show card below image for download button */}
|
||||
<ChatFileCard file={file} contentPath={contentPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For all non-image types (code, document, other): render the file card with download
|
||||
return <ChatFileCard file={file} contentPath={contentPath} />;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue