From dc16369f6aae76465546f490891677fd77c0f814 Mon Sep 17 00:00:00 2001 From: Prince Baghel Date: Fri, 20 Sep 2024 19:00:24 +0530 Subject: [PATCH] update: parallel inputbar2 (mergable to old one) --- src/app/page.tsx | 57 ++-- src/components/audiowaveform.tsx | 31 +- src/components/chat.tsx | 16 +- src/components/inputBar2.tsx | 472 +++++++++++++++++++++++++++++++ src/components/modelswitcher.tsx | 8 +- 5 files changed, 526 insertions(+), 58 deletions(-) create mode 100644 src/components/inputBar2.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 734fc068..49df2ebd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { Header } from "@/components/header"; import { Button, buttonVariants } from "@/components/button"; import Link from "next/link"; @@ -10,8 +10,10 @@ import { motion, AnimatePresence, useAnimation } from "framer-motion"; import { useInView } from "react-intersection-observer"; import { Key, LayoutDashboard } from "lucide-react"; import { useAuth } from "@clerk/nextjs"; -import TextareaAutosize from "react-textarea-autosize"; import { useRouter } from "next/navigation"; +import InputBar from "@/components/inputBar2"; +import { ChatType, chattype } from "@/lib/types"; +import { parseAsStringEnum, useQueryState } from "next-usequerystate"; const handleSmoothScroll = (): void => { if (typeof window !== "undefined") { @@ -51,25 +53,23 @@ export default function Home() { controls.start("hidden"); } }, [controls, inView]); + const [chatType, setChattype] = useQueryState( + "model", + parseAsStringEnum(Object.values(chattype)), + ); const { isSignedIn, orgId, orgSlug, userId } = useAuth(); // if (isSignedIn) { // redirect("/dashboard/user"); // } - console.log( - "orgId, orgSlug, userId", - { isSignedIn, orgId, orgSlug, userId }, - "\n\n\n\n\n\n", - ); - - const handleInputChange = (e: React.ChangeEvent) => { + const handleInputChange = (e: any) => { setInput(e.target.value); navigator.clipboard.writeText(e.target.value); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async () => { + console.log("handleSubmit"); if (input.trim() === "") return; try { @@ -84,6 +84,7 @@ export default function Home() { console.error("Error creating new chat:", error); } }; + return (
@@ -105,7 +106,7 @@ export default function Home() { quality={5} />
-
+

Hello Innovator,

@@ -114,26 +115,18 @@ export default function Home() {
{isSignedIn ? ( -
- { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit( - e as unknown as React.FormEvent, - ); - } - }} - className="flex-none resize-none rounded-sm grow w-full bg-background border border-secondary text-primary p-2 text-sm" - /> - + > + } + /> ) : ( { }; return ( -
@@ -70,10 +69,10 @@ const AudioWaveForm = (props: Props) => { noiseSuppression={true} />
- - -
+
+
); }; diff --git a/src/components/chat.tsx b/src/components/chat.tsx index cdbee832..007e128a 100644 --- a/src/components/chat.tsx +++ b/src/components/chat.tsx @@ -41,7 +41,6 @@ export default function Chat(props: ChatProps) { settldrawImageUrl, onClickOpenChatSheet, } = useImageState(); - const [chattype, setChattype] = useState(props?.type || "chat"); const [isChatCompleted, setIsChatCompleted] = useState(false); const [calculatedMessages, setCalculatedMessages] = useState([]); // const { presenceData, updateStatus } = usePresence(`channel_${props.chatId}`); @@ -50,10 +49,12 @@ export default function Chat(props: ChatProps) { const [imageUrl, setImageUrl] = useState(""); const [imageName, setImageName] = useState(""); const queryClient = useQueryClient(); - const [isNewChat] = useQueryState("new"); - const [isFromClipboard] = useQueryState("clipboard"); - console.log("isFromClipboard", isFromClipboard); - console.log("isNewChat", isNewChat); + const [isNewChat, setIsNewChat] = useQueryState("new"); + const [isFromClipboard, setIsFromClipboard] = useQueryState("clipboard"); + const [incomingModel] = useQueryState("model"); + const [chattype, setChattype] = useState( + props?.type || incomingModel || "chat", + ); const onDrop = useCallback(async (acceptedFiles: File[]) => { if (acceptedFiles && acceptedFiles[0]?.type.startsWith("image/")) { @@ -162,7 +163,8 @@ export default function Chat(props: ChatProps) { console.log("messages", messages); useEffect(() => { - if (isFromClipboard && isNewChat) { + if (isFromClipboard === "true" && isNewChat === "true") { + //TODO: use types for useQueryState navigator.clipboard .readText() .then((text) => { @@ -176,6 +178,8 @@ export default function Chat(props: ChatProps) { } as Message; append(newMessage); } + setIsFromClipboard("false"); + setIsNewChat("false"); }) .catch((err) => { console.error("Failed to read clipboard contents: ", err); diff --git a/src/components/inputBar2.tsx b/src/components/inputBar2.tsx new file mode 100644 index 00000000..d944cae2 --- /dev/null +++ b/src/components/inputBar2.tsx @@ -0,0 +1,472 @@ +"use client"; + +import TextareaAutosize from "react-textarea-autosize"; +import { + ChangeEvent, + Dispatch, + FormEvent, + SetStateAction, + useState, +} from "react"; +import { ChatRequestOptions, CreateMessage, Message, nanoid } from "ai"; +import { Microphone, PaperPlaneTilt } from "@phosphor-icons/react"; +import { Button } from "@/components/button"; +import { ChatType, chattype } from "@/lib/types"; +import { Loader2 } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useQueryClient } from "@tanstack/react-query"; +import { fetchEventSource } from "@microsoft/fetch-event-source"; +import z from "zod"; +import { toast } from "./ui/use-toast"; +import { useImageState } from "@/store/tlDrawImage"; +import ModelSwitcher from "./modelswitcher"; +// import VadAudio from "./vadAudio"; +import AudioWaveForm from "./audiowaveform"; +const isValidImageType = (value: string) => + /^image\/(jpeg|png|jpg|webp)$/.test(value); + +const Schema = z.object({ + imageName: z.any(), + imageType: z.string().refine(isValidImageType, { + message: "File type must be JPEG, PNG, or WEBP image", + path: ["type"], + }), + imageSize: z.number(), + value: z.string(), + userId: z.string(), + orgId: z.string(), + chatId: z.any(), + file: z.instanceof(Blob), + message: z.array(z.any()), + id: z.string(), + chattype: chattype, +}); +function isJSON(str: any) { + let obj: any; + try { + obj = JSON.parse(str); + } catch (e) { + return false; + } + if (typeof obj === "number" || obj instanceof Number) { + return false; + } + return !!obj && typeof obj === "object"; +} + +interface InputBarProps { + dropZoneImage?: File[]; + value?: string; + onChange?: ( + e: ChangeEvent | ChangeEvent, + ) => void; + username?: string; + userId?: string; + append?: ( + message: Message | CreateMessage, + chatRequestOptions?: ChatRequestOptions | undefined, + ) => Promise; + setInput?: Dispatch>; + isChatCompleted?: boolean; + chatId?: string; + messages?: Message[]; + orgId?: string; + setMessages?: (messages: Message[]) => void; + isLoading?: boolean; + chattype?: ChatType; + setChattype?: Dispatch>; + setDropzoneActive?: Dispatch>; + dropZoneActive?: boolean; + onClickOpen?: any; + onClickOpenChatSheet?: boolean | any; + isHome?: boolean; + submitInput?: () => void; +} + +const InputBar = (props: InputBarProps) => { + const { + tldrawImageUrl, + tlDrawImage, + setTlDrawImage, + settldrawImageUrl, + onClickOpenChatSheet, + } = useImageState(); + const [isAudioWaveVisible, setIsAudioWaveVisible] = useState(false); + const [isRecording, setIsRecording] = useState(false); + const [isTranscribing, setIsTranscribing] = useState(false); + const [disableInputs, setDisableInputs] = useState(false); + const [isRagLoading, setIsRagLoading] = useState(false); + const queryClient = useQueryClient(); + + // const preferences = usePreferences(); + // const { presenceData, updateStatus } = usePresence( + // `channel_${props.chatId}`, + // { + // id: props.userId, + // username: props.username, + // isTyping: false, + // } + // ); + // using local state for development purposes + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + console.log("props.value", props.value); + if (props.value?.trim() === "") { + return; + } + const ID = nanoid(); + const message: Message = { + id: ID, + role: "user", + content: props.value || "", + name: `${props.username},${props.userId}`, + audio: "", + }; + if (props.dropZoneActive) { + setDisableInputs(true); + setIsRagLoading(true); + + console.log("image dropped"); + props?.setInput?.(""); + props?.setDropzoneActive?.(false); + + if (props.dropZoneImage && props.dropZoneImage.length > 0) { + const zodMessage: any = Schema.safeParse({ + imageName: props.dropZoneImage[0].name, + imageType: props.dropZoneImage[0].type, + imageSize: props.dropZoneImage[0].size, + file: props.dropZoneImage[0], + value: props.value, + userId: props.userId, + orgId: props.orgId, + chatId: props.chatId, + message: [...(props.messages || []), message], + id: ID, + chattype: props.chattype, + }); + const imageExtension = props.dropZoneImage[0].name.substring( + props.dropZoneImage[0].name.lastIndexOf(".") + 1, + ); + // console.log("zodmessage", zodMessage); + // console.log("dropzone", props.dropZoneActive); + if (zodMessage.success) { + const file = props.dropZoneImage[0]; + const zodMSG = JSON.stringify(zodMessage); + const formData = new FormData(); + formData.append("zodMessage", zodMSG); + formData.append("file", file); + const response = await fetch("/api/imageInput", { + method: "POST", + body: formData, + }); + if (response) { + console.log("responce", response); + let assistantMsg = ""; + const reader = response.body?.getReader(); + console.log("reader", reader); + const decoder = new TextDecoder(); + let charsReceived = 0; + let content = ""; + reader + ?.read() + .then(async function processText({ done, value }) { + if (done) { + settldrawImageUrl(""); + setTlDrawImage(""); + setDisableInputs(false); + setIsRagLoading(false); + console.log("Stream complete"); + return; + } + charsReceived += value.length; + const chunk = decoder.decode(value, { stream: true }); + assistantMsg += chunk === "" ? `${chunk} \n` : chunk; + content += chunk === "" ? `${chunk} \n` : chunk; + // console.log("assistMsg", assistantMsg); + props?.setMessages?.([ + ...(props.messages || []), + awsImageMessage, + message, + { + ...assistantMessage, + content: assistantMsg, + }, + ]); + reader.read().then(processText); + }) + .then((e) => { + console.error("error", e); + }); + const awsImageMessage = { + role: "user", + subRole: "input-image", + content: `${process.env.NEXT_PUBLIC_IMAGE_PREFIX_URL}imagefolder/${props.chatId}/${ID}.${imageExtension}`, + id: ID, + } as Message; + const assistantMessage: Message = { + id: ID, + role: "assistant", + content: content, + }; + } else { + console.error(" Response Error :", response); + } + } else { + toast({ + description: ( +
+                
+                  {zodMessage.error.issues[0].message}
+                
+              
+ ), + }); + } + } + return; + } + + if (props.chattype === "rag") { + setIsRagLoading(true); + setDisableInputs(true); + props?.setMessages?.([...(props.messages || []), message]); + props?.setInput?.(""); + let content = ""; + const id = nanoid(); + const assistantMessage: Message = { + id, + role: "assistant", + content: "", + }; + let message2 = ""; + try { + await fetchEventSource(`/api/chatmodel/${props.chatId}}`, { + method: "POST", + credentials: "include", + body: JSON.stringify({ + input: props.value, + messages: [...(props.messages || []), message], + userId: props.userId, + orgId: props.orgId, + chattype: props.chattype, + enableStreaming: true, + }), + openWhenHidden: true, + async onopen(response) { + setDisableInputs(true); + console.log("events started"); + }, + async onclose() { + setDisableInputs(false); + setIsRagLoading(false); + console.log("event reading closed", message2); + fetch(`/api/updatedb/${props.chatId}`, { + method: "POST", + body: JSON.stringify({ + messages: [ + ...(props.messages || []), + message, + { + ...assistantMessage, + content: content, + }, + ], + orgId: props.orgId, + usreId: props.userId, + }), + }); // TODO: handle echoes is typing + return; + }, + async onmessage(event: any) { + if (event.data !== "[END]" && event.event !== "function_call") { + message2 += event.data === "" ? `${event.data} \n` : event.data; + content += event.data === "" ? `${event.data} \n` : event.data; + props?.setMessages?.([ + ...(props.messages || []), + message, + { + ...assistantMessage, + content: content, + }, + ]); + } + }, + onerror(error: any) { + console.error("event reading error", error); + }, + }); + return; + } catch (error) { + console.error(error); + return; + } + } + // if (props.choosenAI === "universal") { + props?.append?.(message as Message); + props?.setInput?.(""); + }; + + const handleAudio = async (audioFile: File) => { + setIsAudioWaveVisible(false); + setIsTranscribing(true); + const f = new FormData(); + f.append("file", audioFile); + // Buffer.from(audioFile) + console.log(audioFile); + try { + const res = await fetch("/api/transcript", { + method: "POST", + body: f, + }); + + // console.log('data', await data.json()); + const data = await res.json(); + console.log("got the data", data); + props?.setInput?.(data.text); + setIsTranscribing(false); + } catch (err) { + console.error("got in error", err); + setIsTranscribing(false); + } + }; + + //TODO: + const handleInputChange = (e: ChangeEvent) => { + if (props.dropZoneActive) { + props?.setInput?.(e.target.value); + } else { + const inputValue = e.target.value; + navigator.clipboard + .writeText(inputValue) + .then(() => { + console.log("Input value copied to clipboard"); + }) + .catch((err) => { + console.error("Could not copy text: ", err); + }); + props?.onChange?.(e); + } + }; + + return ( +
+
+ {/* */} + {isAudioWaveVisible && ( + + )} +
+
+
+ { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (props?.isHome) { + console.log("home submit"); + props?.submitInput?.(); + } else { + handleSubmit(e as unknown as FormEvent); + } + } + }} + className="flex-none resize-none rounded-sm grow w-full bg-background border border-secondary text-primary p-2 text-sm disabled:text-muted" + /> + +
+ +
+
+
+
+ +
+
+
+ + +
+
+
+ {/*
*/} +
+
+ ); +}; + +export default InputBar; diff --git a/src/components/modelswitcher.tsx b/src/components/modelswitcher.tsx index e09a0db4..8773af06 100644 --- a/src/components/modelswitcher.tsx +++ b/src/components/modelswitcher.tsx @@ -17,12 +17,12 @@ import { ChatType } from "@/lib/types"; export interface InputBarActionProps extends React.ButtonHTMLAttributes { - chattype: ChatType; - setChatType: Dispatch>; + chattype?: ChatType; + setChatType?: Dispatch>; } const ModelSwitcher = React.forwardRef( - ({ chattype, setChatType, className, ...props }, ref) => { + ({ chattype = "chat", setChatType, className, ...props }, ref) => { const Comp = chattype === "advanced" ? ( @@ -49,7 +49,7 @@ const ModelSwitcher = React.forwardRef( setChatType(value as ChatType)} + onValueChange={(value) => setChatType?.(value as ChatType)} > Advanced