This commit is contained in:
Dhravya Shah 2025-01-30 21:18:18 -07:00
parent 3630d584fa
commit 05434b1c57
29 changed files with 300 additions and 313 deletions

View file

@ -136,7 +136,7 @@ export const auth = async (
// Check if request requires authentication
const isPublicSpaceRequest =
c.req.url.includes("/api/spaces/") || c.req.url.includes("/api/memories");
c.req.url.includes("/v1/spaces/") || c.req.url.includes("/v1/memories");
if (!isPublicSpaceRequest && !c.get("user")) {
console.log("Unauthorized access to", c.req.url);

View file

@ -10,7 +10,7 @@ import { waitlist } from "@supermemory/db/schema";
import { cors } from "hono/cors";
import { ContentWorkflow } from "./workflow";
import { Resend } from "resend";
import { StatusCode } from "hono/utils/http-status";
import { RedirectStatusCode, StatusCode } from "hono/utils/http-status";
import { LandingPage } from "./components/landing";
import user from "./routes/user";
import spacesRoute from "./routes/spaces";
@ -44,32 +44,29 @@ export const app = new Hono<{ Variables: Variables; Bindings: Env }>()
exposeHeaders: ["*"],
})
)
.use("/api/*", auth)
.use("/api/*", (c, next) => {
.use("/v1/*", auth)
.use("/v1/*", (c, next) => {
const user = c.get("user");
// RATELIMITS
const rateLimitConfig = {
// Endpoints that bypass rate limiting
excludedPaths: [
"/api/add",
"/api/chat",
"/api/suggested-learnings",
"/api/recommended-questions",
"/v1/add",
"/v1/chat",
"/v1/suggested-learnings",
"/v1/recommended-questions",
] as (string | RegExp)[],
// Custom rate limits for specific endpoints
customLimits: {
notionImport: {
paths: [
"/api/integrations/notion/import",
"/api/integrations/notion",
],
paths: ["/v1/integrations/notion/import", "/v1/integrations/notion"],
windowMs: 10 * 60 * 1000, // 10 minutes
limit: 5, // 5 requests per 10 minutes
},
inviteSpace: {
paths: [/^\/api\/spaces\/[^/]+\/invite$/],
paths: [/^\v1\/spaces\/[^/]+\/invite$/],
windowMs: 60 * 1000, // 1 minute
limit: 5, // 5 requests per minute
},
@ -81,7 +78,6 @@ export const app = new Hono<{ Variables: Variables; Bindings: Env }>()
default: {
windowMs: 60 * 1000, // 1 minute
limit: 100, // 100 requests per minute
},
common: {
@ -128,11 +124,32 @@ export const app = new Hono<{ Variables: Variables; Bindings: Env }>()
.get("/", (c) => {
return c.html(<LandingPage />);
})
.route("/api/user", user)
.route("/api/spaces", spacesRoute)
.route("/api", actions)
.route("/api/integrations", integrations)
.route("/api/memories", memories)
// TEMPORARY REDIRECT
.all("/api/*", async (c) => {
// Get the full URL and path
const url = new URL(c.req.url);
const path = url.pathname.replace("/api", "/v1");
// Preserve query parameters and build target URL
const redirectUrl = path + url.search;
// Forward the request with same method, headers and body
const response = await fetch(redirectUrl, {
method: c.req.method,
headers: c.req.raw.headers,
body:
c.req.method !== "GET" && c.req.method !== "HEAD"
? await c.req.blob()
: undefined,
});
return response;
})
.route("/v1/user", user)
.route("/v1/spaces", spacesRoute)
.route("/v1", actions)
.route("/v1/integrations", integrations)
.route("/v1/memories", memories)
.post(
"/waitlist",
zValidator(

View file

@ -472,7 +472,7 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
});
}
).post(
"/api/space/invites/:action",
"/invites/:action",
zValidator(
"json",
z.object({

View file

@ -2,7 +2,6 @@
title: "Product Updates"
description: "New updates and improvements"
mode: "center"
icon: "rocket"
---
<Update label="2024-10-12" description="v0.1.1">

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

View file

@ -1,86 +1,34 @@
---
title: 'Quickstart'
description: 'Start building awesome documentation in under 5 minutes'
title: 'Getting Started'
description: 'Start using Supermemory API in under 5 minutes'
---
## Setup your development
## Getting Started
Learn how to update your docs locally and deploy them to the public.
To use the Supermemory API, you'll need:
### Edit and preview
1. An API key (get one by signing up at [supermemory.ai](https://supermemory.ai))
2. Basic understanding of REST APIs
3. A tool to make HTTP requests (like curl, Postman, or your favorite programming language)
<AccordionGroup>
<Accordion icon="github" title="Clone your docs locally">
During the onboarding process, we created a repository on your Github with
your docs content. You can find this repository on our
[dashboard](https://dashboard.mintlify.com). To clone the repository
locally, follow these
[instructions](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository)
in your terminal.
</Accordion>
<Accordion icon="rectangle-terminal" title="Preview changes">
Previewing helps you make sure your changes look as intended. We built a
command line interface to render these changes locally. 1. Install the
[Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the
documentation changes locally with this command: ``` npm i -g mintlify ```
2. Run the following command at the root of your documentation (where
`mint.json` is): ``` mintlify dev ```
<Accordion icon="key" title="Getting Your API Key">
1. Login into [supermemory.ai](https://supermemory.ai) and click on the "Add Memory" button
![Login to Supermemory](/images/setup/1.png)
2. Click on "Integrations" in the navigation menu
![Navigate to Add Memory](/images/setup/2.png)
3. You'll see your API key, you can copy it by clicking on the copy button
![View API Key in Integrations](/images/setup/3.png)
Keep your API key secure and never share it publicly. You'll need this key for authenticating all API requests.
</Accordion>
</AccordionGroup>
### Deploy your changes
## Base URL
<AccordionGroup>
<Accordion icon="message-bot" title="Install our Github app">
Our Github app automatically deploys your changes to your docs site, so you
don't need to manage deployments yourself. You can find the link to install on
your [dashboard](https://dashboard.mintlify.com). Once the bot has been
successfully installed, there should be a check mark next to the commit hash
of the repo.
</Accordion>
<Accordion icon="rocket" title="Push your changes">
[Commit and push your changes to
Git](https://docs.github.com/en/get-started/using-git/pushing-commits-to-a-remote-repository#about-git-push)
for your changes to update in your docs site. If you push and don't see that
the Github app successfully deployed your changes, you can also manually
update your docs through our [dashboard](https://dashboard.mintlify.com).
</Accordion>
</AccordionGroup>
## Update your docs
Add content directly in your files with MDX syntax and React components. You can use any of our components, or even build your own.
<CardGroup>
<Card title="Style Your Docs" icon="paintbrush" href="/settings/global">
Add flair to your docs with personalized branding.
</Card>
<Card
title="Add API Endpoints"
icon="square-code"
href="/api-playground/configuration"
>
Implement your OpenAPI spec and enable API user interaction.
</Card>
<Card
title="Integrate Analytics"
icon="chart-mixed"
href="/analytics/supported-integrations"
>
Draw insights from user interactions with your documentation.
</Card>
<Card
title="Host on a Custom Domain"
icon="browser"
href="/settings/custom-domain/subdomain"
>
Keep your docs on your own website's subdomain.
</Card>
</CardGroup>
All API requests should be made to:

View file

@ -14,7 +14,7 @@ const tabStates = new Map<number, TabState>();
const checkIfLoggedIn = async () => {
const baseURL = await getBaseURL();
const response = await fetch(`${baseURL}/api/session`);
const response = await fetch(`${baseURL}/v1/session`);
return response.status == 200;
};
@ -88,7 +88,7 @@ registerMessageHandler<MessageType>(
async (message, sender, sendResponse) => {
// Handle getting spaces
const baseURL = await getBaseURL();
const response = await fetch(`${baseURL}/backend/api/spaces`);
const response = await fetch(`${baseURL}/backend/v1/spaces`);
const data = await response.json();
sendResponse(data);
}
@ -121,7 +121,7 @@ registerMessageHandler<MessageType>(
console.log(message.payload);
const response = await fetch(`${baseURL}/backend/api/add`, {
const response = await fetch(`${baseURL}/backend/v1/add`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -200,7 +200,7 @@ chrome.runtime.onMessageExternal.addListener(
chrome.bookmarks.getRecent(100, async (bookmarks) => {
for (const { url } of bookmarks) {
console.log("Importing bookmark:", url);
const r = await fetch(`${baseURL}/backend/api/add`, {
const r = await fetch(`${baseURL}/backend/v1/add`, {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -241,7 +241,7 @@ const getBookmarks = async (cursor = "", totalImported = 0, allTweets = []) => {
// Send all tweets in parallel
const addRequests = tweetUrls.map((tweetUrl: string) =>
fetch(`${baseURL}/backend/api/add`, {
fetch(`${baseURL}/backend/v1/add`, {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -51,7 +51,7 @@ async function createSpace(data: {
isPublic: boolean;
}): Promise<CreateSpaceResponse> {
const baseURL = await getBaseURL();
const response = await fetch(`${baseURL}/backend/api/space/create`, {
const response = await fetch(`${baseURL}/backend/v1/space/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -72,7 +72,7 @@ async function createSpace(data: {
async function makeFavorite(spaceId: string) {
const baseURL = await getBaseURL();
const response = await fetch(
`${baseURL}/backend/api/space/favorite/${spaceId}`,
`${baseURL}/backend/v1/space/favorite/${spaceId}`,
{
method: "POST",
}

View file

@ -115,7 +115,7 @@ function Reminders() {
const navigate = useNavigate();
useEffect(() => {
fetch(`/backend/api/suggested-learnings`, {
fetch(`/backend/v1/suggested-learnings`, {
credentials: "include",
})
.then((res) => res.json() as Promise<{ suggestedLearnings: Array<Record<string, string>> }>)

View file

@ -99,7 +99,7 @@ export function CSVUploadModal({ isOpen, onClose }: CSVUploadModalProps) {
setIsUploading(true);
try {
const response = await fetch("/backend/api/batch-add", {
const response = await fetch("/backend/v1/batch-add", {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -183,7 +183,7 @@ function Integrations() {
};
const getApiKey = async () => {
const response = await fetch(`/backend/api/user/key`, {
const response = await fetch(`/backend/v1/user/key`, {
credentials: "include",
});
if (response.ok) {
@ -503,7 +503,7 @@ export function IntegrationModals({ integrationId }: { integrationId: string })
let toastId: string | number | undefined;
try {
const eventSource = new EventSource(`/backend/api/integrations/notion/import`, {
const eventSource = new EventSource(`/backend/v1/integrations/notion/import`, {
withCredentials: true,
});

View file

@ -71,7 +71,7 @@ export function MarkdownUploadModal({ isOpen, onClose }: MarkdownUploadModalProp
);
// Send to batch endpoint
const response = await fetch("/backend/api/batch-add", {
const response = await fetch("/backend/v1/batch-add", {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -1,13 +1,24 @@
import * as ReactTweet from "react-tweet";
import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { useInView } from "react-intersection-observer";
import { TweetSkeleton } from "react-tweet";
import { NotionIcon } from "../icons/IntegrationIcons";
import { CustomTwitterComp } from "../twitter/render-tweet";
import Loader from "../ui/Loader";
import { Button, ButtonProps } from "../ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../ui/command";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
@ -16,24 +27,20 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
DropdownMenuGroup,
} from "../ui/dropdown-menu";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../ui/command";
import { FileIcon } from "@radix-ui/react-icons";
import { SpaceIcon } from "@supermemory/shared/icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { FastAverageColor } from "fast-average-color";
import { MenuIcon, TrashIcon } from "lucide-react";
import { toast } from "sonner";
import { pastelColors } from "~/lib/constants/pastelColors";
import { typeIcons } from "~/lib/constants/typeIcons";
import { ExtraSpaceMetaData, fetchSpaces } from "~/lib/hooks/use-spaces";
import { useTextOverflow } from "~/lib/hooks/use-text-overflow";
import { Memory, WebsiteMetadata } from "~/lib/types/memory";
import { cn } from "~/lib/utils";
import Loader from "../ui/Loader";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { TweetSkeleton } from "react-tweet";
const { useTweet } = ReactTweet;
@ -393,119 +400,125 @@ async function fetchWebsiteMetadata(url: string): Promise<WebsiteMetadata> {
}
}
const WebsiteCard = memo(({
url,
title,
description,
image,
}: {
url: string;
title?: string | null;
description?: string | null;
image?: string | null;
}) => {
// Memoize domain extraction to avoid recalculation
const domain = useMemo(() => {
try {
let formattedUrl = url;
if (!formattedUrl.startsWith("http")) {
formattedUrl = "http://" + formattedUrl;
const WebsiteCard = memo(
({
url,
title,
description,
image,
}: {
url: string;
title?: string | null;
description?: string | null;
image?: string | null;
}) => {
// Memoize domain extraction to avoid recalculation
const domain = useMemo(() => {
try {
let formattedUrl = url;
if (!formattedUrl.startsWith("http")) {
formattedUrl = "http://" + formattedUrl;
}
return new URL(formattedUrl).hostname.replace(/^www\./, "");
} catch {
return url;
}
return new URL(formattedUrl).hostname.replace(/^www\./, "");
} catch {
return url;
}
}, [url]);
}, [url]);
// Memoize initial color based on URL
const initialColor = useMemo(() => {
if (!image) {
const hash = url.split("").reduce((acc, char) => {
return char.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
return `hsl(${hash % 360}, 70%, 85%)`; // Slightly lighter base color
}
return "#f0f0f0"; // Lighter default color
}, [url, image]);
// Memoize initial color based on URL
const initialColor = useMemo(() => {
if (!image) {
const hash = url.split("").reduce((acc, char) => {
return char.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
return `hsl(${hash % 360}, 70%, 85%)`; // Slightly lighter base color
}
return "#f0f0f0"; // Lighter default color
}, [url, image]);
const [dominantColor, setDominantColor] = useState(initialColor);
const [isDark, setIsDark] = useState(false);
// Only calculate dominant color when component is in view
useEffect(() => {
if (image) {
const fac = new FastAverageColor();
fac.getColorAsync(image, {
algorithm: "dominant",
crossOrigin: "anonymous",
mode: "speed",
})
.then((color) => {
setDominantColor(color.hex);
setIsDark(color.isDark);
})
.catch((error) => {
console.error("Error getting dominant color:", error);
});
}
}, [image]);
const [dominantColor, setDominantColor] = useState(initialColor);
const [isDark, setIsDark] = useState(false);
// Only calculate dominant color when component is in view
useEffect(() => {
if (image) {
const fac = new FastAverageColor();
fac
.getColorAsync(image, {
algorithm: "dominant",
crossOrigin: "anonymous",
mode: "speed",
})
.then((color) => {
setDominantColor(color.hex);
setIsDark(color.isDark);
})
.catch((error) => {
console.error("Error getting dominant color:", error);
});
}
}, [image]);
const displayTitle = title || domain;
const displayDescription = description || `Saved from ${domain}`;
const displayTitle = title || domain;
const displayDescription = description || `Saved from ${domain}`;
return (
<div className="overflow-hidden rounded-xl shadow-sm hover:shadow-md transition-shadow duration-200">
<div className="relative">
{image && (
<div className="relative h-40">
<img
src={image}
alt={displayTitle}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
style={{ backgroundColor: dominantColor }}
/>
<div
className="absolute inset-0"
style={{
background: `linear-gradient(to bottom, transparent 40%, ${dominantColor} 100%)`,
}}
/>
</div>
)}
<div
className={cn(
"p-5 relative",
!image && "rounded-lg",
isDark ? "text-white/90" : "text-black/90",
return (
<div className="overflow-hidden rounded-xl shadow-sm hover:shadow-md transition-shadow duration-200">
<div className="relative">
{image && (
<div className="relative h-40">
<img
src={image}
alt={displayTitle}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
style={{ backgroundColor: dominantColor }}
/>
<div
className="absolute inset-0"
style={{
background: `linear-gradient(to bottom, transparent 40%, ${dominantColor} 100%)`,
}}
/>
</div>
)}
style={{
backgroundColor: dominantColor,
marginTop: image ? "-2.5rem" : 0,
}}
>
<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"
className="mt-3 inline-flex items-center gap-1 text-sm hover:underline opacity-70 hover:opacity-100 transition-opacity"
<div
className={cn(
"p-5 relative",
!image && "rounded-lg",
isDark ? "text-white/90" : "text-black/90",
)}
style={{
color: isDark ? "white" : "black",
backgroundColor: dominantColor,
marginTop: image ? "-2.5rem" : 0,
}}
>
<span>{domain}</span>
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</a>
<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"
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",
}}
>
<span>{domain}</span>
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</a>
</div>
</div>
</div>
</div>
);
});
);
},
);
export function FetchAndRenderContent({ content }: { content: string }) {
const [memory, setMemory] = useState<Memory | null>(null);
@ -601,71 +614,70 @@ export function FetchAndRenderContent({ content }: { content: string }) {
return memory ? <SharedCard data={memory} /> : null;
}
function SharedCard({ data }: { data: Memory }) {
const queryClient = useQueryClient();
// Delete mutation
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/backend/api/memories/${id}`, {
method: 'DELETE',
credentials: 'include'
const response = await fetch(`/backend/v1/memories/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!response.ok) {
throw new Error('Failed to delete memory');
throw new Error("Failed to delete memory");
}
return response.json();
},
onMutate: async (id) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['memories'] });
await queryClient.cancelQueries({ queryKey: ["memories"] });
// Snapshot the previous value
const previousMemories = queryClient.getQueryData(['memories']);
const previousMemories = queryClient.getQueryData(["memories"]);
// Optimistically remove the memory
queryClient.setQueryData(['memories'], (old: any) => {
queryClient.setQueryData(["memories"], (old: any) => {
return old?.filter((memory: Memory) => memory.id !== id);
});
return { previousMemories };
},
onError: (err, variables, context) => {
// Revert the optimistic update
queryClient.setQueryData(['memories'], context?.previousMemories);
toast.error('Failed to delete memory');
queryClient.setQueryData(["memories"], context?.previousMemories);
toast.error("Failed to delete memory");
},
onSuccess: () => {
toast.success('Memory deleted successfully');
queryClient.invalidateQueries({ queryKey: ['memories'] });
}
toast.success("Memory deleted successfully");
queryClient.invalidateQueries({ queryKey: ["memories"] });
},
});
// Move to space mutation
const moveToSpaceMutation = useMutation({
mutationFn: async ({ spaceId, documentId }: { spaceId: string, documentId: string }) => {
const response = await fetch('/backend/api/spaces/addContent', {
method: 'POST',
mutationFn: async ({ spaceId, documentId }: { spaceId: string; documentId: string }) => {
const response = await fetch("/backend/v1/spaces/addContent", {
method: "POST",
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json",
},
credentials: 'include',
body: JSON.stringify({ spaceId, documentId })
credentials: "include",
body: JSON.stringify({ spaceId, documentId }),
});
if (!response.ok) {
throw new Error('Failed to move memory');
throw new Error("Failed to move memory");
}
return response.json();
},
onError: (err) => {
toast.error('Failed to move memory to space');
toast.error("Failed to move memory to space");
},
onSuccess: () => {
toast.success('Memory moved successfully');
queryClient.invalidateQueries({ queryKey: ['memories'] });
queryClient.invalidateQueries({ queryKey: ['spaces'] });
}
toast.success("Memory moved successfully");
queryClient.invalidateQueries({ queryKey: ["memories"] });
queryClient.invalidateQueries({ queryKey: ["spaces"] });
},
});
// Flatten the data if it's a nested array and get the first item
@ -694,7 +706,7 @@ function SharedCard({ data }: { data: Memory }) {
const handleMoveToSpace = (spaceId: string) => {
moveToSpaceMutation.mutate({
spaceId,
documentId: data.uuid
documentId: data.uuid,
});
};
@ -729,7 +741,7 @@ function SharedCard({ data }: { data: Memory }) {
<MemoizedSpaceSelector contentId={data.id} onSelect={handleMoveToSpace} />
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem onSelect={e => handleDelete(e)} asChild>
<DropdownMenuItem onSelect={(e) => handleDelete(e)} asChild>
<Button className="w-full gap-2 flex" variant={"destructive"}>
<TrashIcon className="h-4 w-4" /> Delete
</Button>
@ -743,24 +755,28 @@ function SharedCard({ data }: { data: Memory }) {
);
}
export const SpaceSelector = function SpaceSelector({
contentId,
onSelect
}: {
export const SpaceSelector = function SpaceSelector({
contentId,
onSelect,
}: {
contentId: number;
onSelect: (spaceId: string) => void;
}) {
const [search, setSearch] = useState("");
const { data: spacesData, isLoading, error } = useQuery({
queryKey: ['spaces'],
const {
data: spacesData,
isLoading,
error,
} = useQuery({
queryKey: ["spaces"],
queryFn: fetchSpaces,
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
});
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()),
);
}, [spacesData?.spaces, search]);
@ -781,7 +797,9 @@ export const SpaceSelector = function SpaceSelector({
return (
<DropdownMenuSubContent>
<DropdownMenuItem disabled>
<div className="text-destructive">Error: {error instanceof Error ? error.message : 'Failed to load spaces'}</div>
<div className="text-destructive">
Error: {error instanceof Error ? error.message : "Failed to load spaces"}
</div>
</DropdownMenuItem>
</DropdownMenuSubContent>
);
@ -790,16 +808,12 @@ export const SpaceSelector = function SpaceSelector({
return (
<DropdownMenuSubContent className="p-0">
<Command>
<CommandInput
placeholder="Search spaces..."
value={search}
onValueChange={setSearch}
/>
<CommandInput placeholder="Search spaces..." value={search} onValueChange={setSearch} />
<CommandList className="max-h-[200px] overflow-y-auto">
<CommandGroup>
{filteredSpaces.map((space) => (
<CommandItem
key={space.uuid}
<CommandItem
key={space.uuid}
value={space.name}
onSelect={() => onSelect(space.uuid)}
>
@ -809,9 +823,7 @@ export const SpaceSelector = function SpaceSelector({
</div>
</CommandItem>
))}
{filteredSpaces.length === 0 && (
<CommandEmpty>No spaces found.</CommandEmpty>
)}
{filteredSpaces.length === 0 && <CommandEmpty>No spaces found.</CommandEmpty>}
</CommandGroup>
</CommandList>
</Command>
@ -821,4 +833,4 @@ export const SpaceSelector = function SpaceSelector({
const MemoizedSpaceSelector = memo(SpaceSelector);
export default SharedCard;
export default SharedCard;

View file

@ -62,7 +62,7 @@ export const getIntegrations = (
},
importData: {
url: `/backend/api/integrations/notion/import`,
url: `/backend/v1/integrations/notion/import`,
withCredentials: true,
parseProgress: (data) => ({
progress: data.progress,
@ -193,7 +193,11 @@ export const getIntegrations = (
},
icon: (props) => (
<img src="https://imagedelivery.net/_Zs8NCbSWCQ8-iurXrWjBg/21434e3e-49ac-4a15-126d-175e95440300/public" alt="iOS Shortcut" {...props} />
<img
src="https://imagedelivery.net/_Zs8NCbSWCQ8-iurXrWjBg/21434e3e-49ac-4a15-126d-175e95440300/public"
alt="iOS Shortcut"
{...props}
/>
),
},
});

View file

@ -10,7 +10,7 @@ export const useChatStream = (initialMessages: CoreMessage[], initialThreadUuid?
const [threadUuid, setThreadUuid] = useState(initialThreadUuid || "");
const { messages, input, setInput, append, isLoading, error, stop, handleSubmit } = useChat({
initialMessages: convertToUIMessages(initialMessages),
api: `/backend/api/chat`,
api: `/backend/v1/chat`,
onResponse: (resp) => {
const newThreadUuid = resp.headers.get("Supermemory-Thread-Uuid");
if (newThreadUuid) {

View file

@ -27,7 +27,7 @@ export function useMemories(start = 0, count = 40, spaceId?: string) {
const { data: memoriesData, isLoading: isInitialLoading } = useQuery<CachedMemories>({
queryKey: cacheKey,
queryFn: async () => {
const url = new URL(`/backend/api/memories`, window.location.origin);
const url = new URL(`/backend/v1/memories`, window.location.origin);
url.searchParams.set("start", "0");
url.searchParams.set("count", count.toString());
if (spaceId) url.searchParams.set("spaceId", spaceId);
@ -54,7 +54,7 @@ export function useMemories(start = 0, count = 40, spaceId?: string) {
return null;
}
const url = new URL(`/backend/api/memories`, window.location.origin);
const url = new URL(`/backend/v1/memories`, window.location.origin);
url.searchParams.set("start", memoriesData.nextCursor.toString());
url.searchParams.set("count", count.toString());
if (spaceId) url.searchParams.set("spaceId", spaceId);
@ -94,7 +94,7 @@ export function useMemories(start = 0, count = 40, spaceId?: string) {
const deleteMemory = useMutation({
mutationFn: async (memoryId: string) => {
const response = await fetch(`/backend/api/memories/${memoryId}`, {
const response = await fetch(`/backend/v1/memories/${memoryId}`, {
method: "DELETE",
credentials: "include",
});
@ -148,7 +148,7 @@ export function useMemories(start = 0, count = 40, spaceId?: string) {
const toastId = toast.loading("Adding content to your second brain...");
try {
const response = await fetch(`/backend/api/add`, {
const response = await fetch(`/backend/v1/add`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, spaces }),
@ -175,7 +175,7 @@ export function useMemories(start = 0, count = 40, spaceId?: string) {
toast.loading("Content queued for processing...", { id: toastId });
const pollForMemory = async (): Promise<Memory> => {
const response = await fetch(`/backend/api/memories/${result.id}`, {
const response = await fetch(`/backend/v1/memories/${result.id}`, {
credentials: "include",
});
if (!response.ok) {

View file

@ -31,7 +31,7 @@ type CreateSpaceResponse = {
};
export async function fetchSpaces(): Promise<SpaceResponse> {
const response = await fetch(`/backend/api/spaces`, {
const response = await fetch(`/backend/v1/spaces`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@ -50,7 +50,7 @@ async function createSpace(data: {
spaceName: string;
isPublic: boolean;
}): Promise<CreateSpaceResponse> {
const response = await fetch(`/backend/api/spaces/create`, {
const response = await fetch(`/backend/v1/spaces/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -68,7 +68,7 @@ async function createSpace(data: {
}
async function makeFavorite(spaceId: string) {
const response = await fetch(`/backend/api/spaces/favorite/${spaceId}`, {
const response = await fetch(`/backend/v1/spaces/favorite/${spaceId}`, {
method: "POST",
credentials: "include",
});

View file

@ -39,9 +39,8 @@ export const loader = async ({ request, context }: LoaderFunctionArgs) => {
const success = searchParams.get("success");
const integration = searchParams.get("integration");
try {
const recommendedQuestionsPromise = proxy("/api/recommended-questions", {}, request, context)
const recommendedQuestionsPromise = proxy("/v1/recommended-questions", {}, request, context)
.then((response) => response.json())
.then((data) => (data as { questions: string[] }).questions ?? null)
.catch(() => {

View file

@ -29,7 +29,7 @@ export const loader = (args: LoaderFunctionArgs) =>
return redirect("/");
}
const chatHistory = await proxy(`/api/chat/${threadId}`, {}, request, context);
const chatHistory = await proxy(`/v1/chat/${threadId}`, {}, request, context);
const chatHistoryJson = (await chatHistory.json()) as { chatHistory: CoreMessage[] };
if (!chatHistory) {

View file

@ -39,7 +39,7 @@ export const loader = (args: LoaderFunctionArgs) =>
});
}
const content = await proxy(`/api/memories/${contentId}`, {}, request, context);
const content = await proxy(`/v1/memories/${contentId}`, {}, request, context);
if (!content) {
throw new Response(null, {
@ -72,7 +72,7 @@ export default function Content() {
// Delete mutation
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/backend/api/memories/${id}`, {
const response = await fetch(`/backend/v1/memories/${id}`, {
method: "DELETE",
credentials: "include",
});
@ -94,7 +94,7 @@ export default function Content() {
// Move to space mutation
const moveToSpaceMutation = useMutation({
mutationFn: async ({ spaceId, documentId }: { spaceId: string; documentId: string }) => {
const response = await fetch("/backend/api/spaces/addContent", {
const response = await fetch("/backend/v1/spaces/addContent", {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -10,10 +10,10 @@ import { Theme, useTheme } from "../lib/theme-provider";
import { authkitLoader } from "@supermemory/authkit-remix-cloudflare";
import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
import { motion } from "framer-motion";
import { toast } from "sonner";
import { loader as routeLoader } from "~/root";
import { proxy } from "server/proxy";
import { toast } from "sonner";
import { getChromeExtensionId } from "~/config/util";
import { loader as routeLoader } from "~/root";
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { ensureSignedIn: true });
@ -21,12 +21,17 @@ export const action = async ({ request, context }: ActionFunctionArgs) => {
const formData = await request.formData();
const intent = formData.get("intent");
await proxy("/api/user/update", {
method: "POST",
body: JSON.stringify({
hasOnboarded: 1,
}),
}, request, context);
await proxy(
"/v1/user/update",
{
method: "POST",
body: JSON.stringify({
hasOnboarded: 1,
}),
},
request,
context,
);
return redirect("/");
};
@ -45,7 +50,7 @@ export default function Onboarding() {
useEffect(() => {
setTheme(Theme.DARK);
// Check if extension is present
try {
chrome?.runtime.sendMessage(getChromeExtensionId(), { action: "ping" }, (response: any) => {

View file

@ -21,7 +21,7 @@ export const loader = async ({ request, context }: LoaderFunctionArgs) => {
return redirect("/signin");
}
const userInfo = await proxy("/api/user", {}, request, context);
const userInfo = await proxy("/v1/user", {}, request, context);
const userInfoJson = (await userInfo.json()) as User;
console.log("userInfoJson", userInfoJson);

View file

@ -40,7 +40,7 @@ export async function loader({ params, request, context }: LoaderFunctionArgs) {
try {
// Fetch space details and check access
const response = await proxy(`/api/spaces/${spaceId}`, { method: "GET" }, request, context);
const response = await proxy(`/v1/spaces/${spaceId}`, { method: "GET" }, request, context);
if (!response.ok) {
if (response.status === 404) {
@ -100,7 +100,7 @@ export default function SpacePage() {
setIsInviting(true);
try {
const response = await fetch(`/backend/api/spaces/${space.uuid}/invite`, {
const response = await fetch(`/backend/v1/spaces/${space.uuid}/invite`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, accessType }),
@ -123,7 +123,7 @@ export default function SpacePage() {
const handleFavorite = async () => {
try {
const response = await fetch(`/backend/api/spaces/${space.uuid}/favorite`, {
const response = await fetch(`/backend/v1/spaces/${space.uuid}/favorite`, {
method: isFavorited ? "DELETE" : "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
@ -144,13 +144,13 @@ export default function SpacePage() {
const handleShare = async () => {
const shareUrl = window.location.href;
if (navigator.share) {
try {
await navigator.share({
title: `${space.name} - Supermemory Space`,
text: `Check out this space on Supermemory: ${space.name}`,
url: shareUrl
url: shareUrl,
});
} catch (err) {
// Fallback to clipboard if share fails or is cancelled
@ -241,16 +241,20 @@ export default function SpacePage() {
<Button variant="outline" size="sm" onClick={handleCopyLink}>
<Clipboard className="h-4 w-4" />
</Button>
{space.isPublic && user && !space.permissions.isOwner && !space.permissions.canEdit && !space.permissions.canRead && (
<Button
variant="ghost"
size="sm"
onClick={handleFavorite}
className={`ml-2 ${isFavorited ? "text-yellow-500" : "text-gray-500"}`}
>
<Star className="h-4 w-4" fill={isFavorited ? "currentColor" : "none"} />
</Button>
)}
{space.isPublic &&
user &&
!space.permissions.isOwner &&
!space.permissions.canEdit &&
!space.permissions.canRead && (
<Button
variant="ghost"
size="sm"
onClick={handleFavorite}
className={`ml-2 ${isFavorited ? "text-yellow-500" : "text-gray-500"}`}
>
<Star className="h-4 w-4" fill={isFavorited ? "currentColor" : "none"} />
</Button>
)}
</div>
</div>
</div>

View file

@ -25,7 +25,7 @@ export async function loader({ request, params, context }: LoaderFunctionArgs) {
try {
// Check if user has pending invitation
const response = await proxy(`/api/spaces/${spaceId}/invitation`, {}, request, context);
const response = await proxy(`/v1/spaces/${spaceId}/invitation`, {}, request, context);
const myJson = await response.json();
@ -50,9 +50,8 @@ export default function SpaceInvitation() {
console.log(invitation);
const navigate = useNavigate();
async function handleInviteResponse(action: "accept" | "reject") {
const response = await fetch(`/backend/api/spaces/invites/${action}`, {
const response = await fetch(`/backend/v1/spaces/invites/${action}`, {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -4,7 +4,7 @@
"sideEffects": true,
"type": "module",
"engineStrict": true,
"packageManager": "bun@1.1.29",
"packageManager": "bun@1.2.1",
"scripts": {
"build": "remix vite:build",
"cf-typegen": "wrangler types",

View file

@ -25,7 +25,7 @@
"engines": {
"node": ">=18"
},
"packageManager": "bun@1.0.26",
"packageManager": "bun@1.2.1",
"workspaces": [
"apps/*",
"packages/*"