CR: ContentBlock abstraction

This commit is contained in:
starmorph
2025-05-19 16:35:11 -07:00
parent d358222ea0
commit 8534f4adbe
4 changed files with 72 additions and 88 deletions

View File

@@ -0,0 +1,32 @@
import React from "react";
import type { Base64ContentBlock } from "@langchain/core/messages";
import { MultimodalPreview } from "../ui/MultimodalPreview";
interface ContentBlocksPreviewProps {
blocks: Base64ContentBlock[];
onRemove: (idx: number) => void;
size?: "sm" | "md" | "lg";
className?: string;
}
export const ContentBlocksPreview: React.FC<ContentBlocksPreviewProps> = ({
blocks,
onRemove,
size = "md",
className = "",
}) => {
if (!blocks.length) return null;
return (
<div className={`flex flex-wrap gap-2 p-3.5 pb-0 ${className}`}>
{blocks.map((block, idx) => (
<MultimodalPreview
key={idx}
block={block}
removable
onRemove={() => onRemove(idx)}
size={size}
/>
))}
</div>
);
};

View File

@@ -37,10 +37,8 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "../ui/tooltip"; } from "../ui/tooltip";
import { fileToImageBlock, fileToPDFBlock } from "@/lib/multimodal-utils";
import type { Base64ContentBlock } from "@langchain/core/messages";
import { MultimodalPreview } from "../ui/MultimodalPreview";
import { useFileUpload } from "@/hooks/use-file-upload"; import { useFileUpload } from "@/hooks/use-file-upload";
import { ContentBlocksPreview } from "./ContentBlocksPreview";
function StickyToBottomContent(props: { function StickyToBottomContent(props: {
content: ReactNode; content: ReactNode;
@@ -393,44 +391,10 @@ export function Thread() {
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2" className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
> >
{contentBlocks.filter((b) => b.type === "image").length > <ContentBlocksPreview
0 && ( blocks={contentBlocks}
<div className="flex flex-wrap gap-2 p-3.5 pb-0"> onRemove={removeBlock}
{contentBlocks
.filter((b) => b.type === "image")
.map((imageBlock, idx) => (
<MultimodalPreview
key={idx}
block={imageBlock}
removable
onRemove={() => removeBlock(idx)}
size="md"
/> />
))}
</div>
)}
{contentBlocks.filter(
(b) =>
b.type === "file" && b.mime_type === "application/pdf",
).length > 0 && (
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
{contentBlocks
.filter(
(b) =>
b.type === "file" &&
b.mime_type === "application/pdf",
)
.map((pdfBlock, idx) => (
<MultimodalPreview
key={idx}
block={pdfBlock}
removable
onRemove={() => removeBlock(idx)}
size="md"
/>
))}
</div>
)}
<textarea <textarea
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}

View File

@@ -1,7 +1,7 @@
import { useState, useRef, useEffect, ChangeEvent } from "react"; import { useState, useRef, useEffect, ChangeEvent } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { Base64ContentBlock } from "@langchain/core/messages"; import type { Base64ContentBlock } from "@langchain/core/messages";
import { fileToImageBlock, fileToPDFBlock } from "@/lib/multimodal-utils"; import { fileToContentBlock } from "@/lib/multimodal-utils";
export const SUPPORTED_IMAGE_TYPES = [ export const SUPPORTED_IMAGE_TYPES = [
"image/jpeg", "image/jpeg",
@@ -73,20 +73,10 @@ export function useFileUpload({
); );
} }
const imageFiles = uniqueFiles.filter((file) => const newBlocks = uniqueFiles.length
SUPPORTED_IMAGE_TYPES.includes(file.type), ? await Promise.all(uniqueFiles.map(fileToContentBlock))
);
const pdfFiles = uniqueFiles.filter(
(file) => file.type === "application/pdf",
);
const imageBlocks = imageFiles.length
? await Promise.all(imageFiles.map(fileToImageBlock))
: []; : [];
const pdfBlocks = pdfFiles.length setContentBlocks((prev) => [...prev, ...newBlocks]);
? await Promise.all(pdfFiles.map(fileToPDFBlock))
: [];
setContentBlocks((prev) => [...prev, ...imageBlocks, ...pdfBlocks]);
e.target.value = ""; e.target.value = "";
}; };
@@ -130,20 +120,10 @@ export function useFileUpload({
); );
} }
const imageFiles = uniqueFiles.filter((file) => const newBlocks = uniqueFiles.length
SUPPORTED_IMAGE_TYPES.includes(file.type), ? await Promise.all(uniqueFiles.map(fileToContentBlock))
);
const pdfFiles = uniqueFiles.filter(
(file) => file.type === "application/pdf",
);
const imageBlocks: Base64ContentBlock[] = imageFiles.length
? await Promise.all(imageFiles.map(fileToImageBlock))
: []; : [];
const pdfBlocks: Base64ContentBlock[] = pdfFiles.length setContentBlocks((prev) => [...prev, ...newBlocks]);
? await Promise.all(pdfFiles.map(fileToPDFBlock))
: [];
setContentBlocks((prev) => [...prev, ...imageBlocks, ...pdfBlocks]);
}; };
const handleDragEnter = (e: DragEvent) => { const handleDragEnter = (e: DragEvent) => {

View File

@@ -1,18 +1,28 @@
import type { Base64ContentBlock } from "@langchain/core/messages"; import type { Base64ContentBlock } from "@langchain/core/messages";
import { toast } from "sonner"; import { toast } from "sonner";
// Returns a Promise of a typed multimodal block for images // Returns a Promise of a typed multimodal block for images or PDFs
export async function fileToImageBlock( export async function fileToContentBlock(
file: File, file: File,
): Promise<Base64ContentBlock> { ): Promise<Base64ContentBlock> {
const supportedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"]; const supportedImageTypes = [
if (!supportedTypes.includes(file.type)) { "image/jpeg",
"image/png",
"image/gif",
"image/webp",
];
const supportedFileTypes = [...supportedImageTypes, "application/pdf"];
if (!supportedFileTypes.includes(file.type)) {
toast.error( toast.error(
`Unsupported image type: ${file.type}. Supported types are: ${supportedTypes.join(", ")}`, `Unsupported file type: ${file.type}. Supported types are: ${supportedFileTypes.join(", ")}`,
); );
return Promise.reject(new Error(`Unsupported image type: ${file.type}`)); return Promise.reject(new Error(`Unsupported file type: ${file.type}`));
} }
const data = await fileToBase64(file); const data = await fileToBase64(file);
if (supportedImageTypes.includes(file.type)) {
return { return {
type: "image", type: "image",
source_type: "base64", source_type: "base64",
@@ -20,11 +30,9 @@ export async function fileToImageBlock(
data, data,
metadata: { name: file.name }, metadata: { name: file.name },
}; };
} }
// Returns a Promise of a typed multimodal block for PDFs // PDF
export async function fileToPDFBlock(file: File): Promise<Base64ContentBlock> {
const data = await fileToBase64(file);
return { return {
type: "file", type: "file",
source_type: "base64", source_type: "base64",