llama-stack-mirror/llama_stack/ui/components/ui/file-preview.tsx
2025-08-14 15:58:43 -06:00

153 lines
4.8 KiB
TypeScript

"use client";
import React, { useEffect } from "react";
import { motion } from "framer-motion";
import { FileIcon, X } from "lucide-react";
interface FilePreviewProps {
file: File;
onRemove?: () => void;
}
export const FilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
(props, ref) => {
if (props.file.type.startsWith("image/")) {
return <ImageFilePreview {...props} ref={ref} />;
}
if (
props.file.type.startsWith("text/") ||
props.file.name.endsWith(".txt") ||
props.file.name.endsWith(".md")
) {
return <TextFilePreview {...props} ref={ref} />;
}
return <GenericFilePreview {...props} ref={ref} />;
}
);
FilePreview.displayName = "FilePreview";
const ImageFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
return (
<motion.div
ref={ref}
className="relative flex max-w-[200px] rounded-md border p-1.5 pr-2 text-xs"
layout
initial={{ opacity: 0, y: "100%" }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: "100%" }}
>
<div className="flex w-full items-center space-x-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt={`Attachment ${file.name}`}
className="grid h-10 w-10 shrink-0 place-items-center rounded-sm border bg-muted object-cover"
src={URL.createObjectURL(file)}
/>
<span className="w-full truncate text-muted-foreground">
{file.name}
</span>
</div>
{onRemove ? (
<button
className="absolute -right-2 -top-2 flex h-4 w-4 items-center justify-center rounded-full border bg-background"
type="button"
onClick={onRemove}
aria-label="Remove attachment"
>
<X className="h-2.5 w-2.5" />
</button>
) : null}
</motion.div>
);
}
);
ImageFilePreview.displayName = "ImageFilePreview";
const TextFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
const [preview, setPreview] = React.useState<string>("");
useEffect(() => {
const reader = new FileReader();
reader.onload = e => {
const text = e.target?.result as string;
setPreview(text.slice(0, 50) + (text.length > 50 ? "..." : ""));
};
reader.readAsText(file);
}, [file]);
return (
<motion.div
ref={ref}
className="relative flex max-w-[200px] rounded-md border p-1.5 pr-2 text-xs"
layout
initial={{ opacity: 0, y: "100%" }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: "100%" }}
>
<div className="flex w-full items-center space-x-2">
<div className="grid h-10 w-10 shrink-0 place-items-center rounded-sm border bg-muted p-0.5">
<div className="h-full w-full overflow-hidden text-[6px] leading-none text-muted-foreground">
{preview || "Loading..."}
</div>
</div>
<span className="w-full truncate text-muted-foreground">
{file.name}
</span>
</div>
{onRemove ? (
<button
className="absolute -right-2 -top-2 flex h-4 w-4 items-center justify-center rounded-full border bg-background"
type="button"
onClick={onRemove}
aria-label="Remove attachment"
>
<X className="h-2.5 w-2.5" />
</button>
) : null}
</motion.div>
);
}
);
TextFilePreview.displayName = "TextFilePreview";
const GenericFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
return (
<motion.div
ref={ref}
className="relative flex max-w-[200px] rounded-md border p-1.5 pr-2 text-xs"
layout
initial={{ opacity: 0, y: "100%" }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: "100%" }}
>
<div className="flex w-full items-center space-x-2">
<div className="grid h-10 w-10 shrink-0 place-items-center rounded-sm border bg-muted">
<FileIcon className="h-6 w-6 text-foreground" />
</div>
<span className="w-full truncate text-muted-foreground">
{file.name}
</span>
</div>
{onRemove ? (
<button
className="absolute -right-2 -top-2 flex h-4 w-4 items-center justify-center rounded-full border bg-background"
type="button"
onClick={onRemove}
aria-label="Remove attachment"
>
<X className="h-2.5 w-2.5" />
</button>
) : null}
</motion.div>
);
}
);
GenericFilePreview.displayName = "GenericFilePreview";