diff options
| -rw-r--r-- | apps/web/app/(dash)/chat/chatWindow.tsx | 80 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/page.tsx | 37 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/queryinput.tsx | 96 |
3 files changed, 141 insertions, 72 deletions
diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx index 6189b874..23f49554 100644 --- a/apps/web/app/(dash)/chat/chatWindow.tsx +++ b/apps/web/app/(dash)/chat/chatWindow.tsx @@ -1,7 +1,7 @@ "use client"; import { AnimatePresence } from "framer-motion"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import QueryInput from "../home/queryinput"; import { cn } from "@repo/ui/lib/utils"; import { motion } from "framer-motion"; @@ -36,15 +36,12 @@ function ChatWindow({ { question: q, answer: { - parts: [ - // { - // text: `It seems like there might be a typo in your question. Could you please clarify or provide more context? If you meant "interesting," please let me know what specific information or topic you find interesting, and I can help you with that.`, - // }, - ], + parts: [], sources: [], }, }, ]); + const [isAutoScroll, setIsAutoScroll] = useState(true); const removeJustificationFromText = (text: string) => { // remove everything after the first "<justification>" word @@ -68,7 +65,7 @@ function ChatWindow({ { method: "POST", body: JSON.stringify({ chatHistory }), - } + }, ); // TODO: handle this properly @@ -89,11 +86,15 @@ function ChatWindow({ } setChatHistory((prevChatHistory) => { + window.scrollTo({ + top: document.documentElement.scrollHeight, + behavior: "smooth", + }); const newChatHistory = [...prevChatHistory]; const lastAnswer = newChatHistory[newChatHistory.length - 1]; if (!lastAnswer) return prevChatHistory; const filteredSourceUrls = new Set( - sourcesParsed.data.metadata.map((source) => source.url) + sourcesParsed.data.metadata.map((source) => source.url), ); const uniqueSources = sourcesParsed.data.metadata.filter((source) => { if (filteredSourceUrls.has(source.url)) { @@ -106,9 +107,9 @@ function ChatWindow({ title: source.title ?? "Untitled", type: source.type ?? "page", source: source.url ?? "https://supermemory.ai", - content: source.content ?? "No content available", + content: source.description ?? "No content available", numChunks: sourcesParsed.data.metadata.filter( - (f) => f.url === source.url + (f) => f.url === source.url, ).length, })); return newChatHistory; @@ -129,7 +130,16 @@ function ChatWindow({ const newChatHistory = [...prevChatHistory]; const lastAnswer = newChatHistory[newChatHistory.length - 1]; if (!lastAnswer) return prevChatHistory; - lastAnswer.answer.parts.push({ text: new TextDecoder().decode(value) }); + const txt = new TextDecoder().decode(value); + + if (isAutoScroll) { + window.scrollTo({ + top: document.documentElement.scrollHeight, + behavior: "smooth", + }); + } + + lastAnswer.answer.parts.push({ text: txt }); return newChatHistory; }); } @@ -137,13 +147,11 @@ function ChatWindow({ useEffect(() => { if (q.trim().length > 0) { + setLayout("chat"); getAnswer( q, - spaces.map((s) => s.id) + spaces.map((s) => s.id), ); - setTimeout(() => { - setLayout("chat"); - }, 300); } else { router.push("/home"); } @@ -159,22 +167,27 @@ function ChatWindow({ className="max-w-3xl h-full justify-center items-center flex mx-auto w-full flex-col" > <div className="w-full h-96"> - <QueryInput initialQuery={q} initialSpaces={[]} disabled /> + <QueryInput + handleSubmit={() => {}} + initialQuery={q} + initialSpaces={[]} + disabled + /> </div> </motion.div> ) : ( <div - className="max-w-3xl flex mx-auto w-full flex-col mt-24" + className="max-w-3xl relative flex mx-auto w-full flex-col mt-24 pb-32" key="chat" > {chatHistory.map((chat, idx) => ( <div key={idx} - className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b" : ""}`} + className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`} > <h2 className={cn( - "text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl" + "text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl", )} > {chat.question} @@ -273,7 +286,7 @@ function ChatWindow({ className="flex flex-col gap-2" > {removeJustificationFromText( - chat.answer.parts.map((part) => part.text).join("") + chat.answer.parts.map((part) => part.text).join(""), )} </Markdown> </div> @@ -307,6 +320,33 @@ function ChatWindow({ </div> </div> ))} + + <div className="fixed bottom-0 w-full max-w-3xl pb-4"> + <QueryInput + mini + className="w-full shadow-md" + initialQuery={""} + initialSpaces={[]} + handleSubmit={async (q, spaces) => { + setChatHistory((prevChatHistory) => { + return [ + ...prevChatHistory, + { + question: q, + answer: { + parts: [], + sources: [], + }, + }, + ]; + }); + await getAnswer( + q, + spaces.map((s) => `${s.id}`), + ); + }} + /> + </div> </div> )} </AnimatePresence> diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx index 55f2928e..6fe26513 100644 --- a/apps/web/app/(dash)/home/page.tsx +++ b/apps/web/app/(dash)/home/page.tsx @@ -1,11 +1,12 @@ -import React from "react"; -import Menu from "../menu"; -import Header from "../header"; +"use client"; + +import React, { useEffect, useState } from "react"; import QueryInput from "./queryinput"; import { homeSearchParamsCache } from "@/lib/searchParams"; import { getSpaces } from "@/app/actions/fetchers"; +import { useRouter } from "next/navigation"; -async function Page({ +function Page({ searchParams, }: { searchParams: Record<string, string | string[] | undefined>; @@ -13,12 +14,18 @@ async function Page({ // TODO: use this to show a welcome page/modal const { firstTime } = homeSearchParamsCache.parse(searchParams); - let spaces = await getSpaces(); + const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]); + + useEffect(() => { + getSpaces().then((res) => { + if (res.success && res.data) { + setSpaces(res.data); + } + // TODO: HANDLE ERROR + }); + }, []); - if (!spaces.success) { - // TODO: handle this error properly. - spaces.data = []; - } + const { push } = useRouter(); return ( <div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col"> @@ -26,7 +33,17 @@ async function Page({ {/* <div className="">hi {firstTime ? 'first time' : ''}</div> */} <div className="w-full h-96"> - <QueryInput initialSpaces={spaces.data} /> + <QueryInput + handleSubmit={(q, spaces) => { + const newQ = + "/chat?q=" + + encodeURI(q) + + (spaces ? "&spaces=" + JSON.stringify(spaces) : ""); + + push(newQ); + }} + initialSpaces={spaces} + /> </div> </div> ); diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx index d0c27b8d..ce45e36b 100644 --- a/apps/web/app/(dash)/home/queryinput.tsx +++ b/apps/web/app/(dash)/home/queryinput.tsx @@ -12,6 +12,9 @@ function QueryInput({ initialQuery = "", initialSpaces = [], disabled = false, + className, + mini = false, + handleSubmit, }: { initialQuery?: string; initialSpaces?: { @@ -19,32 +22,14 @@ function QueryInput({ name: string; }[]; disabled?: boolean; + className?: string; + mini?: boolean; + handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void; }) { const [q, setQ] = useState(initialQuery); const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); - const { push } = useRouter(); - - const parseQ = () => { - // preparedSpaces is list of spaces selected by user, with id and name - const preparedSpaces = initialSpaces - .filter((x) => selectedSpaces.includes(x.id)) - .map((x) => { - return { - id: x.id, - name: x.name, - }; - }); - - const newQ = - "/chat?q=" + - encodeURI(q) + - (selectedSpaces ? "&spaces=" + JSON.stringify(preparedSpaces) : ""); - - return newQ; - }; - const options = useMemo( () => initialSpaces.map((x) => ({ @@ -54,21 +39,43 @@ function QueryInput({ [initialSpaces], ); + const preparedSpaces = useMemo( + () => + initialSpaces + .filter((x) => selectedSpaces.includes(x.id)) + .map((x) => { + return { + id: x.id, + name: x.name, + }; + }), + [selectedSpaces, initialSpaces], + ); + return ( - <div> - <div className="bg-secondary rounded-t-[24px]"> + <div className={className}> + <div + className={`bg-secondary ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`} + > {/* input and action button */} - <form action={async () => push(parseQ())} className="flex gap-4 p-3"> + <form + action={async () => { + handleSubmit(q, preparedSpaces); + setQ(""); + }} + className="flex gap-4 p-3" + > <textarea name="q" cols={30} - rows={4} + rows={mini ? 2 : 4} className="bg-transparent pt-2.5 text-base text-[#989EA4] focus:text-foreground duration-200 tracking-[3%] outline-none resize-none w-full p-4" placeholder="Ask your second brain..." onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); - if (!e.shiftKey) push(parseQ()); + handleSubmit(q, preparedSpaces); + setQ(""); } }} onChange={(e) => setQ(e.target.value)} @@ -84,24 +91,29 @@ function QueryInput({ <Image src={ArrowRightIcon} alt="Right arrow icon" /> </button> </form> - - <Divider /> </div> {/* selected sources */} - <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-[24px]"> - <MultipleSelector - key={options.length} - disabled={disabled} - defaultOptions={options} - onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))} - placeholder="Focus on specific spaces..." - emptyIndicator={ - <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400"> - no results found. - </p> - } - /> - </div> + {!mini && ( + <> + <Divider /> + <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-3xl"> + <MultipleSelector + key={options.length} + disabled={disabled} + defaultOptions={options} + onChange={(e) => + setSelectedSpaces(e.map((x) => parseInt(x.value))) + } + placeholder="Focus on specific spaces..." + emptyIndicator={ + <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400"> + no results found. + </p> + } + /> + </div> + </> + )} </div> ); } |