104 lines
4.4 KiB
TypeScript
104 lines
4.4 KiB
TypeScript
import { FileText, Image as ImageIcon, X } from "lucide-preact";
|
|
import type { ChatAttachment } from "@/lib/api";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type Props = {
|
|
attachments: ChatAttachment[];
|
|
tone?: "composer" | "user" | "assistant";
|
|
onRemove?: (id: string) => void;
|
|
};
|
|
|
|
function getTextPreview(value: string) {
|
|
const normalized = value.replace(/\r/g, "").trim();
|
|
if (!normalized) return "(empty file)";
|
|
return normalized.length <= 280 ? normalized : `${normalized.slice(0, 280).trimEnd()}...`;
|
|
}
|
|
|
|
function getSurfaceClasses(tone: Props["tone"]) {
|
|
if (tone === "user") {
|
|
return "border-white/12 bg-black/16 text-fuchsia-50";
|
|
}
|
|
if (tone === "assistant") {
|
|
return "border-violet-300/16 bg-violet-400/8 text-violet-50";
|
|
}
|
|
return "border-violet-300/18 bg-background/40 text-violet-50";
|
|
}
|
|
|
|
export function ChatAttachmentList({ attachments, tone = "composer", onRemove }: Props) {
|
|
if (!attachments.length) return null;
|
|
|
|
const surfaceClasses = getSurfaceClasses(tone);
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{attachments.map((attachment) => {
|
|
const isImage = attachment.kind === "image";
|
|
return (
|
|
<div key={attachment.id} className={cn("overflow-hidden rounded-xl border", surfaceClasses)}>
|
|
{isImage ? (
|
|
<div className="grid gap-0 md:grid-cols-[minmax(0,220px)_minmax(0,1fr)]">
|
|
<div className="border-b border-white/10 bg-black/10 md:border-b-0 md:border-r">
|
|
<img src={attachment.dataUrl} alt={attachment.filename} className="block max-h-56 w-full object-cover" />
|
|
</div>
|
|
<div className="flex min-w-0 flex-col gap-2 p-3">
|
|
<div className="flex items-start gap-2">
|
|
<span className="mt-0.5 rounded-md border border-white/12 bg-white/5 p-1.5">
|
|
<ImageIcon className="h-3.5 w-3.5" />
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-medium">{attachment.filename}</p>
|
|
<p className="text-xs text-muted-foreground">{attachment.mimeType}</p>
|
|
</div>
|
|
{onRemove ? (
|
|
<button
|
|
type="button"
|
|
className="rounded-md border border-white/10 p-1 text-muted-foreground transition hover:bg-white/8 hover:text-foreground"
|
|
onClick={() => onRemove(attachment.id)}
|
|
aria-label={`Remove ${attachment.filename}`}
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="p-3">
|
|
<div className="flex items-start gap-2">
|
|
<span className="mt-0.5 rounded-md border border-white/12 bg-white/5 p-1.5">
|
|
<FileText className="h-3.5 w-3.5" />
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-start gap-2">
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-medium">{attachment.filename}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{attachment.mimeType}
|
|
{attachment.truncated ? " · truncated" : ""}
|
|
</p>
|
|
</div>
|
|
{onRemove ? (
|
|
<button
|
|
type="button"
|
|
className="rounded-md border border-white/10 p-1 text-muted-foreground transition hover:bg-white/8 hover:text-foreground"
|
|
onClick={() => onRemove(attachment.id)}
|
|
aria-label={`Remove ${attachment.filename}`}
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
<pre className="mt-2 overflow-x-auto rounded-lg border border-white/8 bg-black/16 p-3 text-xs leading-5 text-inherit whitespace-pre-wrap">
|
|
{getTextPreview(attachment.text)}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|