From ee1c084ef87fb61fe0116e05d9f5a4d2bfd0a022 Mon Sep 17 00:00:00 2001 From: neulhan Date: Mon, 28 Apr 2025 10:22:17 +0900 Subject: [PATCH 01/33] feat : Support file uploads #56 --- src/components/thread/index.tsx | 92 +++++++++++++++++++++++- src/components/thread/messages/human.tsx | 22 ++++-- src/components/thread/utils.ts | 10 +++ 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/components/thread/index.tsx b/src/components/thread/index.tsx index 1cec4a21..cfd595d0 100644 --- a/src/components/thread/index.tsx +++ b/src/components/thread/index.tsx @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from "uuid"; -import { ReactNode, useEffect, useRef } from "react"; +import { ReactNode, useEffect, useRef, ChangeEvent } from "react"; import { motion } from "framer-motion"; import { cn } from "@/lib/utils"; import { useStreamContext } from "@/providers/Stream"; @@ -20,6 +20,8 @@ import { PanelRightOpen, PanelRightClose, SquarePen, + Plus, + CircleX, } from "lucide-react"; import { useQueryState, parseAsBoolean } from "nuqs"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; @@ -35,6 +37,7 @@ import { TooltipProvider, TooltipTrigger, } from "../ui/tooltip"; +import { MessageContentImageUrl } from "@langchain/core/messages"; function StickyToBottomContent(props: { content: ReactNode; @@ -112,6 +115,9 @@ export function Thread() { parseAsBoolean.withDefault(false), ); const [input, setInput] = useState(""); + const [imageUrlList, setImageUrlList] = useState( + [], + ); const [firstTokenReceived, setFirstTokenReceived] = useState(false); const isLargeScreen = useMediaQuery("(min-width: 1024px)"); @@ -171,7 +177,13 @@ export function Thread() { const newHumanMessage: Message = { id: uuidv4(), type: "human", - content: input, + content: [ + { + type: "text", + text: input, + }, + ...imageUrlList, + ], }; const toolMessages = ensureToolCallsHaveResponses(stream.messages); @@ -191,6 +203,31 @@ export function Thread() { ); setInput(""); + setImageUrlList([]); + }; + + const handleImageUpload = async (e: ChangeEvent) => { + const files = e.target.files; + if (files) { + const imageUrls = await Promise.all( + Array.from(files).map((file) => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => { + resolve({ + type: "image_url", + image_url: { + url: reader.result as string, + }, + }); + }; + reader.readAsDataURL(file); + }); + }), + ); + setImageUrlList([...imageUrlList, ...imageUrls]); + } + e.target.value = ""; }; const handleRegenerate = ( @@ -398,6 +435,38 @@ export function Thread() { onSubmit={handleSubmit} className="mx-auto grid max-w-3xl grid-rows-[1fr_auto] gap-2" > + {imageUrlList.length > 0 && ( +
+ {imageUrlList.map((imageUrl) => { + const imageUrlString = + typeof imageUrl.image_url === "string" + ? imageUrl.image_url + : imageUrl.image_url.url; + return ( +
+ uploaded + + setImageUrlList( + imageUrlList.filter( + (url) => url !== imageUrl, + ), + ) + } + /> +
+ ); + })} +
+ )}