/* Copyright 2026 Marimo. All rights reserved. */
import type { FileUIPart } from "ai";
import {
AtSignIcon,
FileIcon,
FileTextIcon,
ImageIcon,
PaperclipIcon,
SendHorizontalIcon,
StopCircleIcon,
XIcon,
} from "lucide-react";
import { useState } from "react";
import { cn } from "@/utils/cn";
import { isUrl } from "@/utils/urls";
import { Spinner } from "../icons/spinner";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Tooltip } from "../ui/tooltip";
import { SUPPORTED_ATTACHMENT_TYPES } from "./chat-utils";
export const AttachmentRenderer = ({
attachment,
}: {
attachment: FileUIPart;
}) => {
if (attachment.mediaType?.startsWith("image/")) {
return (
);
}
return (
{attachment.filename || "Attachment"}
);
};
export const FileAttachmentPill = ({
file,
className,
onRemove,
}: {
file: File;
className?: string;
onRemove: () => void;
}) => {
const [isHovered, setIsHovered] = useState(false);
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{isHovered ? (
) : (
renderFileIcon(file)
)}
{file.name}
);
};
export const SendButton = ({
isLoading,
onStop,
onSendClick,
isEmpty,
showStopLabel = false, // Show a stop label and spinner instead of just the stop icon when loading
}: {
isLoading: boolean;
onStop: () => void;
onSendClick: () => void;
isEmpty: boolean;
showStopLabel?: boolean;
}) => {
const loadingContent = showStopLabel ? (
Stop
) : (
);
return (
);
};
export const AddContextButton = ({
handleAddContext,
isLoading,
}: {
handleAddContext: () => void;
isLoading: boolean;
}) => {
return (
);
};
export const AttachFileButton = ({
fileInputRef,
isLoading,
onAddFiles,
}: {
fileInputRef: React.RefObject;
isLoading: boolean;
onAddFiles: (files: File[]) => void;
}) => {
return (
<>
) => {
if (event.target.files) {
onAddFiles([...event.target.files]);
}
}}
accept={SUPPORTED_ATTACHMENT_TYPES.join(",")}
/>
>
);
};
function renderFileIcon(file: File): React.ReactNode {
const classNames = "h-3 w-3 mt-0.5";
if (file.type.startsWith("image/")) {
return ;
} else if (file.type.startsWith("text/")) {
return ;
}
return ;
}
export const SourceChip = ({
icon,
title,
subtitle,
href,
}: {
icon: React.ReactNode;
title: string;
subtitle?: string;
href?: string;
}) => {
const content = (
<>
{icon}
{title}
{subtitle && ({subtitle})}
>
);
const className =
"inline-flex max-w-full items-center gap-1.5 rounded-md border bg-muted/50 px-2 py-1 my-1 text-xs text-muted-foreground";
// Only treat absolute http(s) URLs as safe to render as a clickable link.
// Source URLs come from model output (e.g. citations) and could otherwise
// smuggle in `javascript:`/`data:` schemes.
if (href && isUrl(href)) {
return (
{content}
);
}
return (
{content}
);
};