show image thumbnails and pdf filenames in chat, allow for fileuploads with no text message

This commit is contained in:
starmorph
2025-05-19 13:06:28 -07:00
parent 217fd43eac
commit f1b6aab645
3 changed files with 74 additions and 29 deletions

View File

@@ -37,10 +37,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import {
fileToImageBlock,
fileToPDFBlock,
} from "@/lib/multimodal-utils";
import { fileToImageBlock, fileToPDFBlock } from "@/lib/multimodal-utils";
import type { Base64ContentBlock } from "@langchain/core/messages";
function StickyToBottomContent(props: {
@@ -176,18 +173,17 @@ export function Thread() {
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
if ((input.trim().length === 0 && imageUrlList.length === 0 && pdfUrlList.length === 0) || isLoading) return;
setFirstTokenReceived(false);
// TODO: check configurable object for modelname camelcase or snakecase else do openai format
const isOpenAI = true;
const newHumanMessage: Message = {
id: uuidv4(),
type: "human",
content: [
{ type: "text", text: input },
...(input.trim().length > 0 ? [{ type: "text", text: input }] : []),
...pdfUrlList,
...imageUrlList,
] as Message["content"],
@@ -220,10 +216,15 @@ export function Thread() {
const files = e.target.files;
if (!files) return;
const fileArray = Array.from(files);
const imageFiles = fileArray.filter((file) => file.type.startsWith("image"));
const pdfFiles = fileArray.filter((file) => file.type === "application/pdf");
const imageFiles = fileArray.filter((file) =>
file.type.startsWith("image"),
);
const pdfFiles = fileArray.filter(
(file) => file.type === "application/pdf",
);
const invalidFiles = fileArray.filter(
(file) => !file.type.startsWith("image/") && file.type !== "application/pdf",
(file) =>
!file.type.startsWith("image/") && file.type !== "application/pdf",
);
if (invalidFiles.length > 0) {
@@ -638,7 +639,7 @@ export function Thread() {
<Button
type="submit"
className="shadow-md transition-all"
disabled={isLoading || !input.trim()}
disabled={isLoading || (!input.trim() && imageUrlList.length === 0 && pdfUrlList.length === 0)}
>
Send
</Button>

View File

@@ -86,20 +86,69 @@ export function HumanMessage({
/>
) : (
<div className="flex flex-col gap-2">
{contentImageUrls.length > 0 && (
<div className="flex flex-wrap justify-end gap-2">
{contentImageUrls.map((imageUrl) => (
{/* Render images and files if no text */}
{Array.isArray(message.content) && message.content.length > 0 && (
<div className="flex flex-col gap-2 items-end">
{message.content.map((block, idx) => {
// Type guard for image block
const isImageBlock =
typeof block === "object" &&
block !== null &&
"type" in block &&
(block as any).type === "image" &&
"source_type" in block &&
(block as any).source_type === "base64" &&
"mime_type" in block &&
"data" in block;
if (isImageBlock) {
const imgBlock = block as {
type: string;
source_type: string;
mime_type: string;
data: string;
metadata?: { name?: string };
};
const url = `data:${imgBlock.mime_type};base64,${imgBlock.data}`;
return (
<img
src={imageUrl}
alt="uploaded image"
key={idx}
src={url}
alt={imgBlock.metadata?.name || "uploaded image"}
className="bg-muted h-16 w-16 rounded-md object-cover"
/>
))}
);
}
// Type guard for file block (PDF)
const isPdfBlock =
typeof block === "object" &&
block !== null &&
"type" in block &&
(block as any).type === "file" &&
"mime_type" in block &&
(block as any).mime_type === "application/pdf";
if (isPdfBlock) {
const pdfBlock = block as {
metadata?: { filename?: string; name?: string };
};
return (
<div
key={idx}
className="bg-muted ml-auto w-fit rounded-3xl px-4 py-2 text-right whitespace-pre-wrap"
>
{pdfBlock.metadata?.filename || pdfBlock.metadata?.name || "PDF file"}
</div>
);
}
return null;
})}
</div>
)}
{/* Render text if present, otherwise fallback to file/image name */}
{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">
{contentString}
</p>
) : null}
</div>
)}

View File

@@ -5,12 +5,7 @@ import { convertToOpenAIImageBlock } from "@langchain/core/messages";
export async function fileToImageBlock(
file: File,
): Promise<Base64ContentBlock> {
const supportedTypes = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
];
const supportedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (!supportedTypes.includes(file.type)) {
throw new Error(
`Unsupported image type: ${file.type}. Supported types are: ${supportedTypes.join(", ")}`,