"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { FilterSpaces } from "./Sidebar/FilterCombobox"; import { Textarea2 } from "./ui/textarea"; import { ArrowRight, ArrowUp } from "lucide-react"; import { MemoryDrawer } from "./MemoryDrawer"; import useViewport from "@/hooks/useViewport"; import { AnimatePresence, motion } from "framer-motion"; import { cn, countLines, getIdsFromSource } from "@/lib/utils"; import { ChatHistory } from "../../types/memory"; import { ChatAnswer, ChatMessage, ChatQuestion } from "./ChatMessage"; import { useRouter, useSearchParams } from "next/navigation"; import { useMemory } from "@/contexts/MemoryContext"; import Image from "next/image"; import { getMemoriesFromUrl } from "@/actions/db"; import { ProfileDrawer } from "./ProfileDrawer"; function supportsDVH() { try { return CSS.supports("height: 100dvh"); } catch { return false; } } export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { const searchParams = useSearchParams(); const [hide, setHide] = useState(false); const [layout, setLayout] = useState<"chat" | "initial">("initial"); const [value, setValue] = useState(""); const { width } = useViewport(); const [isAiLoading, setIsAiLoading] = useState(false); const { spaces } = useMemory(); // Variable to keep track of the chat history in this session const [chatHistory, setChatHistory] = useState([]); const [toBeParsed, setToBeParsed] = useState(""); const textArea = useRef(null); const main = useRef(null); const [selectedSpaces, setSelectedSpaces] = useState([]); const [isStreaming, setIsStreaming] = useState(false); useEffect(() => { const search = searchParams.get("q"); if (search && search.trim().length > 0) { setValue(search); onSend(); //router.push("/"); } }, []); useEffect(() => { // function onResize() { // if (!main.current || !window.visualViewport) return; // if ( // window.visualViewport.height < window.innerHeight + 20 && // window.visualViewport.height > window.innerHeight - 20 // ) { // setHide(false); // window.scrollTo(0, 0); // } else { // setHide(true); // window.scrollTo(0, document.body.scrollHeight); // } // } // window.visualViewport?.addEventListener("resize", onResize); // return () => { // window.visualViewport?.removeEventListener("resize", onResize); // }; }, []); 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) { // 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) { setChatHistory((prev: ChatHistory[]) => { const lastMessage = prev[prev.length - 1]; const newParts = [ ...lastMessage.answer.parts, { text: parsedPart.response }, ]; return [ ...prev.slice(0, prev.length - 1), { ...lastMessage, answer: { parts: newParts, sources: lastMessage.answer.sources, }, }, ]; }); } else { } } } 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 modifyChatHistory = useCallback((old: ChatHistory[]) => { const final: { role: "user" | "model"; parts: { text: string }[] }[] = []; old.forEach((chat) => { final.push({ role: "user", parts: [{ text: chat.question }], }); final.push({ role: "model", parts: chat.answer.parts.map((part) => ({ text: part.text })), }); }); return final; }, []); const getSearchResults = async () => { setIsAiLoading(true); const _value = value.trim(); setValue(""); setChatHistory((prev) => [ ...prev, { question: _value, answer: { parts: [], sources: [], }, }, ]); const sourcesResponse = await fetch( `/api/chat?sourcesOnly=true&q=${_value}`, { method: "POST", body: JSON.stringify({ chatHistory: modifyChatHistory(chatHistory), }), }, ); console.log("sources", sourcesResponse); const sourcesInJson = getIdsFromSource( ( (await sourcesResponse.json()) as { ids: string[]; } ).ids, ) ?? []; const notesInSources = sourcesInJson.filter((urls) => urls.startsWith("https://notes.supermemory.dhr.wtf/"), ); const nonNotes = sourcesInJson.filter((i) => !notesInSources.includes(i)); const fetchedTitles = await getMemoriesFromUrl(notesInSources); const sources = [ ...nonNotes.map((n) => ({ isNote: false, source: n ?? "" })), ...fetchedTitles.map((n) => ({ isNote: true, source: n.title ?? "", })), ]; setIsAiLoading(false); setChatHistory((prev) => { const lastMessage = prev[prev.length - 1]; return [ ...prev.slice(0, prev.length - 1), { ...lastMessage, answer: { parts: lastMessage.answer.parts, sources, }, }, ]; }); const actualSelectedSpaces = selectedSpaces.map( (space) => spaces.find((s) => s.id === space)?.name ?? "", ); const response = await fetch( `/api/chat?q=${_value}&spaces=${actualSelectedSpaces.join(",")}`, { method: "POST", body: JSON.stringify({ chatHistory: modifyChatHistory(chatHistory), }), }, ); if (response.status !== 200) { setIsAiLoading(false); return; } setIsStreaming(true); 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) { setIsAiLoading(false); setToBeParsed(""); return; } setToBeParsed((prev) => prev + decoder.decode(value)); return reader?.read().then(processText); }); } }; const onSend = () => { if (value.trim().length < 1) return; setLayout("chat"); getSearchResults(); }; function onValueChange(e: React.ChangeEvent) { const value = e.target.value; setValue(value); const lines = countLines(e.target); e.target.rows = Math.min(5, lines); } return ( <> {layout === "chat" ? ( ) : (
Smort logo
{width <= 768 && }

Ask your second brain

{ textArea.current?.querySelector("textarea")?.focus(); }} side="top" align="start" className="mr-auto bg-[#252525] md:hidden" selectedSpaces={selectedSpaces} setSelectedSpaces={setSelectedSpaces} /> { if (e.key === "Enter") { e.preventDefault(); onSend(); } }, }} >
{ textArea.current?.querySelector("textarea")?.focus(); }} className="hidden md:flex" selectedSpaces={selectedSpaces} setSelectedSpaces={setSelectedSpaces} />
setValue(e.target.value), onKeyDown: (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); onSend(); } }, }} className="hidden md:flex" >
{ textArea.current?.querySelector("textarea")?.focus(); }} className="hidden md:flex" selectedSpaces={selectedSpaces} setSelectedSpaces={setSelectedSpaces} />
)} {width <= 768 && }
); } export function Chat({ sidebarOpen, chatHistory, isLoading = false, askQuestion, setValue, value, selectedSpaces, setSelectedSpaces, }: { sidebarOpen: boolean; isLoading?: boolean; chatHistory: ChatHistory[]; askQuestion: () => void; setValue: (value: string) => void; value: string; selectedSpaces: number[]; setSelectedSpaces: React.Dispatch>; }) { const textArea = useRef(null); function onValueChange(e: React.ChangeEvent) { const value = e.target.value; setValue(value); const lines = countLines(e.target); e.target.rows = Math.min(5, lines); } const { width } = useViewport(); return (
{width <= 768 && }
{chatHistory.map((msg, i) => ( {msg.question} {msg.answer.parts .map((part) => part.text) .join("") .replace("", "")} ))}
{ textArea.current?.querySelector("textarea")?.focus(); }} side="top" align="start" className="mr-auto bg-[#252525]" selectedSpaces={selectedSpaces} setSelectedSpaces={setSelectedSpaces} /> { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); askQuestion(); } }, }} >
); }