CR: ContentBlock abstraction
This commit is contained in:
32
src/components/thread/ContentBlocksPreview.tsx
Normal file
32
src/components/thread/ContentBlocksPreview.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -22,9 +32,7 @@ export async function fileToImageBlock(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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",
|
||||||
|
|||||||
Reference in New Issue
Block a user