better space selector

This commit is contained in:
Dhravya Shah 2025-02-18 15:28:01 -07:00
parent 747306979a
commit 52d89fd1a6
4 changed files with 115 additions and 76 deletions

View file

@ -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 ?? "",

View file

@ -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}
<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}
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>
{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}
/>
{isLoading && <div className="py-4 text-center text-muted-foreground">Loading more...</div>}
</div>
</SelectionContext.Provider>
);
}

View file

@ -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>

View file

@ -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);
}