import type { Message } from "@langchain/langgraph-sdk"; import { FileIcon } from "lucide-react"; import { useParams } from "next/navigation"; import { memo, useMemo } from "react"; import rehypeKatex from "rehype-katex"; import { Message as AIElementMessage, MessageContent as AIElementMessageContent, MessageResponse as AIElementMessageResponse, MessageToolbar, } from "@/components/ai-elements/message"; import { Badge } from "@/components/ui/badge"; import { resolveArtifactURL } from "@/core/artifacts/utils"; import { removeAllCitations } from "@/core/citations"; import { extractContentFromMessage, extractReasoningContentFromMessage, parseUploadedFiles, type UploadedFile, } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { humanMessagePlugins } from "@/core/streamdown"; import { cn } from "@/lib/utils"; import { CopyButton } from "../copy-button"; import { SafeCitationContent } from "./safe-citation-content"; export function MessageListItem({ className, message, isLoading, }: { className?: string; message: Message; isLoading?: boolean; }) { const isHuman = message.type === "human"; return ( ); } /** * Custom image component that handles artifact URLs */ function MessageImage({ src, alt, threadId, maxWidth = "90%", ...props }: React.ImgHTMLAttributes & { threadId: string; maxWidth?: string; }) { if (!src) return null; const imgClassName = cn("overflow-hidden rounded-lg", `max-w-[${maxWidth}]`); if (typeof src !== "string") { return ; } const url = src.startsWith("/mnt/") ? resolveArtifactURL(src, threadId) : src; return ( ); } function MessageContent_({ className, message, isLoading = false, }: { className?: string; message: Message; isLoading?: boolean; }) { const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const isHuman = message.type === "human"; const { thread_id } = useParams<{ thread_id: string }>(); const rawContent = extractContentFromMessage(message); const reasoningContent = extractReasoningContentFromMessage(message); const { contentToParse, uploadedFiles } = useMemo(() => { if (!isLoading && reasoningContent && !rawContent) { return { contentToParse: reasoningContent, uploadedFiles: [] as UploadedFile[] }; } if (isHuman && rawContent) { const { files, cleanContent: contentWithoutFiles } = parseUploadedFiles(rawContent); return { contentToParse: contentWithoutFiles, uploadedFiles: files }; } return { contentToParse: rawContent ?? "", uploadedFiles: [] as UploadedFile[], }; }, [isLoading, rawContent, reasoningContent, isHuman]); const filesList = uploadedFiles.length > 0 && thread_id ? ( ) : null; if (isHuman) { const messageResponse = contentToParse ? ( {contentToParse} ) : null; return ( {filesList} {messageResponse && ( {messageResponse} )} ); } return ( {filesList} ( )} /> ); } /** * Get file extension and check helpers */ const getFileExt = (filename: string) => filename.split(".").pop()?.toLowerCase() ?? ""; const FILE_TYPE_MAP: Record = { json: "JSON", csv: "CSV", txt: "TXT", md: "Markdown", py: "Python", js: "JavaScript", ts: "TypeScript", tsx: "TSX", jsx: "JSX", html: "HTML", css: "CSS", xml: "XML", yaml: "YAML", yml: "YAML", pdf: "PDF", png: "PNG", jpg: "JPG", jpeg: "JPEG", gif: "GIF", svg: "SVG", zip: "ZIP", tar: "TAR", gz: "GZ", }; const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"]; function getFileTypeLabel(filename: string): string { const ext = getFileExt(filename); return FILE_TYPE_MAP[ext] ?? (ext.toUpperCase() || "FILE"); } function isImageFile(filename: string): boolean { return IMAGE_EXTENSIONS.includes(getFileExt(filename)); } /** * Uploaded files list component */ function UploadedFilesList({ files, threadId }: { files: UploadedFile[]; threadId: string }) { if (files.length === 0) return null; return ( {files.map((file, index) => ( ))} ); } /** * Single uploaded file card component */ function UploadedFileCard({ file, threadId }: { file: UploadedFile; threadId: string }) { if (!threadId) return null; const isImage = isImageFile(file.filename); const fileUrl = resolveArtifactURL(file.path, threadId); if (isImage) { return ( ); } return ( {file.filename} {getFileTypeLabel(file.filename)} {file.size} ); } const MessageContent = memo(MessageContent_);