Skip to content

Commit 52379f5

Browse files
committed
CR fixes
1 parent 087587d commit 52379f5

File tree

4 files changed

+161
-95
lines changed

4 files changed

+161
-95
lines changed

src/components/thread/index.tsx

Lines changed: 116 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ export function Thread() {
117117
parseAsBoolean.withDefault(false),
118118
);
119119
const [input, setInput] = useState("");
120-
const [imageUrlList, setImageUrlList] = useState<Base64ContentBlock[]>([]);
121-
const [pdfUrlList, setPdfUrlList] = useState<Base64ContentBlock[]>([]);
120+
const [contentBlocks, setContentBlocks] = useState<Base64ContentBlock[]>([]);
122121
const [firstTokenReceived, setFirstTokenReceived] = useState(false);
123122
const isLargeScreen = useMediaQuery("(min-width: 1024px)");
124123

@@ -174,12 +173,7 @@ export function Thread() {
174173

175174
const handleSubmit = (e: FormEvent) => {
176175
e.preventDefault();
177-
if (
178-
(input.trim().length === 0 &&
179-
imageUrlList.length === 0 &&
180-
pdfUrlList.length === 0) ||
181-
isLoading
182-
)
176+
if ((input.trim().length === 0 && contentBlocks.length === 0) || isLoading)
183177
return;
184178
setFirstTokenReceived(false);
185179

@@ -188,12 +182,10 @@ export function Thread() {
188182
type: "human",
189183
content: [
190184
...(input.trim().length > 0 ? [{ type: "text", text: input }] : []),
191-
...pdfUrlList,
192-
...imageUrlList,
185+
...contentBlocks,
193186
] as Message["content"],
194187
};
195188

196-
197189
const toolMessages = ensureToolCallsHaveResponses(stream.messages);
198190
stream.submit(
199191
{ messages: [...toolMessages, newHumanMessage] },
@@ -211,19 +203,33 @@ export function Thread() {
211203
);
212204

213205
setInput("");
214-
setImageUrlList([]);
215-
setPdfUrlList([]);
206+
setContentBlocks([]);
216207
};
217208

218-
const SUPPORTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
209+
const SUPPORTED_IMAGE_TYPES = [
210+
"image/jpeg",
211+
"image/png",
212+
"image/gif",
213+
"image/webp",
214+
];
219215
const SUPPORTED_FILE_TYPES = [...SUPPORTED_IMAGE_TYPES, "application/pdf"];
220216

221-
const isDuplicate = (file: File, images: Base64ContentBlock[], pdfs: Base64ContentBlock[]) => {
217+
const isDuplicate = (file: File, blocks: Base64ContentBlock[]) => {
222218
if (SUPPORTED_IMAGE_TYPES.includes(file.type)) {
223-
return images.some(img => img.metadata?.name === file.name && img.mime_type === file.type);
219+
return blocks.some(
220+
(b) =>
221+
b.type === "image" &&
222+
b.metadata?.name === file.name &&
223+
b.mime_type === file.type,
224+
);
224225
}
225226
if (file.type === "application/pdf") {
226-
return pdfs.some(pdf => pdf.metadata?.filename === file.name);
227+
return blocks.some(
228+
(b) =>
229+
b.type === "file" &&
230+
b.mime_type === "application/pdf" &&
231+
b.metadata?.filename === file.name,
232+
);
227233
}
228234
return false;
229235
};
@@ -232,10 +238,18 @@ export function Thread() {
232238
const files = e.target.files;
233239
if (!files) return;
234240
const fileArray = Array.from(files);
235-
const validFiles = fileArray.filter((file) => SUPPORTED_FILE_TYPES.includes(file.type));
236-
const invalidFiles = fileArray.filter((file) => !SUPPORTED_FILE_TYPES.includes(file.type));
237-
const duplicateFiles = validFiles.filter((file) => isDuplicate(file, imageUrlList, pdfUrlList));
238-
const uniqueFiles = validFiles.filter((file) => !isDuplicate(file, imageUrlList, pdfUrlList));
241+
const validFiles = fileArray.filter((file) =>
242+
SUPPORTED_FILE_TYPES.includes(file.type),
243+
);
244+
const invalidFiles = fileArray.filter(
245+
(file) => !SUPPORTED_FILE_TYPES.includes(file.type),
246+
);
247+
const duplicateFiles = validFiles.filter((file) =>
248+
isDuplicate(file, contentBlocks),
249+
);
250+
const uniqueFiles = validFiles.filter(
251+
(file) => !isDuplicate(file, contentBlocks),
252+
);
239253

240254
if (invalidFiles.length > 0) {
241255
toast.error(
@@ -244,22 +258,24 @@ export function Thread() {
244258
}
245259
if (duplicateFiles.length > 0) {
246260
toast.error(
247-
`Duplicate file(s) detected: ${duplicateFiles.map(f => f.name).join(", ")}. Each file can only be uploaded once per message.`,
261+
`Duplicate file(s) detected: ${duplicateFiles.map((f) => f.name).join(", ")}. Each file can only be uploaded once per message.`,
248262
);
249263
}
250264

251-
const imageFiles = uniqueFiles.filter((file) => SUPPORTED_IMAGE_TYPES.includes(file.type));
252-
const pdfFiles = uniqueFiles.filter((file) => file.type === "application/pdf");
253-
254-
if (imageFiles.length) {
255-
const imageBlocks = await Promise.all(imageFiles.map(fileToImageBlock));
256-
setImageUrlList((prev) => [...prev, ...imageBlocks]);
257-
}
265+
const imageFiles = uniqueFiles.filter((file) =>
266+
SUPPORTED_IMAGE_TYPES.includes(file.type),
267+
);
268+
const pdfFiles = uniqueFiles.filter(
269+
(file) => file.type === "application/pdf",
270+
);
258271

259-
if (pdfFiles.length) {
260-
const pdfBlocks = await Promise.all(pdfFiles.map(fileToPDFBlock));
261-
setPdfUrlList((prev) => [...prev, ...pdfBlocks]);
262-
}
272+
const imageBlocks = imageFiles.length
273+
? await Promise.all(imageFiles.map(fileToImageBlock))
274+
: [];
275+
const pdfBlocks = pdfFiles.length
276+
? await Promise.all(pdfFiles.map(fileToPDFBlock))
277+
: [];
278+
setContentBlocks((prev) => [...prev, ...imageBlocks, ...pdfBlocks]);
263279
e.target.value = "";
264280
};
265281

@@ -295,10 +311,18 @@ export function Thread() {
295311
if (!e.dataTransfer) return;
296312

297313
const files = Array.from(e.dataTransfer.files);
298-
const validFiles = files.filter((file) => SUPPORTED_FILE_TYPES.includes(file.type));
299-
const invalidFiles = files.filter((file) => !SUPPORTED_FILE_TYPES.includes(file.type));
300-
const duplicateFiles = validFiles.filter((file) => isDuplicate(file, imageUrlList, pdfUrlList));
301-
const uniqueFiles = validFiles.filter((file) => !isDuplicate(file, imageUrlList, pdfUrlList));
314+
const validFiles = files.filter((file) =>
315+
SUPPORTED_FILE_TYPES.includes(file.type),
316+
);
317+
const invalidFiles = files.filter(
318+
(file) => !SUPPORTED_FILE_TYPES.includes(file.type),
319+
);
320+
const duplicateFiles = validFiles.filter((file) =>
321+
isDuplicate(file, contentBlocks),
322+
);
323+
const uniqueFiles = validFiles.filter(
324+
(file) => !isDuplicate(file, contentBlocks),
325+
);
302326

303327
if (invalidFiles.length > 0) {
304328
toast.error(
@@ -307,26 +331,24 @@ export function Thread() {
307331
}
308332
if (duplicateFiles.length > 0) {
309333
toast.error(
310-
`Duplicate file(s) detected: ${duplicateFiles.map(f => f.name).join(", ")}. Each file can only be uploaded once per message.`,
334+
`Duplicate file(s) detected: ${duplicateFiles.map((f) => f.name).join(", ")}. Each file can only be uploaded once per message.`,
311335
);
312336
}
313337

314-
const imageFiles = uniqueFiles.filter((file) => SUPPORTED_IMAGE_TYPES.includes(file.type));
315-
const pdfFiles = uniqueFiles.filter((file) => file.type === "application/pdf");
316-
317-
if (imageFiles.length) {
318-
const imageBlocks: Base64ContentBlock[] = await Promise.all(
319-
imageFiles.map(fileToImageBlock),
320-
);
321-
setImageUrlList((prev) => [...prev, ...imageBlocks]);
322-
}
338+
const imageFiles = uniqueFiles.filter((file) =>
339+
SUPPORTED_IMAGE_TYPES.includes(file.type),
340+
);
341+
const pdfFiles = uniqueFiles.filter(
342+
(file) => file.type === "application/pdf",
343+
);
323344

324-
if (pdfFiles.length) {
325-
const pdfBlocks: Base64ContentBlock[] = await Promise.all(
326-
pdfFiles.map(fileToPDFBlock),
327-
);
328-
setPdfUrlList((prev) => [...prev, ...pdfBlocks]);
329-
}
345+
const imageBlocks: Base64ContentBlock[] = imageFiles.length
346+
? await Promise.all(imageFiles.map(fileToImageBlock))
347+
: [];
348+
const pdfBlocks: Base64ContentBlock[] = pdfFiles.length
349+
? await Promise.all(pdfFiles.map(fileToPDFBlock))
350+
: [];
351+
setContentBlocks((prev) => [...prev, ...imageBlocks, ...pdfBlocks]);
330352
};
331353

332354
const handleDragEnter = (e: DragEvent) => {
@@ -544,30 +566,50 @@ export function Thread() {
544566
onSubmit={handleSubmit}
545567
className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2"
546568
>
547-
{imageUrlList.length > 0 && (
569+
{contentBlocks.filter((b) => b.type === "image").length >
570+
0 && (
548571
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
549-
{imageUrlList.map((imageBlock, idx) => (
550-
<MultimodalPreview
551-
key={idx}
552-
block={imageBlock}
553-
removable
554-
onRemove={() => setImageUrlList(imageUrlList.filter((_, i) => i !== idx))}
555-
size="md"
556-
/>
557-
))}
572+
{contentBlocks
573+
.filter((b) => b.type === "image")
574+
.map((imageBlock, idx) => (
575+
<MultimodalPreview
576+
key={idx}
577+
block={imageBlock}
578+
removable
579+
onRemove={() =>
580+
setContentBlocks(
581+
contentBlocks.filter((_, i) => i !== idx),
582+
)
583+
}
584+
size="md"
585+
/>
586+
))}
558587
</div>
559588
)}
560-
{pdfUrlList.length > 0 && (
589+
{contentBlocks.filter(
590+
(b) =>
591+
b.type === "file" && b.mime_type === "application/pdf",
592+
).length > 0 && (
561593
<div className="flex flex-wrap gap-2 p-3.5 pb-0">
562-
{pdfUrlList.map((pdfBlock, idx) => (
563-
<MultimodalPreview
564-
key={idx}
565-
block={pdfBlock}
566-
removable
567-
onRemove={() => setPdfUrlList(pdfUrlList.filter((_, i) => i !== idx))}
568-
size="md"
569-
/>
570-
))}
594+
{contentBlocks
595+
.filter(
596+
(b) =>
597+
b.type === "file" &&
598+
b.mime_type === "application/pdf",
599+
)
600+
.map((pdfBlock, idx) => (
601+
<MultimodalPreview
602+
key={idx}
603+
block={pdfBlock}
604+
removable
605+
onRemove={() =>
606+
setContentBlocks(
607+
contentBlocks.filter((_, i) => i !== idx),
608+
)
609+
}
610+
size="md"
611+
/>
612+
))}
571613
</div>
572614
)}
573615
<textarea
@@ -637,9 +679,7 @@ export function Thread() {
637679
className="shadow-md transition-all"
638680
disabled={
639681
isLoading ||
640-
(!input.trim() &&
641-
imageUrlList.length === 0 &&
642-
pdfUrlList.length === 0)
682+
(!input.trim() && contentBlocks.length === 0)
643683
}
644684
>
645685
Send

src/components/thread/messages/human.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useStreamContext } from "@/providers/Stream";
22
import { Message } from "@langchain/langgraph-sdk";
33
import { useState } from "react";
4-
import {getContentString } from "../utils";
4+
import { getContentString } from "../utils";
55
import { cn } from "@/lib/utils";
66
import { Textarea } from "@/components/ui/textarea";
77
import { BranchSwitcher, CommandBar } from "./shared";
@@ -36,7 +36,8 @@ function EditableContent({
3636

3737
// Type guard for Base64ContentBlock
3838
function isBase64ContentBlock(block: unknown): block is Base64ContentBlock {
39-
if (typeof block !== "object" || block === null || !("type" in block)) return false;
39+
if (typeof block !== "object" || block === null || !("type" in block))
40+
return false;
4041
// file type (legacy)
4142
if (
4243
(block as { type: unknown }).type === "file" &&
@@ -119,14 +120,21 @@ export function HumanMessage({
119120
{/* Render images and files if no text */}
120121
{Array.isArray(message.content) && message.content.length > 0 && (
121122
<div className="flex flex-col items-end gap-2">
122-
{message.content.reduce<React.ReactNode[]>((acc, block, idx) => {
123-
if (isBase64ContentBlock(block)) {
124-
acc.push(
125-
<MultimodalPreview key={idx} block={block} size="md" />
126-
);
127-
}
128-
return acc;
129-
}, [])}
123+
{message.content.reduce<React.ReactNode[]>(
124+
(acc, block, idx) => {
125+
if (isBase64ContentBlock(block)) {
126+
acc.push(
127+
<MultimodalPreview
128+
key={idx}
129+
block={block}
130+
size="md"
131+
/>,
132+
);
133+
}
134+
return acc;
135+
},
136+
[],
137+
)}
130138
</div>
131139
)}
132140
{/* Render text if present, otherwise fallback to file/image name */}

src/components/ui/MultimodalPreview.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
2323
md: "h-16 w-16 text-lg",
2424
lg: "h-24 w-24 text-xl",
2525
};
26-
const iconSize: string = typeof sizeMap[size] === "string" ? sizeMap[size] : sizeMap["md"];
26+
const iconSize: string =
27+
typeof sizeMap[size] === "string" ? sizeMap[size] : sizeMap["md"];
2728

2829
// Image block
2930
if (
@@ -37,7 +38,9 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
3738
if (size === "sm") imgClass = "rounded-md object-cover h-10 w-10 text-base";
3839
if (size === "lg") imgClass = "rounded-md object-cover h-24 w-24 text-xl";
3940
return (
40-
<div className={`relative inline-block${className ? ` ${className}` : ''}`}>
41+
<div
42+
className={`relative inline-block${className ? ` ${className}` : ""}`}
43+
>
4144
<img
4245
src={url}
4346
alt={String(block.metadata?.name || "uploaded image")}
@@ -63,12 +66,25 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
6366
block.source_type === "base64" &&
6467
block.mime_type === "application/pdf"
6568
) {
66-
const filename = block.metadata?.filename || block.metadata?.name || "PDF file";
67-
const fileClass = `relative flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2${className ? ` ${className}` : ''}`;
69+
const filename =
70+
block.metadata?.filename || block.metadata?.name || "PDF file";
71+
const fileClass = `relative flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2${className ? ` ${className}` : ""}`;
6872
return (
6973
<div className={fileClass}>
70-
<File className={"text-teal-700 flex-shrink-0 " + (size === "sm" ? "h-5 w-5" : "h-7 w-7")} />
71-
<span className={"truncate text-sm text-gray-800 " + (size === "sm" ? "max-w-[80px]" : "max-w-[160px]")}>{String(filename)}</span>
74+
<File
75+
className={
76+
"flex-shrink-0 text-teal-700 " +
77+
(size === "sm" ? "h-5 w-5" : "h-7 w-7")
78+
}
79+
/>
80+
<span
81+
className={
82+
"truncate text-sm text-gray-800 " +
83+
(size === "sm" ? "max-w-[80px]" : "max-w-[160px]")
84+
}
85+
>
86+
{String(filename)}
87+
</span>
7288
{removable && (
7389
<button
7490
type="button"
@@ -84,7 +100,7 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
84100
}
85101

86102
// Fallback for unknown types
87-
const fallbackClass = `flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2 text-gray-500${className ? ` ${className}` : ''}`;
103+
const fallbackClass = `flex items-center gap-2 rounded-md border bg-gray-100 px-3 py-2 text-gray-500${className ? ` ${className}` : ""}`;
88104
return (
89105
<div className={fallbackClass}>
90106
<File className="h-5 w-5 flex-shrink-0" />
@@ -101,4 +117,4 @@ export const MultimodalPreview: React.FC<MultimodalPreviewProps> = ({
101117
)}
102118
</div>
103119
);
104-
};
120+
};

0 commit comments

Comments
 (0)