Skip to content

Commit 8534f4a

Browse files
committed
CR: ContentBlock abstraction
1 parent d358222 commit 8534f4a

File tree

4 files changed

+69
-85
lines changed

4 files changed

+69
-85
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from "react";
2+
import type { Base64ContentBlock } from "@langchain/core/messages";
3+
import { MultimodalPreview } from "../ui/MultimodalPreview";
4+
5+
interface ContentBlocksPreviewProps {
6+
blocks: Base64ContentBlock[];
7+
onRemove: (idx: number) => void;
8+
size?: "sm" | "md" | "lg";
9+
className?: string;
10+
}
11+
12+
export const ContentBlocksPreview: React.FC<ContentBlocksPreviewProps> = ({
13+
blocks,
14+
onRemove,
15+
size = "md",
16+
className = "",
17+
}) => {
18+
if (!blocks.length) return null;
19+
return (
20+
<div className={`flex flex-wrap gap-2 p-3.5 pb-0 ${className}`}>
21+
{blocks.map((block, idx) => (
22+
<MultimodalPreview
23+
key={idx}
24+
block={block}
25+
removable
26+
onRemove={() => onRemove(idx)}
27+
size={size}
28+
/>
29+
))}
30+
</div>
31+
);
32+
};

src/components/thread/index.tsx

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,8 @@ import {
3737
TooltipProvider,
3838
TooltipTrigger,
3939
} from "../ui/tooltip";
40-
import { fileToImageBlock, fileToPDFBlock } from "@/lib/multimodal-utils";
41-
import type { Base64ContentBlock } from "@langchain/core/messages";
42-
import { MultimodalPreview } from "../ui/MultimodalPreview";
4340
import { useFileUpload } from "@/hooks/use-file-upload";
41+
import { ContentBlocksPreview } from "./ContentBlocksPreview";
4442

4543
function StickyToBottomContent(props: {
4644
content: ReactNode;
@@ -393,44 +391,10 @@ export function Thread() {
393391
onSubmit={handleSubmit}
394392
className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
395393
>
396-
{contentBlocks.filter((b) => b.type === "image").length >
397-
0 && (
398-
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
399-
{contentBlocks
400-
.filter((b) => b.type === "image")
401-
.map((imageBlock, idx) => (
402-
<MultimodalPreview
403-
key={idx}
404-
block={imageBlock}
405-
removable
406-
onRemove={() => removeBlock(idx)}
407-
size="md"
408-
/>
409-
))}
410-
</div>
411-
)}
412-
{contentBlocks.filter(
413-
(b) =>
414-
b.type === "file" && b.mime_type === "application/pdf",
415-
).length > 0 && (
416-
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
417-
{contentBlocks
418-
.filter(
419-
(b) =>
420-
b.type === "file" &&
421-
b.mime_type === "application/pdf",
422-
)
423-
.map((pdfBlock, idx) => (
424-
<MultimodalPreview
425-
key={idx}
426-
block={pdfBlock}
427-
removable
428-
onRemove={() => removeBlock(idx)}
429-
size="md"
430-
/>
431-
))}
432-
</div>
433-
)}
394+
<ContentBlocksPreview
395+
blocks={contentBlocks}
396+
onRemove={removeBlock}
397+
/>
434398
<textarea
435399
value={input}
436400
onChange={(e) => setInput(e.target.value)}

src/hooks/use-file-upload.tsx

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState, useRef, useEffect, ChangeEvent } from "react";
22
import { toast } from "sonner";
33
import type { Base64ContentBlock } from "@langchain/core/messages";
4-
import { fileToImageBlock, fileToPDFBlock } from "@/lib/multimodal-utils";
4+
import { fileToContentBlock } from "@/lib/multimodal-utils";
55

66
export const SUPPORTED_IMAGE_TYPES = [
77
"image/jpeg",
@@ -73,20 +73,10 @@ export function useFileUpload({
7373
);
7474
}
7575

76-
const imageFiles = uniqueFiles.filter((file) =>
77-
SUPPORTED_IMAGE_TYPES.includes(file.type),
78-
);
79-
const pdfFiles = uniqueFiles.filter(
80-
(file) => file.type === "application/pdf",
81-
);
82-
83-
const imageBlocks = imageFiles.length
84-
? await Promise.all(imageFiles.map(fileToImageBlock))
85-
: [];
86-
const pdfBlocks = pdfFiles.length
87-
? await Promise.all(pdfFiles.map(fileToPDFBlock))
76+
const newBlocks = uniqueFiles.length
77+
? await Promise.all(uniqueFiles.map(fileToContentBlock))
8878
: [];
89-
setContentBlocks((prev) => [...prev, ...imageBlocks, ...pdfBlocks]);
79+
setContentBlocks((prev) => [...prev, ...newBlocks]);
9080
e.target.value = "";
9181
};
9282

@@ -130,20 +120,10 @@ export function useFileUpload({
130120
);
131121
}
132122

133-
const imageFiles = uniqueFiles.filter((file) =>
134-
SUPPORTED_IMAGE_TYPES.includes(file.type),
135-
);
136-
const pdfFiles = uniqueFiles.filter(
137-
(file) => file.type === "application/pdf",
138-
);
139-
140-
const imageBlocks: Base64ContentBlock[] = imageFiles.length
141-
? await Promise.all(imageFiles.map(fileToImageBlock))
142-
: [];
143-
const pdfBlocks: Base64ContentBlock[] = pdfFiles.length
144-
? await Promise.all(pdfFiles.map(fileToPDFBlock))
123+
const newBlocks = uniqueFiles.length
124+
? await Promise.all(uniqueFiles.map(fileToContentBlock))
145125
: [];
146-
setContentBlocks((prev) => [...prev, ...imageBlocks, ...pdfBlocks]);
126+
setContentBlocks((prev) => [...prev, ...newBlocks]);
147127
};
148128

149129
const handleDragEnter = (e: DragEvent) => {

src/lib/multimodal-utils.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
11
import type { Base64ContentBlock } from "@langchain/core/messages";
22
import { toast } from "sonner";
33

4-
// Returns a Promise of a typed multimodal block for images
5-
export async function fileToImageBlock(
4+
// Returns a Promise of a typed multimodal block for images or PDFs
5+
export async function fileToContentBlock(
66
file: File,
77
): Promise<Base64ContentBlock> {
8-
const supportedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
9-
if (!supportedTypes.includes(file.type)) {
8+
const supportedImageTypes = [
9+
"image/jpeg",
10+
"image/png",
11+
"image/gif",
12+
"image/webp",
13+
];
14+
const supportedFileTypes = [...supportedImageTypes, "application/pdf"];
15+
16+
if (!supportedFileTypes.includes(file.type)) {
1017
toast.error(
11-
`Unsupported image type: ${file.type}. Supported types are: ${supportedTypes.join(", ")}`,
18+
`Unsupported file type: ${file.type}. Supported types are: ${supportedFileTypes.join(", ")}`,
1219
);
13-
return Promise.reject(new Error(`Unsupported image type: ${file.type}`));
20+
return Promise.reject(new Error(`Unsupported file type: ${file.type}`));
1421
}
15-
const data = await fileToBase64(file);
16-
return {
17-
type: "image",
18-
source_type: "base64",
19-
mime_type: file.type,
20-
data,
21-
metadata: { name: file.name },
22-
};
23-
}
2422

25-
// Returns a Promise of a typed multimodal block for PDFs
26-
export async function fileToPDFBlock(file: File): Promise<Base64ContentBlock> {
2723
const data = await fileToBase64(file);
24+
25+
if (supportedImageTypes.includes(file.type)) {
26+
return {
27+
type: "image",
28+
source_type: "base64",
29+
mime_type: file.type,
30+
data,
31+
metadata: { name: file.name },
32+
};
33+
}
34+
35+
// PDF
2836
return {
2937
type: "file",
3038
source_type: "base64",

0 commit comments

Comments
 (0)