mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
redepl
This commit is contained in:
parent
3630d584fa
commit
05434b1c57
29 changed files with 300 additions and 313 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
});
|
||||
}
|
||||
).post(
|
||||
"/api/space/invites/:action",
|
||||
"/invites/:action",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
BIN
apps/docs/images/setup/1.png
Normal file
BIN
apps/docs/images/setup/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 994 KiB |
BIN
apps/docs/images/setup/2.png
Normal file
BIN
apps/docs/images/setup/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 638 KiB |
BIN
apps/docs/images/setup/3.png
Normal file
BIN
apps/docs/images/setup/3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 645 KiB |
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
</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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>> }>)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"packageManager": "bun@1.0.26",
|
||||
"packageManager": "bun@1.2.1",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue