feat: chat
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* The link card shown under a message, and in the composer before sending.
|
||||
*
|
||||
* Everything rendered here was fetched by the BFF. A client can choose whether
|
||||
* a card appears, never what it says — otherwise any link could be dressed up
|
||||
* as any site.
|
||||
*/
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { LinkPreview } from "@/lib/chat";
|
||||
|
||||
export function LinkPreviewCard({
|
||||
preview,
|
||||
onDismiss,
|
||||
compact = false,
|
||||
className,
|
||||
}: {
|
||||
preview: LinkPreview;
|
||||
/** When set, a dismiss control is shown (composer only). */
|
||||
onDismiss?: () => void;
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const body = (
|
||||
<div className="flex min-w-0 gap-2">
|
||||
{preview.image_url && (
|
||||
// Remote preview images come from arbitrary hosts, which next/image
|
||||
// would need configured per-domain.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={preview.image_url}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
className={cn(
|
||||
"shrink-0 rounded object-cover",
|
||||
compact ? "size-10" : "size-14",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
{preview.site_name && (
|
||||
<p className="truncate text-[11px] uppercase tracking-wide opacity-70">
|
||||
{preview.site_name}
|
||||
</p>
|
||||
)}
|
||||
<p className="line-clamp-2 text-xs font-medium">
|
||||
{preview.title || preview.url}
|
||||
</p>
|
||||
{!compact && preview.description && (
|
||||
<p className="line-clamp-2 text-[11px] opacity-80">
|
||||
{preview.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDismiss}
|
||||
aria-label="Remove link preview"
|
||||
className="shrink-0 self-start rounded-md p-1 opacity-70 hover:opacity-100"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const shell = cn(
|
||||
"rounded-lg border-l-2 border-current/30 bg-black/5 px-2 py-1.5 dark:bg-white/10",
|
||||
className,
|
||||
);
|
||||
|
||||
// In the composer the card is being edited, so it isn't a link; under a
|
||||
// message it is.
|
||||
if (onDismiss) return <div className={shell}>{body}</div>;
|
||||
return (
|
||||
<a
|
||||
href={preview.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(shell, "block hover:opacity-90")}
|
||||
>
|
||||
{body}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user