adds attachment support
This commit is contained in:
103
web/src/components/chat/chat-attachment-list.tsx
Normal file
103
web/src/components/chat/chat-attachment-list.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Message } from "@/lib/api";
|
||||
import { ChatAttachmentList } from "@/components/chat/chat-attachment-list";
|
||||
import { getMessageAttachments, type Message } from "@/lib/api";
|
||||
import { MarkdownContent } from "@/components/markdown/markdown-content";
|
||||
import { Globe2, Link2, Wrench } from "lucide-preact";
|
||||
|
||||
@@ -68,28 +69,30 @@ export function ChatMessagesPanel({ messages, isLoading, isSending }: Props) {
|
||||
|
||||
const isUser = message.role === "user";
|
||||
const isPendingAssistant = message.id.startsWith("temp-assistant-") && isSending && message.content.trim().length === 0;
|
||||
const attachments = getMessageAttachments(message.metadata);
|
||||
return (
|
||||
<div key={message.id} className={cn("flex", isUser ? "justify-end" : "justify-start")}>
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-[85%]",
|
||||
"max-w-[85%] space-y-3",
|
||||
isUser
|
||||
? "rounded-xl border border-violet-300/24 bg-[linear-gradient(135deg,hsl(258_86%_48%_/_0.86),hsl(278_72%_29%_/_0.86))] px-4 py-3 text-sm leading-6 text-fuchsia-50 shadow-sm"
|
||||
: "text-base leading-7 text-violet-50"
|
||||
)}
|
||||
>
|
||||
{attachments.length ? <ChatAttachmentList attachments={attachments} tone={isUser ? "user" : "assistant"} /> : null}
|
||||
{isPendingAssistant ? (
|
||||
<span className="inline-flex items-center gap-1" aria-label="Assistant is typing" role="status">
|
||||
<span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]" />
|
||||
<span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:140ms]" />
|
||||
<span className="inline-block h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:280ms]" />
|
||||
</span>
|
||||
) : (
|
||||
) : message.content.trim() ? (
|
||||
<MarkdownContent
|
||||
markdown={message.content}
|
||||
className={cn("[&_a]:text-inherit [&_a]:underline", isUser ? "leading-[1.78] text-fuchsia-50" : "leading-[1.82] text-violet-50")}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user