mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 07:42:43 +00:00
better space selector
This commit is contained in:
parent
747306979a
commit
52d89fd1a6
4 changed files with 115 additions and 76 deletions
|
|
@ -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 ?? "",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue