diff options
| author | Dhravya Shah <[email protected]> | 2025-02-18 15:28:01 -0700 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2025-02-18 15:28:01 -0700 |
| commit | 52d89fd1a6036c00bdc79bf1e0ec0df87760890f (patch) | |
| tree | 0bfeb0af4db20ed2f00ff265770678d73868102f /apps | |
| parent | added a batch delete feature (diff) | |
| download | supermemory-52d89fd1a6036c00bdc79bf1e0ec0df87760890f.tar.xz supermemory-52d89fd1a6036c00bdc79bf1e0ec0df87760890f.zip | |
better space selector
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/backend/src/routes/actions.ts | 18 | ||||
| -rw-r--r-- | apps/web/app/components/memories/MemoriesPage.tsx | 134 | ||||
| -rw-r--r-- | apps/web/app/components/memories/SharedCard.tsx | 29 | ||||
| -rw-r--r-- | apps/web/app/routes/signin.tsx | 8 |
4 files changed, 114 insertions, 75 deletions
diff --git a/apps/backend/src/routes/actions.ts b/apps/backend/src/routes/actions.ts index deba952b..c0801ada 100644 --- a/apps/backend/src/routes/actions.ts +++ b/apps/backend/src/routes/actions.ts @@ -50,9 +50,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() }) ), async (c) => { - const startTime = performance.now(); - console.log("[chat] Starting request"); - const user = c.get("user"); if (!user) { return c.json({ error: "Unauthorized" }, 401); @@ -60,7 +57,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() const { messages, threadId } = await c.req.valid("json"); - console.log("[chat] Converting messages"); const unfilteredCoreMessages = convertToCoreMessages( (messages as Message[]) .filter((m) => m.content.length > 0) @@ -83,7 +79,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() (message) => message.content.length > 0 ); - console.log("[chat] Setting up DB and logger"); const db = database(c.env.HYPERDRIVE.connectionString); const { initLogger, wrapAISDKModel } = await import("braintrust"); @@ -106,8 +101,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() return c.json({ error: "Empty query" }, 400); } - console.log("[chat] Generating embeddings and creating thread"); - const embedStart = performance.now(); // Run embedding generation and thread creation in parallel const [{ data: embedding }, thread] = await Promise.all([ c.env.AI.run("@cf/baai/bge-base-en-v1.5", { text: queryText }), @@ -123,7 +116,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() .returning() : null, ]); - console.log(`[chat] Embedding generation took ${performance.now() - embedStart}ms`); const threadUuid = threadId || thread?.[0].uuid; @@ -131,8 +123,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() return c.json({ error: "Failed to generate embedding" }, 500); } - console.log("[chat] Performing semantic search"); - const searchStart = performance.now(); // Perform semantic search const similarity = sql<number>`1 - (${cosineDistance(chunk.embeddings, embedding[0])})`; @@ -154,7 +144,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() .where(and(eq(documents.userId, user.id), sql`${similarity} > 0.4`)) .orderBy(desc(similarity)) .limit(5); - console.log(`[chat] Semantic search took ${performance.now() - searchStart}ms`); const cleanDocumentsForContext = finalResults.map((d) => ({ title: d.title, @@ -180,8 +169,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() } try { - console.log("[chat] Starting stream generation"); - const streamStart = performance.now(); const data = new StreamData(); // De-duplicate chunks by URL to avoid showing duplicate content const uniqueResults = finalResults.reduce((acc, current) => { @@ -237,8 +224,6 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() ], async onFinish(completion) { try { - console.log("[chat] Stream finished, updating thread"); - const updateStart = performance.now(); if (lastUserMessage) { lastUserMessage.content = typeof lastUserMessage.content === "string" @@ -272,15 +257,12 @@ const actions = new Hono<{ Variables: Variables; Bindings: Env }>() .set({ messages: newMessages }) .where(eq(chatThreads.uuid, threadUuid)); } - console.log(`[chat] Thread update took ${performance.now() - updateStart}ms`); } catch (error) { console.error("Failed to update thread:", error); } }, }); - console.log(`[chat] Stream generation took ${performance.now() - streamStart}ms`); - console.log(`[chat] Total request time: ${performance.now() - startTime}ms`); return result.toDataStreamResponse({ headers: { "Supermemory-Thread-Uuid": threadUuid ?? "", diff --git a/apps/web/app/components/memories/MemoriesPage.tsx b/apps/web/app/components/memories/MemoriesPage.tsx index 8b0dbdc9..da12427e 100644 --- a/apps/web/app/components/memories/MemoriesPage.tsx +++ b/apps/web/app/components/memories/MemoriesPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { createContext, memo, useCallback, useContext, useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { Button } from "../ui/button"; @@ -22,6 +22,14 @@ interface MemoriesPageProps { isSpace?: boolean; } +interface SelectionContextType { + isSelectionMode: boolean; + selectedItems: Set<string>; + toggleSelection: (uuid: string) => void; +} + +const SelectionContext = createContext<SelectionContextType | null>(null); + function MemoriesPage({ showAddButtons = true, isSpace = false }: MemoriesPageProps) { const isHydrated = useHydrated(); const { spaceId } = useParams(); @@ -106,21 +114,23 @@ function MemoriesPage({ showAddButtons = true, isSpace = false }: MemoriesPagePr // Memoize space items transformation const spaceItems = useMemo(() => { if (spaceId) return []; - return spaces.map((space) => ({ - id: space.uuid, - type: "space", - content: space.name, - createdAt: new Date(space.createdAt), - description: null, - ogImage: null, - title: space.name, - url: `/space/${space.uuid}`, - uuid: space.uuid, - updatedAt: null, - raw: null, - userId: space.ownerId, - isSuccessfullyProcessed: true, - })); + return spaces + .filter((space) => space.uuid !== "<HOME>") + .map((space) => ({ + id: space.uuid, + type: "space", + content: space.name, + createdAt: new Date(space.createdAt), + description: null, + ogImage: null, + title: space.name, + url: `/space/${space.uuid}`, + uuid: space.uuid, + updatedAt: null, + raw: null, + userId: space.ownerId, + isSuccessfullyProcessed: true, + })); }, [spaces, spaceId]); // Memoize filtered memories @@ -180,8 +190,29 @@ function MemoriesPage({ showAddButtons = true, isSpace = false }: MemoriesPagePr }; }, [addButtonItem, spaceItems, filteredMemories, selectedVariant, spaceId, isSpace]); - const renderCard = useCallback( - ({ data, index }: { data: Memory; index: number }) => { + const selectionContextValue = useMemo( + () => ({ + isSelectionMode, + selectedItems, + toggleSelection: handleToggleSelection, + }), + [isSelectionMode, selectedItems, handleToggleSelection], + ); + + const MemoizedSharedCard = memo( + ({ + data, + index, + showAddButtons, + isSpace, + }: { + data: Memory; + index: number; + showAddButtons: boolean; + isSpace: boolean; + }) => { + const selection = useContext(SelectionContext); + if (index === 0 && showAddButtons) { return <AddMemory isSpace={isSpace} />; } @@ -191,13 +222,30 @@ function MemoriesPage({ showAddButtons = true, isSpace = false }: MemoriesPagePr return ( <SharedCard data={data} - isSelectionMode={isSelectionMode} - isSelected={selectedItems.has(data.uuid)} - onToggleSelect={() => handleToggleSelection(data.uuid)} + isSelectionMode={selection?.isSelectionMode ?? false} + isSelected={selection?.selectedItems.has(data.uuid) ?? false} + onToggleSelect={() => selection?.toggleSelection(data.uuid)} /> ); }, - [showAddButtons, isSelectionMode, selectedItems, handleToggleSelection], + (prevProps, nextProps) => { + // Custom comparison function for memo + return prevProps.data.uuid === nextProps.data.uuid; + }, + ); + + MemoizedSharedCard.displayName = "MemoizedSharedCard"; + + const renderCard = useCallback( + ({ data, index }: { data: Memory; index: number }) => ( + <MemoizedSharedCard + data={data} + index={index} + showAddButtons={showAddButtons} + isSpace={isSpace} + /> + ), + [showAddButtons, isSpace], ); const handleVariantClick = useCallback((variant: Variant) => { @@ -345,28 +393,30 @@ function MemoriesPage({ showAddButtons = true, isSpace = false }: MemoriesPagePr if (!isHydrated) return null; return ( - <div className="min-h-screen p-2 md:p-4"> - <div className="mb-4"> - {MobileVariantButton} - {MobileVariantMenu} - {DesktopVariantMenu} - </div> + <SelectionContext.Provider value={selectionContextValue}> + <div className="min-h-screen p-2 md:p-4"> + <div className="mb-4"> + {MobileVariantButton} + {MobileVariantMenu} + {DesktopVariantMenu} + </div> - {SelectionControls} - - <Masonry - key={key + "memories"} - id="memories-masonry" - items={items} - // @ts-ignore - render={renderCard} - columnGutter={16} - columnWidth={Math.min(270, window.innerWidth - 32)} - onRender={maybeLoadMore} - /> + {SelectionControls} + + <Masonry + key={key} + id="memories-masonry" + items={items} + // @ts-ignore + render={renderCard} + columnGutter={16} + columnWidth={Math.min(270, window.innerWidth - 32)} + onRender={maybeLoadMore} + /> - {isLoading && <div className="py-4 text-center text-muted-foreground">Loading more...</div>} - </div> + {isLoading && <div className="py-4 text-center text-muted-foreground">Loading more...</div>} + </div> + </SelectionContext.Provider> ); } diff --git a/apps/web/app/components/memories/SharedCard.tsx b/apps/web/app/components/memories/SharedCard.tsx index cebdc796..69283324 100644 --- a/apps/web/app/components/memories/SharedCard.tsx +++ b/apps/web/app/components/memories/SharedCard.tsx @@ -3,7 +3,7 @@ import { memo, useCallback, useEffect, useMemo, useState } from "react"; import { useInView } from "react-intersection-observer"; import { TweetSkeleton } from "react-tweet"; -import { useNavigate } from "@remix-run/react"; +import { useNavigate, useParams } from "@remix-run/react"; import { NotionIcon } from "../icons/IntegrationIcons"; import { CustomTwitterComp } from "../twitter/render-tweet"; @@ -111,6 +111,7 @@ const renderContent = { page: ({ data }: { data: Memory }) => ( <WebsiteCard + id={data.uuid} url={data.url ?? ""} title={data.title} description={data.description} @@ -219,7 +220,7 @@ const renderContent = { space: ({ data }: { data: Memory & Partial<ExtraSpaceMetaData> }) => { return ( <a - href={`${data.url}`} + href={data.url ?? ""} className="flex flex-col gap-2 p-6 bg-white dark:bg-neutral-800 border border-gray-200 dark:border-gray-800 rounded-3xl" > <div className="flex items-center gap-2 text-gray-600 dark:text-gray-300"> @@ -325,7 +326,7 @@ const renderContent = { // TODO: This can be improved return ( <a - href={data.url ?? ""} + href={`/content/${data.id}`} className="block p-4 rounded-3xl border border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" > <div className="flex items-center gap-3 text-gray-600 dark:text-gray-300"> @@ -411,11 +412,13 @@ const WebsiteCard = memo( title, description, image, + id, }: { url: string; title?: string | null; description?: string | null; image?: string | null; + id: string; }) => { // Memoize domain extraction to avoid recalculation const domain = useMemo(() => { @@ -500,9 +503,7 @@ const WebsiteCard = memo( <h3 className="text-lg font-semibold tracking-tight">{displayTitle}</h3> <p className="mt-2 line-clamp-2 text-sm opacity-80">{displayDescription}</p> <a - href={url} - target="_blank" - rel="noopener noreferrer" + href={`/content/${id}`} className="mt-3 inline-flex items-center gap-1 text-sm hover:underline opacity-70 hover:opacity-100 transition-opacity" style={{ color: isDark ? "white" : "black", @@ -686,6 +687,7 @@ export default function SharedCard({ onSuccess: () => { toast.success("Memory deleted successfully"); queryClient.invalidateQueries({ queryKey: ["memories"] }); + queryClient.invalidateQueries({ queryKey: ["spaces"] }); }, }); @@ -742,11 +744,6 @@ export default function SharedCard({ onToggleSelect(); return; } - - // Normal navigation behavior - if (data.url) { - window.location.href = data.url; - } }; return ( @@ -809,6 +806,10 @@ export const SpaceSelector = function SpaceSelector({ onSelect: (spaceId: string) => void; }) { const [search, setSearch] = useState(""); + const { spaceId } = useParams(); + + console.log(spaceId); + const { data: spacesData, isLoading, @@ -821,11 +822,13 @@ export const SpaceSelector = function SpaceSelector({ const filteredSpaces = useMemo(() => { if (!spacesData?.spaces) return []; - return spacesData.spaces.filter((space) => - space.name.toLowerCase().includes(search.toLowerCase()), + return spacesData.spaces.filter( + (space) => + space.name.toLowerCase().includes(search.toLowerCase()) && space.uuid !== (spaceId ? spaceId.split("---")[0] : "<HOME>"), ); }, [spacesData?.spaces, search]); + if (isLoading) { return ( <DropdownMenuSubContent> diff --git a/apps/web/app/routes/signin.tsx b/apps/web/app/routes/signin.tsx index 93fa792f..0ed0a848 100644 --- a/apps/web/app/routes/signin.tsx +++ b/apps/web/app/routes/signin.tsx @@ -1,8 +1,12 @@ import { LoaderFunctionArgs, redirect } from "@remix-run/cloudflare"; - import { getSignInUrl } from "@supermemory/authkit-remix-cloudflare"; +import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session"; -export async function loader({ context }: LoaderFunctionArgs) { +export async function loader({ request, context }: LoaderFunctionArgs) { + const session = await getSessionFromRequest(request, context); + if (session) { + return redirect("/"); + } const signinUrl = await getSignInUrl(context); return redirect(signinUrl); } |