diff --git a/apps/web/src/app/api/query/route.ts b/apps/web/src/app/api/query/route.ts index 4e2f0674..28f441bc 100644 --- a/apps/web/src/app/api/query/route.ts +++ b/apps/web/src/app/api/query/route.ts @@ -21,7 +21,7 @@ export async function GET(req: NextRequest) { return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 }); } - const session = {session: sessionData[0], user: user[0]} + const session = { session: sessionData[0], user: user[0] } const query = new URL(req.url).searchParams.get("q"); const sourcesOnly = new URL(req.url).searchParams.get("sourcesOnly") ?? "false"; @@ -36,8 +36,11 @@ export async function GET(req: NextRequest) { } }) + console.log(resp.status) + if (resp.status !== 200 || !resp.ok) { const errorData = await resp.json(); + console.log(errorData) return new Response(JSON.stringify({ message: "Error in CF function", error: errorData }), { status: resp.status }); } diff --git a/apps/web/src/app/content.tsx b/apps/web/src/app/content.tsx index 8bfebcb9..39f2948d 100644 --- a/apps/web/src/app/content.tsx +++ b/apps/web/src/app/content.tsx @@ -1,15 +1,18 @@ -"use client"; -import Main from "@/components/Main"; -import Sidebar from "@/components/Sidebar/index"; -import { useState } from "react"; +'use client'; +import Main from '@/components/Main'; +import Sidebar from '@/components/Sidebar/index'; +import { SessionProvider } from 'next-auth/react'; +import { useState } from 'react'; export default function Content() { const [selectedItem, setSelectedItem] = useState(null); return ( -
- -
-
+ +
+ +
+
+
); } diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index 0bfd76a5..86679dcf 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -1,16 +1,17 @@ -"use client"; -import { useEffect, useRef, useState } from "react"; -import { FilterCombobox } from "./Sidebar/FilterCombobox"; -import { Textarea2 } from "./ui/textarea"; -import { ArrowRight } from "lucide-react"; -import { MemoryDrawer } from "./MemoryDrawer"; -import useViewport from "@/hooks/useViewport"; -import { motion } from "framer-motion"; -import { cn } from "@/lib/utils"; +'use client'; +import { useEffect, useRef, useState } from 'react'; +import { FilterCombobox } from './Sidebar/FilterCombobox'; +import { Textarea2 } from './ui/textarea'; +import { ArrowRight } from 'lucide-react'; +import { MemoryDrawer } from './MemoryDrawer'; +import useViewport from '@/hooks/useViewport'; +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import SearchResults from './SearchResults'; function supportsDVH() { try { - return CSS.supports("height: 100dvh"); + return CSS.supports('height: 100dvh'); } catch { return false; } @@ -18,8 +19,13 @@ function supportsDVH() { export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { const [hide, setHide] = useState(false); - const [value, setValue] = useState(""); + const [value, setValue] = useState(''); const { width } = useViewport(); + const [searchResults, setSearchResults] = useState([]); + const [isAiLoading, setIsAiLoading] = useState(false); + + const [aiResponse, setAIResponse] = useState(''); + const [toBeParsed, setToBeParsed] = useState(''); const textArea = useRef(null); const main = useRef(null); @@ -39,46 +45,145 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { } } - window.visualViewport?.addEventListener("resize", onResize); + window.visualViewport?.addEventListener('resize', onResize); return () => { - window.visualViewport?.removeEventListener("resize", onResize); + window.visualViewport?.removeEventListener('resize', onResize); }; }, []); + 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) => { + e.preventDefault(); + setIsAiLoading(true); + + const sourcesResponse = await fetch( + `/api/query?sourcesOnly=true&q=${value}`, + ); + + const sourcesInJson = (await sourcesResponse.json()) as { + ids: string[]; + }; + + setSearchResults(sourcesInJson.ids); + + const response = await fetch(`/api/query?q=${value}`); + + 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 (

Ask your Second brain

- setValue(e.target.value), - }} - > -
- - -
-
+
await getSearchResults(e)}> + setValue(e.target.value), + }} + > +
+ + +
+
+
+ {searchResults && ( + + )} {width <= 768 && }
); diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index 8effffbd..830b0f05 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -1,13 +1,12 @@ -"use client"; -import { StoredContent } from "@/server/db/schema"; -import { MemoryIcon } from "../../assets/Memories"; -import { Trash2, User2 } from "lucide-react"; -import React, { ElementType, useEffect, useState } from "react"; -import { MemoriesBar } from "./MemoriesBar"; -import { AnimatePresence, motion } from "framer-motion"; -import { Bin } from "@/assets/Bin"; -import { CollectedSpaces } from "../../../types/memory"; -import { useMemory } from "@/contexts/MemoryContext"; +'use client'; +import { MemoryIcon } from '../../assets/Memories'; +import { Trash2, User2 } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import { MemoriesBar } from './MemoriesBar'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Bin } from '@/assets/Bin'; +import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar'; +import { useSession } from 'next-auth/react'; export type MenuItem = { icon: React.ReactNode | React.ReactNode[]; @@ -15,29 +14,48 @@ export type MenuItem = { content?: React.ReactNode; }; -const menuItemsBottom: Array = [ - { - icon: , - label: "Trash", - }, - { - icon: , - label: "Profile", - }, -]; - export default function Sidebar({ selectChange, }: { selectChange?: (selectedItem: string | null) => void; }) { + const { data: session } = useSession(); const menuItemsTop: Array = [ { icon: , - label: "Memories", + label: 'Memories', content: , }, ]; + + const menuItemsBottom: Array = [ + { + icon: , + label: 'Trash', + }, + { + icon: ( +
+ + {session?.user?.image ? ( + + ) : ( + + )} + + {session?.user?.name?.split(' ').map((n) => n[0])}{' '} + + +
+ ), + label: 'Profile', + }, + ]; + const menuItems = [...menuItemsTop, ...menuItemsBottom]; const [selectedItem, setSelectedItem] = useState(null); @@ -55,7 +73,7 @@ export default function Sidebar({
, content: , }} @@ -67,7 +85,7 @@ export default function Sidebar({ , }} selectedItem={selectedItem} @@ -76,8 +94,25 @@ export default function Sidebar({ /> , + label: 'Profile', + icon: ( +
+ + {session?.user?.image ? ( + + ) : ( + + )} + + {session?.user?.name?.split(' ').map((n) => n[0])}{' '} + + +
+ ), }} selectedItem={selectedItem} setSelectedItem={setSelectedItem} @@ -115,11 +150,11 @@ const MenuItem = ({ export function SubSidebar({ children }: { children?: React.ReactNode }) { return (