aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-02-18 15:28:01 -0700
committerDhravya Shah <[email protected]>2025-02-18 15:28:01 -0700
commit52d89fd1a6036c00bdc79bf1e0ec0df87760890f (patch)
tree0bfeb0af4db20ed2f00ff265770678d73868102f
parentadded a batch delete feature (diff)
downloadsupermemory-52d89fd1a6036c00bdc79bf1e0ec0df87760890f.tar.xz
supermemory-52d89fd1a6036c00bdc79bf1e0ec0df87760890f.zip
better space selector
-rw-r--r--apps/backend/src/routes/actions.ts18
-rw-r--r--apps/web/app/components/memories/MemoriesPage.tsx134
-rw-r--r--apps/web/app/components/memories/SharedCard.tsx29
-rw-r--r--apps/web/app/routes/signin.tsx8
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);
}