diff options
| author | Yash <[email protected]> | 2024-04-09 06:21:58 +0000 |
|---|---|---|
| committer | Yash <[email protected]> | 2024-04-09 06:21:58 +0000 |
| commit | f4917ef8f6cb90235d142b7ace1328ead4449061 (patch) | |
| tree | 1bd5c18b4719d58f2d6b9bfbaabbdffaff1fc5ae /apps/web/src | |
| parent | test (diff) | |
| parent | removed unnecessary files (diff) | |
| download | supermemory-f4917ef8f6cb90235d142b7ace1328ead4449061.tar.xz supermemory-f4917ef8f6cb90235d142b7ace1328ead4449061.zip | |
Merge branch 'new-ui' of https://github.com/Dhravya/supermemory into new-ui
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/api/chat/route.ts | 5 | ||||
| -rw-r--r-- | apps/web/src/components/ChatMessage.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/components/Main.tsx | 137 | ||||
| -rw-r--r-- | apps/web/src/components/QueryAI.tsx | 139 | ||||
| -rw-r--r-- | apps/web/src/components/SearchResults.tsx | 38 | ||||
| -rw-r--r-- | apps/web/src/contexts/MemoryContext.tsx | 2 |
6 files changed, 104 insertions, 219 deletions
diff --git a/apps/web/src/app/api/chat/route.ts b/apps/web/src/app/api/chat/route.ts index 2cb03186..ef59fd43 100644 --- a/apps/web/src/app/api/chat/route.ts +++ b/apps/web/src/app/api/chat/route.ts @@ -31,22 +31,25 @@ export async function POST(req: NextRequest) { chatHistory: ChatHistory[] }; + console.log("CHathistory", chatHistory) if (!query) { return new Response(JSON.stringify({ message: "Invalid query" }), { status: 400 }); } + const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/chat?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}`, { headers: { "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, }, method: "POST", body: JSON.stringify({ - chatHistory + chatHistory: chatHistory.chatHistory ?? [] }) }) console.log(resp.status) + console.log(resp.statusText) if (resp.status !== 200 || !resp.ok) { const errorData = await resp.json(); diff --git a/apps/web/src/components/ChatMessage.tsx b/apps/web/src/components/ChatMessage.tsx index a8199758..114d0a48 100644 --- a/apps/web/src/components/ChatMessage.tsx +++ b/apps/web/src/components/ChatMessage.tsx @@ -7,9 +7,11 @@ import Image from 'next/image'; function ChatMessage({ message, user, + sources, }: { message: string; user: User | 'ai'; + sources?: string[]; }) { return ( <div className="flex flex-col gap-4"> diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index 813c0d62..3f021906 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -7,10 +7,10 @@ import { MemoryDrawer } from "./MemoryDrawer"; import useViewport from "@/hooks/useViewport"; import { motion } from "framer-motion"; import { cn } from "@/lib/utils"; -import SearchResults from "./SearchResults"; import { ChatHistory } from "../../types/memory"; import { ChatMessage } from "./ChatMessage"; import { useSession } from "next-auth/react"; +import { Card, CardContent } from "./ui/card"; function supportsDVH() { try { @@ -21,8 +21,6 @@ function supportsDVH() { } export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { - return <Chat sidebarOpen={sidebarOpen} />; - const [hide, setHide] = useState(false); const [value, setValue] = useState(""); const { width } = useViewport(); @@ -37,7 +35,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { // TEMPORARY solution: Basically this is to just keep track of the sources used for each chat message // Not a great solution const [chatTextSourceDict, setChatTextSourceDict] = useState< - Record<string, string> + Record<string, string[]> >({}); // helper function to append a new msg @@ -107,7 +105,36 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { remainingData = part; } else if (parsedPart && parsedPart.response) { // If the part is parsable and has the "response" field, update the AI response state - setAIResponse((prev) => prev + parsedPart.response); + // setAIResponse((prev) => prev + parsedPart.response); + // appendToChatHistory('model', parsedPart.response); + + // Append to chat history in this way: + // If the last message was from the model, append to that message + // Otherwise, Start a new message from the model and append to that + if ( + chatHistory.length > 0 && + chatHistory[chatHistory.length - 1].role === "model" + ) { + setChatHistory((prev: any) => { + const lastMessage = prev[prev.length - 1]; + const newParts = [ + ...lastMessage.parts, + { text: parsedPart.response }, + ]; + return [ + ...prev.slice(0, prev.length - 1), + { ...lastMessage, parts: newParts }, + ]; + }); + } else { + setChatHistory((prev) => [ + ...prev, + { + role: "model", + parts: [{ text: parsedPart.response }], + }, + ]); + } } } catch (error) { // If parsing fails and it's not the last part, it's a malformed JSON @@ -139,17 +166,35 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { e.preventDefault(); setIsAiLoading(true); + console.log(value); + + appendToChatHistory("user", value); + const sourcesResponse = await fetch( - `/api/query?sourcesOnly=true&q=${value}`, + `/api/chat?sourcesOnly=true&q=${value}`, + { + method: "POST", + body: JSON.stringify({ + chatHistory, + }), + }, ); const sourcesInJson = (await sourcesResponse.json()) as { ids: string[]; }; - setSearchResults(sourcesInJson.ids); + setSearchResults((prev) => + Array.from(new Set([...prev, ...sourcesInJson.ids])), + ); - const response = await fetch(`/api/query?q=${value}`); + // TODO: PASS THE `SPACE` TO THE API + const response = await fetch(`/api/chat?q=${value}`, { + method: "POST", + body: JSON.stringify({ + chatHistory, + }), + }); if (response.status !== 200) { setIsAiLoading(false); @@ -164,8 +209,10 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { // @ts-ignore reader.read().then(function processText({ done, value }) { if (done) { - // setSearchResults(JSON.parse(result.replace('data: ', ''))); - // setIsAiLoading(false); + setIsAiLoading(false); + setToBeParsed(""); + setValue(""); + return; } @@ -177,7 +224,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { }; return ( - <main + <motion.main data-sidebar-open={sidebarOpen} ref={main} className={cn( @@ -189,43 +236,55 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { {chatHistory.map((chat, index) => ( <ChatMessage key={index} - message={chat.parts[0].text} + message={chat.parts.map((part) => part.text).join("")} user={chat.role === "model" ? "ai" : session?.user!} /> ))} + {searchResults.length > 0 && ( + <div className="mt-4"> + <h1>Related memories</h1> + <div className="grid gap-6"> + {searchResults.map((value, index) => ( + <Card key={index}> + <CardContent className="space-y-2">{value}</CardContent> + </Card> + ))} + </div> + </div> + )} </div> <h1 className="text-rgray-11 mt-auto w-full text-center text-3xl md:mt-0"> Ask your Second brain </h1> - - <Textarea2 - ref={textArea} - className="mt-auto h-max max-h-[30em] min-h-[3em] resize-y flex-row items-start justify-center overflow-auto py-5 md:mt-0 md:h-[20vh] md:resize-none md:flex-col md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2" - textAreaProps={{ - placeholder: "Ask your SuperMemory...", - className: - "h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]", - value, - autoFocus: true, - onChange: (e) => setValue(e.target.value), - }} + <form + className="overflow-none mt-auto h-max min-h-[3em] w-full resize-y flex-row items-start justify-center py-5 md:mt-0 md:h-[20vh] md:resize-none md:flex-col md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2" + onSubmit={async (e) => await getSearchResults(e)} > - <div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2"> - <FilterCombobox className="hidden md:flex" /> - <button - type="submit" - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0" - > - <ArrowRight className="h-5 w-5" /> - </button> - </div> - </Textarea2> - {/* {searchResults && ( - <SearchResults aiResponse={aiResponse} sources={searchResults} /> - )} */} + <Textarea2 + ref={textArea} + textAreaProps={{ + placeholder: "Ask your SuperMemory...", + className: + "h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]", + value, + autoFocus: true, + onChange: (e) => setValue(e.target.value), + }} + > + <div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2"> + <FilterCombobox className="hidden md:flex" /> + <button + type="submit" + disabled={value.trim().length < 1} + className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0" + > + <ArrowRight className="h-5 w-5" /> + </button> + </div> + </Textarea2> + </form> {width <= 768 && <MemoryDrawer hide={hide} />} - </main> + </motion.main> ); } diff --git a/apps/web/src/components/QueryAI.tsx b/apps/web/src/components/QueryAI.tsx deleted file mode 100644 index 3cb14178..00000000 --- a/apps/web/src/components/QueryAI.tsx +++ /dev/null @@ -1,139 +0,0 @@ -'use client'; - -import { Label } from './ui/label'; -import React, { useEffect, useState } from 'react'; -import { Input } from './ui/input'; -import { Button } from './ui/button'; -import SearchResults from './SearchResults'; - -function QueryAI() { - const [searchResults, setSearchResults] = useState<string[]>([]); - const [isAiLoading, setIsAiLoading] = useState(false); - - const [aiResponse, setAIResponse] = useState(''); - const [input, setInput] = useState(''); - const [toBeParsed, setToBeParsed] = useState(''); - - const handleStreamData = (newChunk: string) => { - // Append the new chunk to the existing data to be parsed - setToBeParsed((prev) => prev + newChunk); - }; - - useEffect(() => { - // Define a function to try parsing the accumulated data - const tryParseAccumulatedData = () => { - // Attempt to parse the "toBeParsed" state as JSON - try { - // Split the accumulated data by the known delimiter "\n\n" - const parts = toBeParsed.split('\n\n'); - let remainingData = ''; - - // Process each part to extract JSON objects - parts.forEach((part, index) => { - try { - const parsedPart = JSON.parse(part.replace('data: ', '')); // Try to parse the part as JSON - - // If the part is the last one and couldn't be parsed, keep it to accumulate more data - if (index === parts.length - 1 && !parsedPart) { - remainingData = part; - } else if (parsedPart && parsedPart.response) { - // If the part is parsable and has the "response" field, update the AI response state - setAIResponse((prev) => prev + parsedPart.response); - } - } catch (error) { - // If parsing fails and it's not the last part, it's a malformed JSON - if (index !== parts.length - 1) { - console.error('Malformed JSON part: ', part); - } else { - // If it's the last part, it may be incomplete, so keep it - remainingData = part; - } - } - }); - - // Update the toBeParsed state to only contain the unparsed remainder - if (remainingData !== toBeParsed) { - setToBeParsed(remainingData); - } - } catch (error) { - console.error('Error parsing accumulated data: ', error); - } - }; - - // Call the parsing function if there's data to be parsed - if (toBeParsed) { - tryParseAccumulatedData(); - } - }, [toBeParsed]); - - const getSearchResults = async (e: React.FormEvent<HTMLFormElement>) => { - e.preventDefault(); - setIsAiLoading(true); - - const sourcesResponse = await fetch( - `/api/query?sourcesOnly=true&q=${input}`, - ); - - const sourcesInJson = (await sourcesResponse.json()) as { - ids: string[]; - }; - - setSearchResults(sourcesInJson.ids); - - const response = await fetch(`/api/query?q=${input}`); - - if (response.status !== 200) { - setIsAiLoading(false); - return; - } - - if (response.body) { - let reader = response.body.getReader(); - let decoder = new TextDecoder('utf-8'); - let result = ''; - - // @ts-ignore - reader.read().then(function processText({ done, value }) { - if (done) { - // setSearchResults(JSON.parse(result.replace('data: ', ''))); - // setIsAiLoading(false); - return; - } - - handleStreamData(decoder.decode(value)); - - return reader.read().then(processText); - }); - } - }; - - return ( - <div className="w-full max-w-2xl mx-auto"> - <form onSubmit={async (e) => await getSearchResults(e)} className="mt-8"> - <Label htmlFor="searchInput">Ask your SuperMemory</Label> - <div className="flex flex-col md:flex-row md:w-full md:items-center space-y-2 md:space-y-0 md:space-x-2"> - <Input - value={input} - onChange={(e) => setInput(e.target.value)} - placeholder="Search using AI... ✨" - id="searchInput" - /> - <Button - disabled={isAiLoading} - className="max-w-min md:w-full" - type="submit" - variant="default" - > - Ask AI - </Button> - </div> - </form> - - {searchResults && ( - <SearchResults aiResponse={aiResponse} sources={searchResults} /> - )} - </div> - ); -} - -export default QueryAI; diff --git a/apps/web/src/components/SearchResults.tsx b/apps/web/src/components/SearchResults.tsx deleted file mode 100644 index 0445d0b4..00000000 --- a/apps/web/src/components/SearchResults.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client' - -import React from 'react'; -import { Card, CardContent } from './ui/card'; -import Markdown from 'react-markdown'; -import remarkGfm from 'remark-gfm' - -function SearchResults({ - aiResponse, - sources, -}: { - aiResponse: string; - sources: string[]; -}) { - return ( - <div - style={{ - backgroundImage: `linear-gradient(to right, #E5D9F2, #CDC1FF)`, - }} - className="w-full max-w-2xl mx-auto px-4 py-6 space-y-6 border mt-4 rounded-xl" - > - <div className="text-start"> - <div className="text-xl text-black"> - <Markdown remarkPlugins={[remarkGfm]}>{aiResponse.replace('</s>', '')}</Markdown> - </div> - </div> - <div className="grid gap-6"> - {sources.map((value, index) => ( - <Card key={index}> - <CardContent className="space-y-2">{value}</CardContent> - </Card> - ))} - </div> - </div> - ); -} - -export default SearchResults; diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index 820736ff..3727c464 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -31,8 +31,6 @@ export const MemoryProvider: React.FC< [spaces], ); - console.log(spaces); - return ( <MemoryContext.Provider value={{ spaces, addSpace, deleteSpace }}> {children} |