CR: cn utility, refactor accepted files, nextImage

This commit is contained in:
starmorph
2025-05-19 20:52:38 -07:00
parent 1fbef481fd
commit 25b247354f
4 changed files with 37 additions and 28 deletions

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import type { Base64ContentBlock } from "@langchain/core/messages"; import type { Base64ContentBlock } from "@langchain/core/messages";
import { MultimodalPreview } from "../ui/MultimodalPreview"; import { MultimodalPreview } from "../ui/MultimodalPreview";
import { cn } from "@/lib/utils";
interface ContentBlocksPreviewProps { interface ContentBlocksPreviewProps {
blocks: Base64ContentBlock[]; blocks: Base64ContentBlock[];
@@ -9,15 +10,19 @@ interface ContentBlocksPreviewProps {
className?: string; className?: string;
} }
/**
* Renders a preview of content blocks with optional remove functionality.
* Uses cn utility for robust class merging.
*/
export const ContentBlocksPreview: React.FC<ContentBlocksPreviewProps> = ({ export const ContentBlocksPreview: React.FC<ContentBlocksPreviewProps> = ({
blocks, blocks,
onRemove, onRemove,
size = "md", size = "md",
className = "", className,
}) => { }) => {
if (!blocks.length) return null; if (!blocks.length) return null;
return ( return (
<div className={`flex flex-wrap gap-2 p-3.5 pb-0 ${className}`}> <div className={cn("flex flex-wrap gap-2 p-3.5 pb-0", className)}>
{blocks.map((block, idx) => ( {blocks.map((block, idx) => (
<MultimodalPreview <MultimodalPreview
key={idx} key={idx}

View File

@@ -138,9 +138,7 @@ export function HumanMessage({
</div> </div>
)} )}
{/* Render text if present, otherwise fallback to file/image name */} {/* Render text if present, otherwise fallback to file/image name */}
{contentString && {contentString ? (
contentString !== "Other" &&
contentString !== "Multimodal message" ? (
<p className="bg-muted ml-auto w-fit rounded-3xl px-4 py-2 text-right whitespace-pre-wrap"> <p className="bg-muted ml-auto w-fit rounded-3xl px-4 py-2 text-right whitespace-pre-wrap">
{contentString} {contentString}
</p> </p>

View File

@@ -1,7 +1,8 @@
import React from "react"; import React from "react";
import { File, Image as ImageIcon, X as XIcon } from "lucide-react"; import { File, Image as ImageIcon, X as XIcon } from "lucide-react";
import type { Base64ContentBlock } from "@langchain/core/messages"; import type { Base64ContentBlock } from "@langchain/core/messages";
import { cn } from "@/lib/utils";
import Image from "next/image";
export interface MultimodalPreviewProps { export interface MultimodalPreviewProps {
block: Base64ContentBlock; block: Base64ContentBlock;
removable?: boolean; removable?: boolean;
@@ -14,7 +15,7 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
block, block,
removable = false, removable = false,
onRemove, onRemove,
className = "", className,
size = "md", size = "md",
}) => { }) => {
// Sizing // Sizing
@@ -38,13 +39,13 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
if (size === "sm") imgClass = "rounded-md object-cover h-10 w-10 text-base"; if (size === "sm") imgClass = "rounded-md object-cover h-10 w-10 text-base";
if (size === "lg") imgClass = "rounded-md object-cover h-24 w-24 text-xl"; if (size === "lg") imgClass = "rounded-md object-cover h-24 w-24 text-xl";
return ( return (
<div <div className={cn("relative inline-block", className)}>
className={`relative inline-block${className ? ` ${className}` : ""}`} <Image
>
<img
src={url} src={url}
alt={String(block.metadata?.name || "uploaded image")} alt={String(block.metadata?.name || "uploaded image")}
className={imgClass} className={imgClass}
width={size === "sm" ? 16 : size === "md" ? 32 : 48}
height={size === "sm" ? 16 : size === "md" ? 32 : 48}
/> />
{removable && ( {removable && (
<button <button
@@ -68,20 +69,24 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
) { ) {
const filename = const filename =
block.metadata?.filename || block.metadata?.name || "PDF file"; block.metadata?.filename || block.metadata?.name || "PDF file";
const fileClass = `relative flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2${className ? ` ${className}` : ""}`;
return ( return (
<div className={fileClass}> <div
className={cn(
"relative flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2",
className
)}
>
<File <File
className={ className={cn(
"flex-shrink-0 text-teal-700 " + "flex-shrink-0 text-teal-700",
(size === "sm" ? "h-5 w-5" : "h-7 w-7") size === "sm" ? "h-5 w-5" : "h-7 w-7"
} )}
/> />
<span <span
className={ className={cn(
"truncate text-sm text-gray-800 " + "truncate text-sm text-gray-800",
(size === "sm" ? "max-w-[80px]" : "max-w-[160px]") size === "sm" ? "max-w-[80px]" : "max-w-[160px]"
} )}
> >
{String(filename)} {String(filename)}
</span> </span>
@@ -100,9 +105,13 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
} }
// Fallback for unknown types // Fallback for unknown types
const fallbackClass = `flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2 text-gray-500${className ? ` ${className}` : ""}`;
return ( return (
<div className={fallbackClass}> <div
className={cn(
"flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2 text-gray-500",
className
)}
>
<File className="h-5 w-5 flex-shrink-0" /> <File className="h-5 w-5 flex-shrink-0" />
<span className="truncate text-xs">Unsupported file type</span> <span className="truncate text-xs">Unsupported file type</span>
{removable && ( {removable && (

View File

@@ -3,14 +3,11 @@ import { toast } from "sonner";
import type { Base64ContentBlock } from "@langchain/core/messages"; import type { Base64ContentBlock } from "@langchain/core/messages";
import { fileToContentBlock } from "@/lib/multimodal-utils"; import { fileToContentBlock } from "@/lib/multimodal-utils";
export const SUPPORTED_IMAGE_TYPES = [ export const SUPPORTED_FILE_TYPES = [
"image/jpeg", "image/jpeg",
"image/png", "image/png",
"image/gif", "image/gif",
"image/webp", "image/webp",
];
export const SUPPORTED_FILE_TYPES = [
...SUPPORTED_IMAGE_TYPES,
"application/pdf", "application/pdf",
]; ];
@@ -26,7 +23,7 @@ export function useFileUpload({
const dropRef = useRef<HTMLDivElement>(null); const dropRef = useRef<HTMLDivElement>(null);
const isDuplicate = (file: File, blocks: Base64ContentBlock[]) => { const isDuplicate = (file: File, blocks: Base64ContentBlock[]) => {
if (SUPPORTED_IMAGE_TYPES.includes(file.type)) { if (SUPPORTED_FILE_TYPES.includes(file.type)) {
return blocks.some( return blocks.some(
(b) => (b) =>
b.type === "image" && b.type === "image" &&