diff --git a/apps/backend/src/auth.ts b/apps/backend/src/auth.ts index b66bba39..0ceeb0c1 100644 --- a/apps/backend/src/auth.ts +++ b/apps/backend/src/auth.ts @@ -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); diff --git a/apps/backend/src/index.tsx b/apps/backend/src/index.tsx index cb63e53e..55aa58c7 100644 --- a/apps/backend/src/index.tsx +++ b/apps/backend/src/index.tsx @@ -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(); }) - .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( diff --git a/apps/backend/src/routes/spaces.ts b/apps/backend/src/routes/spaces.ts index 1311128c..9c53dee7 100644 --- a/apps/backend/src/routes/spaces.ts +++ b/apps/backend/src/routes/spaces.ts @@ -472,7 +472,7 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>() }); } ).post( - "/api/space/invites/:action", + "/invites/:action", zValidator( "json", z.object({ diff --git a/apps/docs/changelog/overview.mdx b/apps/docs/changelog/overview.mdx index c04403dd..14b07587 100644 --- a/apps/docs/changelog/overview.mdx +++ b/apps/docs/changelog/overview.mdx @@ -2,7 +2,6 @@ title: "Product Updates" description: "New updates and improvements" mode: "center" -icon: "rocket" --- diff --git a/apps/docs/images/setup/1.png b/apps/docs/images/setup/1.png new file mode 100644 index 00000000..aba55dc7 Binary files /dev/null and b/apps/docs/images/setup/1.png differ diff --git a/apps/docs/images/setup/2.png b/apps/docs/images/setup/2.png new file mode 100644 index 00000000..32f195cb Binary files /dev/null and b/apps/docs/images/setup/2.png differ diff --git a/apps/docs/images/setup/3.png b/apps/docs/images/setup/3.png new file mode 100644 index 00000000..a91ecaa4 Binary files /dev/null and b/apps/docs/images/setup/3.png differ diff --git a/apps/docs/quickstart.mdx b/apps/docs/quickstart.mdx index c44cfaaf..e2e4b65f 100644 --- a/apps/docs/quickstart.mdx +++ b/apps/docs/quickstart.mdx @@ -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) - - 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. - - - 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 ``` + + 1. Login into [supermemory.ai](https://supermemory.ai) and click on the "Add Memory" button + +  + + 2. Click on "Integrations" in the navigation menu + +  + + 3. You'll see your API key, you can copy it by clicking on the copy button + +  + + Keep your API key secure and never share it publicly. You'll need this key for authenticating all API requests. -### Deploy your changes +## Base URL - - - - 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. - - - [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). - - - - -## 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. - - - - - Add flair to your docs with personalized branding. - - - - Implement your OpenAPI spec and enable API user interaction. - - - - Draw insights from user interactions with your documentation. - - - - Keep your docs on your own website's subdomain. - - - +All API requests should be made to: diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index 21179f86..7d96feff 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -14,7 +14,7 @@ const tabStates = new Map(); 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( 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( 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", diff --git a/apps/extension/src/twitter.ts b/apps/extension/src/twitter.ts index 8202e583..5b19c503 100644 --- a/apps/extension/src/twitter.ts +++ b/apps/extension/src/twitter.ts @@ -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", diff --git a/apps/extension/ui/hooks/use-spaces.tsx b/apps/extension/ui/hooks/use-spaces.tsx index ffffbb57..a7b5f4df 100644 --- a/apps/extension/ui/hooks/use-spaces.tsx +++ b/apps/extension/ui/hooks/use-spaces.tsx @@ -51,7 +51,7 @@ async function createSpace(data: { isPublic: boolean; }): Promise { 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", } diff --git a/apps/web/app/components/Reminders.tsx b/apps/web/app/components/Reminders.tsx index 1a3fca49..5de30e25 100644 --- a/apps/web/app/components/Reminders.tsx +++ b/apps/web/app/components/Reminders.tsx @@ -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> }>) diff --git a/apps/web/app/components/memories/CSVUploadModal.tsx b/apps/web/app/components/memories/CSVUploadModal.tsx index 88c65eed..66359269 100644 --- a/apps/web/app/components/memories/CSVUploadModal.tsx +++ b/apps/web/app/components/memories/CSVUploadModal.tsx @@ -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", diff --git a/apps/web/app/components/memories/Integrations.tsx b/apps/web/app/components/memories/Integrations.tsx index 9b01796b..c5bd2996 100644 --- a/apps/web/app/components/memories/Integrations.tsx +++ b/apps/web/app/components/memories/Integrations.tsx @@ -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, }); diff --git a/apps/web/app/components/memories/MarkdownUploadModal.tsx b/apps/web/app/components/memories/MarkdownUploadModal.tsx index 48ac196f..23e6f33b 100644 --- a/apps/web/app/components/memories/MarkdownUploadModal.tsx +++ b/apps/web/app/components/memories/MarkdownUploadModal.tsx @@ -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", diff --git a/apps/web/app/components/memories/SharedCard.tsx b/apps/web/app/components/memories/SharedCard.tsx index 582a8b76..7bd2cc6a 100644 --- a/apps/web/app/components/memories/SharedCard.tsx +++ b/apps/web/app/components/memories/SharedCard.tsx @@ -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 { } } -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 ( - - - {image && ( - - - - - )} - + + {image && ( + + + + )} - style={{ - backgroundColor: dominantColor, - marginTop: image ? "-2.5rem" : 0, - }} - > - {displayTitle} - - {displayDescription} - - - {domain} - - - - + {displayTitle} + {displayDescription} + + {domain} + + + + + - - ); -}); + ); + }, +); export function FetchAndRenderContent({ content }: { content: string }) { const [memory, setMemory] = useState(null); @@ -601,71 +614,70 @@ export function FetchAndRenderContent({ content }: { content: string }) { return 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 }) { - handleDelete(e)} asChild> + handleDelete(e)} asChild> Delete @@ -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 ( - Error: {error instanceof Error ? error.message : 'Failed to load spaces'} + + Error: {error instanceof Error ? error.message : "Failed to load spaces"} + ); @@ -790,16 +808,12 @@ export const SpaceSelector = function SpaceSelector({ return ( - + {filteredSpaces.map((space) => ( - onSelect(space.uuid)} > @@ -809,9 +823,7 @@ export const SpaceSelector = function SpaceSelector({ ))} - {filteredSpaces.length === 0 && ( - No spaces found. - )} + {filteredSpaces.length === 0 && No spaces found.} @@ -821,4 +833,4 @@ export const SpaceSelector = function SpaceSelector({ const MemoizedSpaceSelector = memo(SpaceSelector); -export default SharedCard; \ No newline at end of file +export default SharedCard; diff --git a/apps/web/app/config/integrations.tsx b/apps/web/app/config/integrations.tsx index 365f6314..0b55c143 100644 --- a/apps/web/app/config/integrations.tsx +++ b/apps/web/app/config/integrations.tsx @@ -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) => ( - + ), }, }); diff --git a/apps/web/app/lib/hooks/use-chat-stream.ts b/apps/web/app/lib/hooks/use-chat-stream.ts index 4e0e0f63..f47df3df 100644 --- a/apps/web/app/lib/hooks/use-chat-stream.ts +++ b/apps/web/app/lib/hooks/use-chat-stream.ts @@ -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) { diff --git a/apps/web/app/lib/hooks/use-memories.tsx b/apps/web/app/lib/hooks/use-memories.tsx index 4e5e8c70..b956c319 100644 --- a/apps/web/app/lib/hooks/use-memories.tsx +++ b/apps/web/app/lib/hooks/use-memories.tsx @@ -27,7 +27,7 @@ export function useMemories(start = 0, count = 40, spaceId?: string) { const { data: memoriesData, isLoading: isInitialLoading } = useQuery({ 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 => { - const response = await fetch(`/backend/api/memories/${result.id}`, { + const response = await fetch(`/backend/v1/memories/${result.id}`, { credentials: "include", }); if (!response.ok) { diff --git a/apps/web/app/lib/hooks/use-spaces.tsx b/apps/web/app/lib/hooks/use-spaces.tsx index 5ddb8b21..827c7c79 100644 --- a/apps/web/app/lib/hooks/use-spaces.tsx +++ b/apps/web/app/lib/hooks/use-spaces.tsx @@ -31,7 +31,7 @@ type CreateSpaceResponse = { }; export async function fetchSpaces(): Promise { - 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 { - 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", }); diff --git a/apps/web/app/routes/_index.tsx b/apps/web/app/routes/_index.tsx index 84373771..b28355bb 100644 --- a/apps/web/app/routes/_index.tsx +++ b/apps/web/app/routes/_index.tsx @@ -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(() => { diff --git a/apps/web/app/routes/chat.$chatId.tsx b/apps/web/app/routes/chat.$chatId.tsx index af4340df..ba31d600 100644 --- a/apps/web/app/routes/chat.$chatId.tsx +++ b/apps/web/app/routes/chat.$chatId.tsx @@ -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) { diff --git a/apps/web/app/routes/content.$contentid.tsx b/apps/web/app/routes/content.$contentid.tsx index 635bdf32..ca81d039 100644 --- a/apps/web/app/routes/content.$contentid.tsx +++ b/apps/web/app/routes/content.$contentid.tsx @@ -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", diff --git a/apps/web/app/routes/onboarding.import.tsx b/apps/web/app/routes/onboarding.import.tsx index 3f24c64b..69c11b14 100644 --- a/apps/web/app/routes/onboarding.import.tsx +++ b/apps/web/app/routes/onboarding.import.tsx @@ -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) => { diff --git a/apps/web/app/routes/onboarding.index.tsx b/apps/web/app/routes/onboarding.index.tsx index a82e2acc..5ffff732 100644 --- a/apps/web/app/routes/onboarding.index.tsx +++ b/apps/web/app/routes/onboarding.index.tsx @@ -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); diff --git a/apps/web/app/routes/space.$spaceId.tsx b/apps/web/app/routes/space.$spaceId.tsx index 0f65f910..65ac6e3f 100644 --- a/apps/web/app/routes/space.$spaceId.tsx +++ b/apps/web/app/routes/space.$spaceId.tsx @@ -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() { - {space.isPublic && user && !space.permissions.isOwner && !space.permissions.canEdit && !space.permissions.canRead && ( - - - - )} + {space.isPublic && + user && + !space.permissions.isOwner && + !space.permissions.canEdit && + !space.permissions.canRead && ( + + + + )} diff --git a/apps/web/app/routes/space.($spaceId).invitation.tsx b/apps/web/app/routes/space.($spaceId).invitation.tsx index bf8e6f80..26b3b2df 100644 --- a/apps/web/app/routes/space.($spaceId).invitation.tsx +++ b/apps/web/app/routes/space.($spaceId).invitation.tsx @@ -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", diff --git a/apps/web/package.json b/apps/web/package.json index 202d795f..51841eb6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/package.json b/package.json index 5031ce4d..ef25819d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "engines": { "node": ">=18" }, - "packageManager": "bun@1.0.26", + "packageManager": "bun@1.2.1", "workspaces": [ "apps/*", "packages/*"
- {displayDescription} -
{displayDescription}