show image thumbnails and pdf filenames in chat, allow for fileuploads with no text message
This commit is contained in:
@@ -37,10 +37,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "../ui/tooltip";
|
} from "../ui/tooltip";
|
||||||
import {
|
import { fileToImageBlock, fileToPDFBlock } from "@/lib/multimodal-utils";
|
||||||
fileToImageBlock,
|
|
||||||
fileToPDFBlock,
|
|
||||||
} from "@/lib/multimodal-utils";
|
|
||||||
import type { Base64ContentBlock } from "@langchain/core/messages";
|
import type { Base64ContentBlock } from "@langchain/core/messages";
|
||||||
|
|
||||||
function StickyToBottomContent(props: {
|
function StickyToBottomContent(props: {
|
||||||
@@ -176,18 +173,17 @@ export function Thread() {
|
|||||||
|
|
||||||
const handleSubmit = (e: FormEvent) => {
|
const handleSubmit = (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!input.trim() || isLoading) return;
|
if ((input.trim().length === 0 && imageUrlList.length === 0 && pdfUrlList.length === 0) || isLoading) return;
|
||||||
setFirstTokenReceived(false);
|
setFirstTokenReceived(false);
|
||||||
|
|
||||||
// TODO: check configurable object for modelname camelcase or snakecase else do openai format
|
// TODO: check configurable object for modelname camelcase or snakecase else do openai format
|
||||||
const isOpenAI = true;
|
const isOpenAI = true;
|
||||||
|
|
||||||
|
|
||||||
const newHumanMessage: Message = {
|
const newHumanMessage: Message = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
type: "human",
|
type: "human",
|
||||||
content: [
|
content: [
|
||||||
{ type: "text", text: input },
|
...(input.trim().length > 0 ? [{ type: "text", text: input }] : []),
|
||||||
...pdfUrlList,
|
...pdfUrlList,
|
||||||
...imageUrlList,
|
...imageUrlList,
|
||||||
] as Message["content"],
|
] as Message["content"],
|
||||||
@@ -220,10 +216,15 @@ export function Thread() {
|
|||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
const fileArray = Array.from(files);
|
const fileArray = Array.from(files);
|
||||||
const imageFiles = fileArray.filter((file) => file.type.startsWith("image"));
|
const imageFiles = fileArray.filter((file) =>
|
||||||
const pdfFiles = fileArray.filter((file) => file.type === "application/pdf");
|
file.type.startsWith("image"),
|
||||||
|
);
|
||||||
|
const pdfFiles = fileArray.filter(
|
||||||
|
(file) => file.type === "application/pdf",
|
||||||
|
);
|
||||||
const invalidFiles = fileArray.filter(
|
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) {
|
if (invalidFiles.length > 0) {
|
||||||
@@ -638,7 +639,7 @@ export function Thread() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="shadow-md transition-all"
|
className="shadow-md transition-all"
|
||||||
disabled={isLoading || !input.trim()}
|
disabled={isLoading || (!input.trim() && imageUrlList.length === 0 && pdfUrlList.length === 0)}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -86,20 +86,69 @@ export function HumanMessage({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{contentImageUrls.length > 0 && (
|
{/* Render images and files if no text */}
|
||||||
<div className="flex flex-wrap justify-end gap-2">
|
{Array.isArray(message.content) && message.content.length > 0 && (
|
||||||
{contentImageUrls.map((imageUrl) => (
|
<div className="flex flex-col gap-2 items-end">
|
||||||
<img
|
{message.content.map((block, idx) => {
|
||||||
src={imageUrl}
|
// Type guard for image block
|
||||||
alt="uploaded image"
|
const isImageBlock =
|
||||||
className="bg-muted h-16 w-16 rounded-md object-cover"
|
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
|
||||||
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="bg-muted ml-auto w-fit rounded-3xl px-4 py-2 text-right whitespace-pre-wrap">
|
{/* Render text if present, otherwise fallback to file/image name */}
|
||||||
{contentString}
|
{contentString && contentString !== "Other" && contentString !== "Multimodal message" ? (
|
||||||
</p>
|
<p className="bg-muted ml-auto w-fit rounded-3xl px-4 py-2 text-right whitespace-pre-wrap">
|
||||||
|
{contentString}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ import { convertToOpenAIImageBlock } from "@langchain/core/messages";
|
|||||||
export async function fileToImageBlock(
|
export async function fileToImageBlock(
|
||||||
file: File,
|
file: File,
|
||||||
): Promise<Base64ContentBlock> {
|
): Promise<Base64ContentBlock> {
|
||||||
const supportedTypes = [
|
const supportedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
||||||
"image/jpeg",
|
|
||||||
"image/png",
|
|
||||||
"image/gif",
|
|
||||||
"image/webp",
|
|
||||||
];
|
|
||||||
if (!supportedTypes.includes(file.type)) {
|
if (!supportedTypes.includes(file.type)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unsupported image type: ${file.type}. Supported types are: ${supportedTypes.join(", ")}`,
|
`Unsupported image type: ${file.type}. Supported types are: ${supportedTypes.join(", ")}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user