CR: cn utility, refactor accepted files, nextImage
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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" &&
|
||||||
|
|||||||
Reference in New Issue
Block a user