This commit is contained in:
Dhravya Shah 2025-10-03 02:37:50 -07:00
commit be34a14550
40 changed files with 4894 additions and 168 deletions

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ drizzle.config.ts
node_modules
.pnp
.pnp.js
bun.lock
# Local env files
.env

14
apps/raycast-extension/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# Raycast specific files
raycast-env.d.ts
.raycast-swift-build
.swiftpm
compiled_raycast_swift
compiled_raycast_rust
# misc
.DS_Store

View file

@ -0,0 +1,6 @@
# supermemory-raycast Changelog
## [Initial Version] - {2025-09-27}
- Added Supermemory integration with Add Memory and Search Memories commands
- Added project organization support for memories

View file

@ -0,0 +1,35 @@
# Raycast Extension for Supermemory
A Raycast extension that lets you add memories and search through your Supermemory collection directly from Raycast.
## Setup
1. Install the extension in Raycast
2. Get your API key from [app.supermemory.ai](https://app.supermemory.ai)
3. Open the extension preferences and enter your API key
## Features
### Add Memory
- Add new memories to your Supermemory collection
- Organize memories by project
- Add optional titles and URLs
- Keyboard shortcut: Cmd+Enter to submit
### Search Memories
- Search through your entire Supermemory collection
- Real-time search with debouncing
- View detailed memory information
- Copy content or open related URLs
- Shows relevance scores and creation dates
## Commands
- `Add Memory` - Add a new memory to your collection
- `Search Memories` - Search through your existing memories
## Authentication
This extension requires a Supermemory API key. You can get your API key from [supermemory.link/raycast](https://supermemory.link/raycast).
The API key is stored securely in Raycast preferences and is required for all operations.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,4 @@
const { defineConfig } = require("eslint/config")
const raycastConfig = require("@raycast/eslint-config")
module.exports = defineConfig([...raycastConfig])

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

3233
apps/raycast-extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "supermemory",
"title": "Supermemory",
"description": "Add and search memories with your personal AI-powered knowledge base",
"icon": "extension-icon.png",
"author": "supermemory",
"platforms": [
"macOS",
"Windows"
],
"categories": [
"Productivity",
"Web"
],
"license": "MIT",
"commands": [
{
"name": "add-memory",
"title": "Add Memory",
"subtitle": "add memory to your supermemory app",
"description": "Add text, URLs, or documents to your supermemory knowledge base",
"mode": "view"
},
{
"name": "search-memories",
"title": "Search Memories",
"description": "Search through your saved memories and find relevant information",
"mode": "view"
}
],
"preferences": [
{
"name": "apiKey",
"type": "password",
"required": true,
"title": "API Key",
"description": "Your Supermemory API Key. Get it from https://supermemory.link/raycast",
"placeholder": "Enter your Supermemory API Key"
}
],
"dependencies": {
"@raycast/api": "^1.103.2",
"@raycast/utils": "^1.17.0"
},
"devDependencies": {
"@raycast/eslint-config": "^2.0.4",
"@types/node": "22.13.10",
"@types/react": "19.0.10",
"eslint": "^9.22.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2"
},
"scripts": {
"build": "ray build",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
"publish": "npx @raycast/api@latest publish"
}
}

View file

@ -0,0 +1,110 @@
import {
Form,
ActionPanel,
Action,
showToast,
Toast,
useNavigation,
} from "@raycast/api";
import { useEffect, useState } from "react";
import {
addMemory,
fetchProjects,
checkApiConnection,
type Project,
} from "./api";
interface FormValues {
content: string;
project: string;
}
export default function Command() {
const [projects, setProjects] = useState<Project[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const { pop } = useNavigation();
useEffect(() => {
async function loadProjects() {
try {
setIsLoading(true);
const isConnected = await checkApiConnection();
if (!isConnected) {
return;
}
const fetchedProjects = await fetchProjects();
setProjects(fetchedProjects);
} catch (error) {
console.error("Failed to load projects:", error);
} finally {
setIsLoading(false);
}
}
loadProjects();
}, []);
async function handleSubmit(values: FormValues) {
if (!values.content.trim()) {
await showToast({
style: Toast.Style.Failure,
title: "Content Required",
message: "Please enter some content for the memory",
});
return;
}
try {
setIsSubmitting(true);
const containerTags = values.project ? [values.project] : undefined;
await addMemory({
content: values.content.trim(),
containerTags,
});
pop();
} catch (error) {
console.error("Failed to add memory:", error);
} finally {
setIsSubmitting(false);
}
}
return (
<Form
isLoading={isLoading || isSubmitting}
actions={
<ActionPanel>
<Action.SubmitForm title="Add Memory" onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.TextArea
id="content"
title="Content"
placeholder="Enter the memory content..."
info="The main content of your memory. This is required."
/>
<Form.Separator />
<Form.Dropdown
id="project"
title="Project"
info="Select a project to organize this memory"
storeValue
>
<Form.Dropdown.Item value="" title="No Project" />
{projects.map((project) => (
<Form.Dropdown.Item
key={project.id}
value={project.containerTag}
title={project.name}
/>
))}
</Form.Dropdown>
</Form>
);
}

View file

@ -0,0 +1,227 @@
import { getPreferenceValues, showToast, Toast } from "@raycast/api";
export interface Project {
id: string;
name: string;
containerTag: string;
description?: string;
}
export interface Memory {
id: string;
content: string;
title?: string;
url?: string;
containerTag?: string;
createdAt: string;
}
export interface SearchResult {
documentId: string;
chunks: unknown[];
title?: string;
metadata: Record<string, unknown>;
score?: number;
createdAt: string;
updatedAt: string;
type: string;
}
export interface AddMemoryRequest {
content: string;
containerTags?: string[];
title?: string;
url?: string;
metadata?: Record<string, unknown>;
}
export interface SearchRequest {
q: string;
containerTags?: string[];
limit?: number;
}
export interface SearchResponse {
results: SearchResult[];
timing: number;
total: number;
}
const API_BASE_URL = "https://api.supermemory.ai";
class SupermemoryAPIError extends Error {
constructor(
message: string,
public status?: number,
) {
super(message);
this.name = "SupermemoryAPIError";
}
}
class AuthenticationError extends Error {
constructor(message: string) {
super(message);
this.name = "AuthenticationError";
}
}
async function getApiKey(): Promise<string> {
try {
const preferences = getPreferenceValues<{ apiKey: string }>();
const apiKey = preferences.apiKey?.trim();
if (!apiKey) {
throw new AuthenticationError(
"API key is required. Please add your Supermemory API key in preferences.",
);
}
return apiKey;
} catch {
throw new AuthenticationError("Failed to get API key from preferences.");
}
}
async function makeAuthenticatedRequest<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
const apiKey = await getApiKey();
const url = `${API_BASE_URL}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
if (response.status === 401) {
throw new AuthenticationError(
"Invalid API key. Please check your API key in preferences. Get a new one from https://supermemory.link/raycast",
);
}
let errorMessage = `API request failed: ${response.statusText}`;
try {
const errorBody = (await response.json()) as { message?: string };
if (errorBody.message) {
errorMessage = errorBody.message;
}
} catch {
// Ignore JSON parsing errors, use default message
}
throw new SupermemoryAPIError(errorMessage, response.status);
}
if (!response.headers.get("content-type")?.includes("application/json")) {
throw new SupermemoryAPIError("Invalid response format from API");
}
const data = (await response.json()) as T;
return data;
} catch (err) {
if (
err instanceof AuthenticationError ||
err instanceof SupermemoryAPIError
) {
throw err;
}
// Handle network errors or other fetch errors
throw new SupermemoryAPIError(
`Network error: ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
}
export async function fetchProjects(): Promise<Project[]> {
try {
const response = await makeAuthenticatedRequest<{ projects: Project[] }>(
"/v3/projects",
);
return response.projects || [];
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to fetch projects",
message:
error instanceof Error ? error.message : "Unknown error occurred",
});
throw error;
}
}
export async function addMemory(request: AddMemoryRequest): Promise<Memory> {
try {
const response = await makeAuthenticatedRequest<Memory>("/v3/documents", {
method: "POST",
body: JSON.stringify(request),
});
await showToast({
style: Toast.Style.Success,
title: "Memory Added",
message: "Successfully added memory to Supermemory",
});
return response;
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to add memory",
message:
error instanceof Error ? error.message : "Unknown error occurred",
});
throw error;
}
}
export async function searchMemories(
request: SearchRequest,
): Promise<SearchResult[]> {
try {
const response = await makeAuthenticatedRequest<SearchResponse>(
"/v3/search",
{
method: "POST",
body: JSON.stringify(request),
},
);
return response.results || [];
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to search memories",
message:
error instanceof Error ? error.message : "Unknown error occurred",
});
throw error;
}
}
// Helper function to check if API key is configured and valid
export async function checkApiConnection(): Promise<boolean> {
try {
await fetchProjects();
return true;
} catch (error) {
if (error instanceof AuthenticationError) {
await showToast({
style: Toast.Style.Failure,
title: "API Key Required",
message:
"Please configure your Supermemory API key in preferences. Get it from https://supermemory.link/raycast",
});
}
return false;
}
}

View file

@ -0,0 +1,253 @@
import {
ActionPanel,
Detail,
List,
Action,
Icon,
showToast,
Toast,
Clipboard,
openExtensionPreferences,
} from "@raycast/api";
import { useState, useEffect, useCallback } from "react";
import { searchMemories, checkApiConnection, type SearchResult } from "./api";
const extractContent = (memory: SearchResult) => {
if (memory.chunks && memory.chunks.length > 0) {
return memory.chunks
.map((chunk: unknown) => {
if (typeof chunk === "string") return chunk;
if (
chunk &&
typeof chunk === "object" &&
"content" in chunk &&
typeof chunk.content === "string"
)
return chunk.content;
if (
chunk &&
typeof chunk === "object" &&
"text" in chunk &&
typeof chunk.text === "string"
)
return chunk.text;
return "";
})
.filter(Boolean)
.join(" ");
}
return "No content available";
};
const extractUrl = (memory: SearchResult) => {
if (memory.metadata?.url && typeof memory.metadata.url === "string") {
return memory.metadata.url;
}
return null;
};
export default function Command() {
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [searchText, setSearchText] = useState("");
const [hasSearched, setHasSearched] = useState(false);
const [isConnected, setIsConnected] = useState<boolean | null>(null);
useEffect(() => {
async function checkConnection() {
const connected = await checkApiConnection();
setIsConnected(connected);
}
checkConnection();
}, []);
const performSearch = useCallback(
async (query: string) => {
if (!query.trim() || !isConnected) return;
try {
setIsLoading(true);
setHasSearched(true);
const results = await searchMemories({
q: query.trim(),
limit: 50,
});
setSearchResults(results);
if (results.length === 0) {
await showToast({
style: Toast.Style.Success,
title: "Search Complete",
message: "No memories found for your query",
});
}
} catch (error) {
console.error("Search failed:", error);
setSearchResults([]);
} finally {
setIsLoading(false);
}
},
[isConnected],
);
useEffect(() => {
if (!searchText.trim()) {
setSearchResults([]);
setHasSearched(false);
return;
}
const debounceTimer = setTimeout(() => {
performSearch(searchText);
}, 500);
return () => clearTimeout(debounceTimer);
}, [searchText, performSearch]);
const formatDate = (dateString: string) => {
try {
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return "Unknown date";
}
};
const truncateContent = (content: string, maxLength = 100) => {
if (content.length <= maxLength) return content;
return `${content.substring(0, maxLength)}...`;
};
if (isConnected === false) {
return (
<List>
<List.EmptyView
icon={Icon.ExclamationMark}
title="API Key Required"
description="Please configure your Supermemory API key to search memories"
actions={
<ActionPanel>
<Action
title="Open Extension Preferences"
onAction={() => openExtensionPreferences()}
icon={Icon.Gear}
/>
</ActionPanel>
}
/>
</List>
);
}
return (
<List
isLoading={isLoading}
onSearchTextChange={setSearchText}
searchBarPlaceholder="Search your memories..."
throttle
>
{!hasSearched && !searchText.trim() ? (
<List.EmptyView
icon={Icon.MagnifyingGlass}
title="Search Your Memories"
description="Type to search through your Supermemory collection"
/>
) : hasSearched && searchResults.length === 0 ? (
<List.EmptyView
icon={Icon.Document}
title="No Memories Found"
description={`No memories found for "${searchText}"`}
/>
) : (
searchResults.map((memory) => {
const content = extractContent(memory);
const url = extractUrl(memory);
return (
<List.Item
key={memory.documentId}
icon={url ? Icon.Link : Icon.Document}
title={memory.title || "Untitled Memory"}
subtitle={truncateContent(content)}
accessories={[
{ text: formatDate(memory.createdAt) },
...(memory.score
? [{ text: `${Math.round(memory.score * 100)}%` }]
: []),
]}
actions={
<ActionPanel>
<Action.Push
title="View Details"
target={<MemoryDetail memory={memory} />}
icon={Icon.Eye}
/>
<Action
title="Copy Content"
onAction={() => Clipboard.copy(content)}
icon={Icon.Clipboard}
shortcut={{ modifiers: ["cmd"], key: "c" }}
/>
{url && (
<Action.OpenInBrowser
title="Open URL"
url={url}
shortcut={{ modifiers: ["cmd"], key: "o" }}
/>
)}
</ActionPanel>
}
/>
);
})
)}
</List>
);
}
function MemoryDetail({ memory }: { memory: SearchResult }) {
const content = extractContent(memory);
const url = extractUrl(memory);
const markdown = `
# ${memory.title || "Untitled Memory"}
${content}
---
**Created:** ${new Date(memory.createdAt).toLocaleString()}
${url ? `**URL:** ${url}` : ""}
${memory.score ? `**Relevance:** ${Math.round(memory.score * 100)}%` : ""}
`;
return (
<Detail
markdown={markdown}
actions={
<ActionPanel>
<Action
title="Copy Content"
onAction={() => Clipboard.copy(content)}
icon={Icon.Clipboard}
shortcut={{ modifiers: ["cmd"], key: "c" }}
/>
{url && (
<Action.OpenInBrowser
title="Open URL"
url={url}
shortcut={{ modifiers: ["cmd"], key: "o" }}
/>
)}
</ActionPanel>
}
/>
);
}

View file

@ -0,0 +1,16 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["src/**/*", "raycast-env.d.ts"],
"compilerOptions": {
"lib": ["ES2023"],
"module": "commonjs",
"target": "ES2023",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"resolveJsonModule": true
}
}

View file

@ -6,6 +6,7 @@ import { ChevronsDown, LoaderIcon } from "lucide-react"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { InstallPrompt } from "@/components/install-prompt"
import { ChromeExtensionButton } from "@/components/chrome-extension-button"
import { ChatInput } from "@/components/chat-input"
import { BackgroundPlus } from "@ui/components/grid-plus"
import { Memories } from "@/components/memories"
@ -76,6 +77,7 @@ export default function Page() {
<Memories />
<InstallPrompt />
<ChromeExtensionButton />
</div>
)
}

View file

@ -0,0 +1,234 @@
"use client"
import { Button } from "@ui/components/button"
import {
Bookmark,
Zap,
CircleX,
Users,
Lock,
ChromeIcon,
TwitterIcon,
} from "lucide-react"
import { useEffect, useState } from "react"
import { motion } from "framer-motion"
import Image from "next/image"
import { analytics } from "@/lib/analytics"
export function ChromeExtensionButton() {
const [isExtensionInstalled, setIsExtensionInstalled] = useState(false)
const [isChecking, setIsChecking] = useState(true)
const [isDismissed, setIsDismissed] = useState(false)
const [isMinimized, setIsMinimized] = useState(false)
useEffect(() => {
const dismissed =
localStorage.getItem("chrome-extension-dismissed") === "true"
setIsDismissed(dismissed)
const checkExtension = () => {
const message = { action: "check-extension" }
const timeout = setTimeout(() => {
setIsExtensionInstalled(false)
setIsChecking(false)
// Auto-minimize after 3 seconds if extension is not installed and not dismissed
if (!dismissed) {
setTimeout(() => {
setIsMinimized(true)
}, 3000)
}
}, 1000)
const handleMessage = (event: MessageEvent) => {
if (event.data?.action === "extension-detected") {
clearTimeout(timeout)
setIsExtensionInstalled(true)
setIsChecking(false)
window.removeEventListener("message", handleMessage)
}
}
window.addEventListener("message", handleMessage)
window.postMessage(message, "*")
return () => {
clearTimeout(timeout)
window.removeEventListener("message", handleMessage)
}
}
if (!dismissed) {
checkExtension()
} else {
setIsChecking(false)
}
}, [])
const handleInstall = () => {
analytics.extensionInstallClicked()
window.open(
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
"_blank",
"noopener,noreferrer",
)
}
const handleDismiss = () => {
localStorage.setItem("chrome-extension-dismissed", "true")
setIsDismissed(true)
}
// Don't show if extension is installed, checking, or dismissed
if (isExtensionInstalled || isChecking || isDismissed) {
return null
}
return (
<motion.div
className="fixed bottom-4 right-4 z-50"
initial={{ opacity: 0, y: 20, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div
className={`bg-background/95 backdrop-blur-md shadow-xl ${
isMinimized
? "flex items-center gap-1 rounded-full"
: "max-w-md w-90 rounded-2xl"
}`}
>
{!isMinimized && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
className="overflow-hidden"
>
<div className="p-4 text-white bg-cover bg-center">
<div
className="p-4 rounded-lg"
style={{
backgroundImage: "url('/images/extension-bg.png')",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<div className="relative">
<h1 className="text-2xl font-bold mb-1">
supermemory extension
</h1>
<p className="text-sm opacity-90">
your second brain for the web.
</p>
</div>
</div>
</div>
<div className="px-6 py-2 pb-4 space-y-4">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-center flex-shrink-0">
<TwitterIcon className="fill-blue-500 text-blue-500" />
</div>
<div>
<h3 className="font-semibold text-sm text-gray-800">
Twitter Imports
</h3>
<p className="text-xs text-gray-600">
Import your twitter timeline & save tweets.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-orange-50 border border-orange-200 rounded-lg flex items-center justify-center flex-shrink-0">
<Bookmark className="w-5 h-5 text-orange-600" />
</div>
<div>
<h3 className="font-semibold text-sm text-gray-800">
Save All Bookmarks
</h3>
<p className="text-xs text-gray-600">
Instantly save any webpage to your memory.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-green-50 border border-green-200 rounded-lg flex items-center justify-center flex-shrink-0">
<Zap className="w-5 h-5 text-green-600" />
</div>
<div>
<h3 className="font-semibold text-sm text-gray-800">
Charge Empty Memory
</h3>
<p className="text-xs text-gray-600">
Automatically capture & organize your browsing history.
</p>
</div>
</div>
</div>
<div className="px-6 pb-4">
<Button
onClick={handleInstall}
className="w-full bg-white border border-[#686CFD] text-gray-800 hover:bg-gray-50 font-semibold rounded-lg h-10 flex items-center justify-center gap-3"
>
<div className="w-6 h-6 bg-[#686CFD] rounded-full flex items-center justify-center">
<Image
src="/images/extension-logo.png"
alt="Extension Logo"
width={24}
height={24}
/>
</div>
Add to Chrome - It's Free
</Button>
</div>
<div className="px-6 pb-4 flex items-center justify-center gap-6 text-xs text-gray-500">
<div className="flex items-center gap-1">
<Users className="w-3 h-3" />
<span>4K+ users</span>
</div>
<div className="flex items-center gap-1">
<Lock className="w-3 h-3" />
<span>Privacy first</span>
</div>
</div>
</motion.div>
)}
{isMinimized && (
<div className="relative flex items-center w-full group">
<Button
size={"lg"}
onClick={handleInstall}
className="text-xs rounded-full"
style={{
backgroundImage: "url('/images/extension-bg.png')",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<ChromeIcon className="h-3 w-3 mr-1" />
Get Extension
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleDismiss}
className="absolute top-[-16px] right-[-12px] h-6 w-6 p-0 text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-75 transition-opacity duration-200"
>
<CircleX className="w-4 h-4" />
</Button>
</div>
)}
</div>
</motion.div>
)
}

View file

@ -2,8 +2,18 @@
import { Card, CardContent } from "@repo/ui/components/card"
import { Badge } from "@repo/ui/components/badge"
import { ExternalLink, FileText, Brain } from "lucide-react"
import { useState } from "react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@repo/ui/components/alert-dialog"
import { ExternalLink, FileText, Brain, Trash2 } from "lucide-react"
import { cn } from "@lib/utils"
import { colors } from "@repo/ui/memory-graph/constants"
import { getPastelBackgroundColor } from "../memories-utils"
@ -14,6 +24,7 @@ interface GoogleDocsCardProps {
description?: string | null
className?: string
onClick?: () => void
onDelete?: () => void
showExternalLink?: boolean
activeMemories?: Array<{ id: string; isForgotten?: boolean }>
lastModified?: string | Date
@ -25,12 +36,11 @@ export const GoogleDocsCard = ({
description,
className,
onClick,
onDelete,
showExternalLink = true,
activeMemories,
lastModified,
}: GoogleDocsCardProps) => {
const [imageError, setImageError] = useState(false)
const handleCardClick = () => {
if (onClick) {
onClick()
@ -57,6 +67,54 @@ export const GoogleDocsCard = ({
backgroundColor: getPastelBackgroundColor(url || title || "googledocs"),
}}
>
{onDelete && (
<AlertDialog>
<AlertDialogTrigger asChild>
<button
className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
onClick={(e) => {
e.stopPropagation()
}}
style={{
color: colors.text.muted,
backgroundColor: "rgba(255, 255, 255, 0.1)",
backdropFilter: "blur(4px)",
}}
type="button"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Document</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this document and all its
related memories? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={(e) => {
e.stopPropagation()
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-red-600 hover:bg-red-700 text-white"
onClick={(e) => {
e.stopPropagation()
onDelete()
}}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<CardContent className="p-0">
<div className="px-4 border-b border-white/10">
<div className="flex items-center justify-between">
@ -99,16 +157,18 @@ export const GoogleDocsCard = ({
</span>
</div>
</div>
{showExternalLink && (
<button
onClick={handleExternalLinkClick}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-white/10 flex-shrink-0"
type="button"
aria-label="Open in Google Docs"
>
<ExternalLink className="w-4 h-4" />
</button>
)}
<div className="flex items-center gap-1">
{showExternalLink && (
<button
onClick={handleExternalLinkClick}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-white/10 flex-shrink-0"
type="button"
aria-label="Open in Google Docs"
>
<ExternalLink className="w-4 h-4" />
</button>
)}
</div>
</div>
</div>

View file

@ -1,8 +1,19 @@
import { Badge } from "@repo/ui/components/badge"
import { Card, CardContent, CardHeader } from "@repo/ui/components/card"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@repo/ui/components/alert-dialog"
import { colors } from "@repo/ui/memory-graph/constants"
import { Brain, ExternalLink } from "lucide-react"
import { Brain, ExternalLink, Trash2 } from "lucide-react"
import { cn } from "@lib/utils"
import {
formatDate,
@ -32,6 +43,7 @@ export const NoteCard = ({
activeMemories,
forgottenMemories,
onOpenDetails,
onDelete,
}: NoteCardProps) => {
return (
<Card
@ -47,6 +59,52 @@ export const NoteCard = ({
width: width,
}}
>
<AlertDialog>
<AlertDialogTrigger asChild>
<button
className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 group-hover:cursor-pointer transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
onClick={(e) => {
e.stopPropagation()
}}
style={{
color: colors.text.muted,
backgroundColor: "rgba(255, 255, 255, 0.1)",
backdropFilter: "blur(4px)",
}}
type="button"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Document</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this document and all its related
memories? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={(e) => {
e.stopPropagation()
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-red-600 hover:bg-red-700 text-white"
onClick={(e) => {
e.stopPropagation()
onDelete(document)
}}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<CardHeader className="relative z-10 px-0 pb-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1">
@ -59,23 +117,25 @@ export const NoteCard = ({
{document.title || "Untitled Document"}
</p>
</div>
{document.url && (
<button
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded"
onClick={(e) => {
e.stopPropagation()
const sourceUrl = getSourceUrl(document)
window.open(sourceUrl ?? undefined, "_blank")
}}
style={{
backgroundColor: "rgba(255, 255, 255, 0.05)",
color: colors.text.secondary,
}}
type="button"
>
<ExternalLink className="w-3 h-3" />
</button>
)}
<div className="flex items-center gap-1">
{document.url && (
<button
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded"
onClick={(e) => {
e.stopPropagation()
const sourceUrl = getSourceUrl(document)
window.open(sourceUrl ?? undefined, "_blank")
}}
style={{
backgroundColor: "rgba(255, 255, 255, 0.05)",
color: colors.text.secondary,
}}
type="button"
>
<ExternalLink className="w-3 h-3" />
</button>
)}
</div>
<div className="flex items-center gap-2 text-[10px] text-muted-foreground">
<span>{formatDate(document.createdAt)}</span>
</div>

View file

@ -14,7 +14,18 @@ import {
enrichTweet,
} from "react-tweet"
import { Badge } from "@repo/ui/components/badge"
import { Brain } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@repo/ui/components/alert-dialog"
import { Brain, Trash2 } from "lucide-react"
import { colors } from "@repo/ui/memory-graph/constants"
import { getPastelBackgroundColor } from "../memories-utils"
@ -71,18 +82,69 @@ const CustomTweet = ({
export const TweetCard = ({
data,
activeMemories,
onDelete,
}: {
data: Tweet
activeMemories?: Array<{ id: string; isForgotten?: boolean }>
onDelete?: () => void
}) => {
return (
<div
className="relative transition-all"
className="relative transition-all group"
style={{
backgroundColor: getPastelBackgroundColor(data.id_str || "tweet"),
}}
>
<CustomTweet components={{}} tweet={data} />
{onDelete && (
<AlertDialog>
<AlertDialogTrigger asChild>
<button
className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
onClick={(e) => {
e.stopPropagation()
}}
style={{
color: colors.text.muted,
backgroundColor: "rgba(255, 255, 255, 0.1)",
backdropFilter: "blur(4px)",
}}
type="button"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Document</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this document and all its
related memories? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={(e) => {
e.stopPropagation()
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-red-600 hover:bg-red-700 text-white"
onClick={(e) => {
e.stopPropagation()
onDelete()
}}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
{activeMemories && activeMemories.length > 0 && (
<div className="absolute bottom-2 left-4 z-10">
<Badge

View file

@ -1,10 +1,22 @@
"use client"
import { Card, CardContent } from "@repo/ui/components/card"
import { ExternalLink } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@repo/ui/components/alert-dialog"
import { ExternalLink, Trash2 } from "lucide-react"
import { useState } from "react"
import { cn } from "@lib/utils"
import { getPastelBackgroundColor } from "../memories-utils"
import { colors } from "@repo/ui/memory-graph/constants"
interface WebsiteCardProps {
title: string
@ -13,6 +25,7 @@ interface WebsiteCardProps {
description?: string
className?: string
onClick?: () => void
onDelete?: () => void
showExternalLink?: boolean
}
@ -23,6 +36,7 @@ export const WebsiteCard = ({
description,
className,
onClick,
onDelete,
showExternalLink = true,
}: WebsiteCardProps) => {
const [imageError, setImageError] = useState(false)
@ -51,7 +65,7 @@ export const WebsiteCard = ({
return (
<Card
className={cn(
"cursor-pointer transition-all hover:shadow-md group overflow-hidden py-0",
"cursor-pointer transition-all hover:shadow-md group overflow-hidden py-0 relative",
className,
)}
onClick={handleCardClick}
@ -59,6 +73,54 @@ export const WebsiteCard = ({
backgroundColor: getPastelBackgroundColor(url || title || "website"),
}}
>
{onDelete && (
<AlertDialog>
<AlertDialogTrigger asChild>
<button
className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
onClick={(e) => {
e.stopPropagation()
}}
style={{
color: colors.text.muted,
backgroundColor: "rgba(255, 255, 255, 0.1)",
backdropFilter: "blur(4px)",
}}
type="button"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Document</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this document and all its
related memories? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
onClick={(e) => {
e.stopPropagation()
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-red-600 hover:bg-red-700 text-white"
onClick={(e) => {
e.stopPropagation()
onDelete()
}}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<CardContent className="p-0">
{image && !imageError && (
<div className="relative h-38 bg-gray-100 overflow-hidden">
@ -75,16 +137,18 @@ export const WebsiteCard = ({
<div className="px-4 py-2 space-y-2">
<div className="font-semibold text-sm line-clamp-2 leading-tight flex items-center justify-between">
{title}
{showExternalLink && (
<button
onClick={handleExternalLinkClick}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-100 flex-shrink-0"
type="button"
aria-label="Open in new tab"
>
<ExternalLink className="w-3 h-3" />
</button>
)}
<div className="flex items-center gap-1">
{showExternalLink && (
<button
onClick={handleExternalLinkClick}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-100 flex-shrink-0"
type="button"
aria-label="Open in new tab"
>
<ExternalLink className="w-3 h-3" />
</button>
)}
</div>
</div>
{description && (

View file

@ -1,7 +1,17 @@
import { Button } from "@ui/components/button"
import { Logo, LogoFull } from "@ui/assets/Logo"
import Link from "next/link"
import { MoonIcon, Plus, SunIcon, MonitorIcon, Network } from "lucide-react"
import {
MoonIcon,
Plus,
SunIcon,
MonitorIcon,
Network,
User,
CreditCard,
Chrome,
LogOut,
} from "lucide-react"
import {
DropdownMenuContent,
DropdownMenuTrigger,
@ -93,13 +103,27 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push("/settings")}>
<User className="h-4 w-4 mr-2" />
Profile
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => router.push("/settings/billing")}
>
<CreditCard className="h-4 w-4 mr-2" />
Billing
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
window.open(
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
"_blank",
"noopener,noreferrer",
)
}}
>
<Chrome className="h-4 w-4 mr-2" />
Chrome Extension
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center justify-between p-2 cursor-default hover:bg-transparent focus:bg-transparent data-[highlighted]:bg-transparent"
onSelect={(e) => e.preventDefault()}
@ -164,6 +188,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleSignOut()}>
<LogOut className="h-4 w-4 mr-2" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>

View file

@ -63,6 +63,7 @@ const DocumentCard = memo(
description={document.content}
activeMemories={activeMemories}
lastModified={document.updatedAt || document.createdAt}
onDelete={() => onDelete(document)}
/>
)
}
@ -77,6 +78,7 @@ const DocumentCard = memo(
document.metadata?.sm_internal_twitter_metadata as unknown as Tweet
}
activeMemories={activeMemories}
onDelete={() => onDelete(document)}
/>
)
}
@ -87,6 +89,7 @@ const DocumentCard = memo(
url={document.url}
title={document.title || "Untitled Document"}
image={document.ogImage}
onDelete={() => onDelete(document)}
/>
)
}
@ -212,9 +215,7 @@ export const MasonryMemoryList = ({
) : isLoading ? (
<div className="h-full flex items-center justify-center p-4">
<div className="rounded-xl overflow-hidden">
<div
className="relative z-10 px-6 py-4"
>
<div className="relative z-10 px-6 py-4">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 animate-spin text-blue-400" />
<span>Loading memory list...</span>
@ -232,6 +233,7 @@ export const MasonryMemoryList = ({
data-theme="light"
>
<Masonry
key={`masonry-${filteredDocuments.length}-${filteredDocuments.map((d) => d.id).join(",")}`}
items={filteredDocuments}
render={renderDocumentCard}
columnGutter={16}

View file

@ -1,67 +1,67 @@
import { Button } from '@repo/ui/components/button';
import { Loader2, type LucideIcon } from 'lucide-react';
import { motion } from 'motion/react';
import { Button } from "@repo/ui/components/button"
import { Loader2, type LucideIcon } from "lucide-react"
import { motion } from "motion/react"
interface ActionButtonsProps {
onCancel: () => void;
onSubmit?: () => void;
submitText: string;
submitIcon?: LucideIcon;
isSubmitting?: boolean;
isSubmitDisabled?: boolean;
submitType?: 'button' | 'submit';
className?: string;
onCancel: () => void
onSubmit?: () => void
submitText: string
submitIcon?: LucideIcon
isSubmitting?: boolean
isSubmitDisabled?: boolean
submitType?: "button" | "submit"
className?: string
}
export function ActionButtons({
onCancel,
onSubmit,
submitText,
submitIcon: SubmitIcon,
isSubmitting = false,
isSubmitDisabled = false,
submitType = 'submit',
className = '',
onCancel,
onSubmit,
submitText,
submitIcon: SubmitIcon,
isSubmitting = false,
isSubmitDisabled = false,
submitType = "submit",
className = "",
}: ActionButtonsProps) {
return (
<div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
<Button
className="hover:bg-foreground/10 border-none flex-1 sm:flex-initial"
onClick={onCancel}
type="button"
variant="ghost"
>
Cancel
</Button>
return (
<div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
<Button
className="hover:bg-foreground/10 border-none flex-1 sm:flex-initial"
onClick={onCancel}
type="button"
variant="ghost"
>
Cancel
</Button>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="flex-1 sm:flex-initial"
>
<Button
className="bg-foreground hover:bg-foreground/20 border-foreground/20 w-full"
disabled={isSubmitting || isSubmitDisabled}
onClick={submitType === 'button' ? onSubmit : undefined}
type={submitType}
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
{submitText.includes('Add')
? 'Adding...'
: submitText.includes('Upload')
? 'Uploading...'
: 'Processing...'}
</>
) : (
<>
{SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
{submitText}
</>
)}
</Button>
</motion.div>
</div>
);
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="flex-1 sm:flex-initial"
>
<Button
className="w-full"
disabled={isSubmitting || isSubmitDisabled}
onClick={submitType === "button" ? onSubmit : undefined}
type={submitType}
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
{submitText.includes("Add")
? "Adding..."
: submitText.includes("Upload")
? "Uploading..."
: "Processing..."}
</>
) : (
<>
{SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
{submitText}
</>
)}
</Button>
</motion.div>
</div>
)
}

View file

@ -88,7 +88,10 @@ export function AddMemoryView({
const [newProjectName, setNewProjectName] = useState("")
// Check memory limits
const { data: memoriesCheck } = fetchMemoriesFeature(autumn, !autumn.isLoading)
const { data: memoriesCheck } = fetchMemoriesFeature(
autumn,
!autumn.isLoading,
)
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
@ -757,7 +760,7 @@ export function AddMemoryView({
{({ state, handleChange, handleBlur }) => (
<>
<Input
className={`bg-black/5 border-black/10 text-black ${
className={`bg-black/5 border-black/10 ${
addContentMutation.isPending ? "opacity-50" : ""
}`}
disabled={addContentMutation.isPending}

View file

@ -118,17 +118,9 @@ export function BillingView() {
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Memories</span>
<span className="text-sm text-foreground">
{memoriesUsed} / {memoriesLimit}
Unlimited
</span>
</div>
<div className="w-full bg-muted-foreground/50 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{
width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
}}
/>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">

View file

@ -4,6 +4,7 @@ import { useAuth } from "@lib/auth-context"
import { generateId } from "@lib/generate-id"
import {
ADD_MEMORY_SHORTCUT_URL,
RAYCAST_EXTENSION_URL,
SEARCH_MEMORY_SHORTCUT_URL,
} from "@repo/lib/constants"
import { fetchConnectionsFeature } from "@repo/lib/queries"
@ -20,9 +21,17 @@ import type { ConnectionResponseSchema } from "@repo/validation/api"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
import { Check, Copy, Smartphone, Trash2 } from "lucide-react"
import {
Check,
Copy,
DownloadIcon,
KeyIcon,
Smartphone,
Trash2,
} from "lucide-react"
import { motion } from "motion/react"
import Image from "next/image"
import { useSearchParams } from "next/navigation"
import { useEffect, useId, useState } from "react"
import { toast } from "sonner"
import type { z } from "zod"
@ -87,6 +96,7 @@ export function IntegrationsView() {
const queryClient = useQueryClient()
const { selectedProject } = useProject()
const autumn = useCustomer()
const searchParams = useSearchParams()
const [showApiKeyModal, setShowApiKeyModal] = useState(false)
const [apiKey, setApiKey] = useState<string>("")
const [copied, setCopied] = useState(false)
@ -94,7 +104,12 @@ export function IntegrationsView() {
const [selectedShortcutType, setSelectedShortcutType] = useState<
"add" | "search" | null
>(null)
const [showRaycastApiKeyModal, setShowRaycastApiKeyModal] = useState(false)
const [raycastApiKey, setRaycastApiKey] = useState<string>("")
const [raycastCopied, setRaycastCopied] = useState(false)
const [hasTriggeredRaycast, setHasTriggeredRaycast] = useState(false)
const apiKeyId = useId()
const raycastApiKeyId = useId()
const handleUpgrade = async () => {
try {
@ -233,7 +248,7 @@ export function IntegrationsView() {
setApiKey(apiKey)
setShowApiKeyModal(true)
setCopied(false)
handleCopyApiKey()
handleCopyApiKey(apiKey)
},
onError: (error) => {
toast.error("Failed to create API key", {
@ -242,12 +257,54 @@ export function IntegrationsView() {
},
})
const createRaycastApiKeyMutation = useMutation({
mutationFn: async () => {
if (!org?.id) {
throw new Error("Organization ID is required")
}
const res = await authClient.apiKey.create({
metadata: {
organizationId: org?.id,
type: "raycast-extension",
},
name: `raycast-${generateId().slice(0, 8)}`,
prefix: `sm_${org?.id}_`,
})
return res.key
},
onSuccess: (apiKey) => {
setRaycastApiKey(apiKey)
setShowRaycastApiKeyModal(true)
setRaycastCopied(false)
handleCopyApiKey(apiKey)
},
onError: (error) => {
toast.error("Failed to create Raycast API key", {
description: error instanceof Error ? error.message : "Unknown error",
})
},
})
useEffect(() => {
const qParam = searchParams.get("q")
if (
qParam === "raycast" &&
!hasTriggeredRaycast &&
!createRaycastApiKeyMutation.isPending &&
org?.id
) {
setHasTriggeredRaycast(true)
createRaycastApiKeyMutation.mutate()
}
}, [searchParams, hasTriggeredRaycast, createRaycastApiKeyMutation, org])
const handleShortcutClick = (shortcutType: "add" | "search") => {
setSelectedShortcutType(shortcutType)
createApiKeyMutation.mutate()
}
const handleCopyApiKey = async () => {
const handleCopyApiKey = async (apiKey: string) => {
try {
await navigator.clipboard.writeText(apiKey)
setCopied(true)
@ -281,6 +338,18 @@ export function IntegrationsView() {
}
}
const handleRaycastDialogClose = (open: boolean) => {
setShowRaycastApiKeyModal(open)
if (!open) {
setRaycastApiKey("")
setRaycastCopied(false)
}
}
const handleRaycastClick = () => {
createRaycastApiKeyMutation.mutate()
}
return (
<div className="space-y-4 sm:space-y-4 custom-scrollbar">
{/* iOS Shortcuts */}
@ -336,6 +405,64 @@ export function IntegrationsView() {
</div>
</div>
{/* Raycast Extension */}
<div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
<div className="p-4 sm:p-5">
<div className="flex items-start gap-3 mb-3">
<div className="p-2 bg-purple-500/10 rounded-lg flex-shrink-0">
<svg
width="24"
height="24"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Raycast Icon</title>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7 18.079V21L0 14L1.46 12.54L7 18.081V18.079ZM9.921 21H7L14 28L15.46 26.54L9.921 21ZM26.535 15.462L27.996 14L13.996 0L12.538 1.466L18.077 7.004H14.73L10.864 3.146L9.404 4.606L11.809 7.01H10.129V17.876H20.994V16.196L23.399 18.6L24.859 17.14L20.994 13.274V9.927L26.535 15.462ZM7.73 6.276L6.265 7.738L7.833 9.304L9.294 7.844L7.73 6.276ZM20.162 18.708L18.702 20.17L20.268 21.738L21.73 20.276L20.162 18.708ZM4.596 9.41L3.134 10.872L7 14.738V11.815L4.596 9.41ZM16.192 21.006H13.268L17.134 24.872L18.596 23.41L16.192 21.006Z"
fill="#FF6363"
/>
</svg>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-card-foreground font-semibold text-base mb-1">
Raycast Extension
</h3>
<p className="text-muted-foreground text-sm leading-relaxed">
Add and search memories directly from Raycast on Mac and
Windows.
</p>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<Button
variant="secondary"
className="flex-1"
onClick={handleRaycastClick}
disabled={createRaycastApiKeyMutation.isPending}
>
<KeyIcon className="h-4 w-4" />
{createRaycastApiKeyMutation.isPending
? "Generating..."
: "Get API Key"}
</Button>
<Button
variant="secondary"
className="flex-1"
onClick={() => {
window.open(RAYCAST_EXTENSION_URL, "_blank")
analytics.extensionInstallClicked()
}}
>
<DownloadIcon className="h-4 w-4" />
Install Extension
</Button>
</div>
</div>
</div>
{/* Chrome Extension */}
<div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
<div className="p-4 sm:p-5">
@ -630,8 +757,8 @@ export function IntegrationsView() {
<Button
size="sm"
variant="ghost"
onClick={handleCopyApiKey}
className="hover:bg-accent"
onClick={() => handleCopyApiKey(apiKey)}
className="text-white/70 hover:text-white hover:bg-white/10"
>
{copied ? (
<Check className="h-4 w-4 text-chart-2" />
@ -696,6 +823,115 @@ export function IntegrationsView() {
</DialogContent>
</DialogPortal>
</Dialog>
<Dialog
open={showRaycastApiKeyModal}
onOpenChange={handleRaycastDialogClose}
>
<DialogPortal>
<DialogContent className="bg-card border-border text-card-foreground md:max-w-md z-[100]">
<DialogHeader>
<DialogTitle className="text-card-foreground text-lg font-semibold">
Setup Raycast Extension
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* API Key Section */}
<div className="space-y-2">
<label
htmlFor={raycastApiKeyId}
className="text-sm font-medium text-muted-foreground"
>
Your Raycast API Key
</label>
<div className="flex items-center gap-2">
<input
id={raycastApiKeyId}
type="text"
value={raycastApiKey}
readOnly
className="flex-1 bg-input border border-border rounded-lg px-3 py-2 text-sm text-foreground font-mono"
/>
<Button
size="sm"
variant="ghost"
onClick={() => handleCopyApiKey(raycastApiKey)}
className="text-muted-foreground hover:text-foreground hover:bg-accent"
>
{raycastCopied ? (
<Check className="h-4 w-4 text-chart-2" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
{/* Steps */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-muted-foreground">
Follow these steps:
</h4>
<div className="space-y-2">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
1
</div>
<p className="text-sm text-muted-foreground">
Install the Raycast extension from the Raycast Store
</p>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
2
</div>
<p className="text-sm text-muted-foreground">
Open Raycast preferences and paste your API key
</p>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
3
</div>
<p className="text-sm text-muted-foreground">
Use "Add Memory" or "Search Memories" commands!
</p>
</div>
</div>
</div>
<div className="flex gap-2 pt-2">
<Button
onClick={() => {
window.open(RAYCAST_EXTENSION_URL, "_blank")
analytics.extensionInstallClicked()
}}
className="flex-1"
variant="default"
>
<svg
width="24"
height="24"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Raycast Icon</title>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7 18.079V21L0 14L1.46 12.54L7 18.081V18.079ZM9.921 21H7L14 28L15.46 26.54L9.921 21ZM26.535 15.462L27.996 14L13.996 0L12.538 1.466L18.077 7.004H14.73L10.864 3.146L9.404 4.606L11.809 7.01H10.129V17.876H20.994V16.196L23.399 18.6L24.859 17.14L20.994 13.274V9.927L26.535 15.462ZM7.73 6.276L6.265 7.738L7.833 9.304L9.294 7.844L7.73 6.276ZM20.162 18.708L18.702 20.17L20.268 21.738L21.73 20.276L20.162 18.708ZM4.596 9.41L3.134 10.872L7 14.738V11.815L4.596 9.41ZM16.192 21.006H13.268L17.134 24.872L18.596 23.41L16.192 21.006Z"
fill="#FF6363"
/>
</svg>
Install Extension
</Button>
</div>
</div>
</DialogContent>
</DialogPortal>
</Dialog>
</div>
)
}

View file

@ -37,7 +37,10 @@ export function ProfileView() {
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
const { data: connectionsCheck } = fetchConnectionsFeature(autumn, !isCheckingStatus && !autumn.isLoading)
const { data: connectionsCheck } = fetchConnectionsFeature(
autumn,
!isCheckingStatus && !autumn.isLoading,
)
const connectionsUsed = connectionsCheck?.usage ?? 0
const handleUpgrade = async () => {
@ -190,26 +193,32 @@ export function ProfileView() {
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Memories</span>
<span
className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-500" : "text-foreground"}`}
>
{memoriesUsed} / {memoriesLimit}
</span>
</div>
<div className="w-full bg-muted-foreground/50 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
memoriesUsed >= memoriesLimit
? "bg-red-500"
: isPro
? "bg-green-500"
: "bg-blue-500"
}`}
style={{
width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
}}
/>
{isPro ? (
<span className="text-sm text-foreground">Unlimited</span>
) : (
<span
className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-500" : "text-foreground"}`}
>
{memoriesUsed} / {memoriesLimit}
</span>
)}
</div>
{!isPro && (
<div className="w-full bg-muted-foreground/50 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
memoriesUsed >= memoriesLimit
? "bg-red-500"
: isPro
? "bg-green-500"
: "bg-blue-500"
}`}
style={{
width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
}}
/>
</div>
)}
</div>
{isPro && (
@ -322,4 +331,4 @@ export function ProfileView() {
)}
</div>
)
}
}

View file

@ -0,0 +1,13 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5502_15741)">
<path d="M13.44 17.3199C13.12 17.3999 12.64 17.5599 12.04 17.5599C10.96 17.5599 9.92002 17.2799 9.04 16.6799C8.15998 16.0799 7.48002 15.3199 6.99998 14.3599L1.87999 5.3999L1.51999 5.99989C0.519977 7.79992 0 9.7999 0 11.9999C0 14.9999 1.00002 17.6399 2.91999 19.8799C4.84001 22.0799 7.31999 23.4399 10.2 23.8799L10.48 23.9199L14.52 16.9999L13.44 17.3199ZM10.04 22.9999C7.47998 22.5599 5.27997 21.3199 3.51998 19.3199C1.71996 17.2399 0.800005 14.7999 0.800005 11.9999C0.800005 10.1999 1.16 8.51991 1.87999 7.0399L6.19998 14.7199C6.71995 15.7599 7.47998 16.6399 8.51998 17.3199C9.55998 17.9599 10.72 18.3199 11.96 18.3199C12.28 18.3199 12.56 18.2799 12.8 18.2399L10.04 22.9999Z" fill="white"/>
<path d="M6.59935 10.52C6.87933 9.32003 7.55933 8.32001 8.59934 7.52001C9.55932 6.75999 10.6793 6.39999 11.9993 6.39999H22.6393L22.2793 5.8C21.2793 4.07999 19.8793 2.71998 17.9593 1.60001C16.1593 0.519977 14.1593 0 11.9993 0C10.1593 0 8.35934 0.399979 6.75933 1.19998C5.03932 2.03997 3.55931 3.24 2.51936 4.64L2.35938 4.84001L6.35935 11.4L6.59935 10.52ZM3.31936 4.91998C4.27935 3.67996 5.55935 2.67999 7.11937 1.91997C8.59938 1.15995 10.2394 0.799957 11.9994 0.799957C13.9994 0.799957 15.8793 1.31993 17.5193 2.31995C19.1193 3.23995 20.2793 4.31994 21.1994 5.63997H11.9593C10.4793 5.63997 9.19933 6.03995 8.07936 6.91997C7.15936 7.63996 6.47935 8.47995 6.07937 9.47996L3.31936 4.91998Z" fill="white"/>
<path d="M23.1175 7.48023L22.9975 7.24023H15.0375L15.7175 7.92024C16.9175 9.12022 17.5575 10.5603 17.5575 12.0002C17.5575 13.1602 17.2375 14.2402 16.5575 15.2003L11.4375 24.0003H12.1175C15.3975 23.9603 18.1975 22.7602 20.5175 20.4402C22.8375 18.1202 23.9975 15.2803 23.9975 12.0002C23.9975 10.8002 23.8375 9.08024 23.1175 7.48023ZM19.9575 19.8802C17.9575 21.8802 15.5975 22.9603 12.8375 23.1602L17.1976 15.6402C17.9576 14.5202 18.3176 13.3202 18.3176 12.0002C18.3176 10.6002 17.8376 9.24022 16.8776 8.04024H22.4776C23.0776 9.44024 23.1976 10.9602 23.1976 12.0002C23.1975 15.0402 22.1175 17.6802 19.9575 19.8802Z" fill="white"/>
<path d="M7.19531 11.9997C7.19531 14.5997 9.39531 16.7997 11.9953 16.7997C14.5953 16.7997 16.7953 14.5997 16.7953 11.9997C16.7953 9.39971 14.5953 7.19971 11.9953 7.19971C9.39531 7.19971 7.19531 9.39971 7.19531 11.9997ZM11.9953 7.99971C14.1553 7.99971 15.9953 9.83972 15.9953 11.9997C15.9953 14.1597 14.1553 15.9997 11.9953 15.9997C9.83532 15.9997 7.99532 14.1597 7.99532 11.9997C7.99532 9.83972 9.83532 7.99971 11.9953 7.99971Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_5502_15741">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -15,6 +15,7 @@
"packageManager": "bun@1.2.17",
"workspaces": [
"apps/*",
"!apps/raycast-extension",
"packages/*"
],
"dependencies": {

View file

@ -4,10 +4,12 @@ const SEARCH_MEMORY_SHORTCUT_URL =
"https://www.icloud.com/shortcuts/f2b5c544372844a38ab4c6900e2a88de"
const ADD_MEMORY_SHORTCUT_URL =
"https://www.icloud.com/shortcuts/0fd3e855be444845b457f94c78c2c8d9"
const RAYCAST_EXTENSION_URL = "https://www.raycast.com/supermemory/supermemory"
export {
BIG_DIMENSIONS_NEW,
DEFAULT_PROJECT_ID,
SEARCH_MEMORY_SHORTCUT_URL,
ADD_MEMORY_SHORTCUT_URL,
RAYCAST_EXTENSION_URL,
}

View file

@ -15,13 +15,13 @@ export function ExternalAuthButton({
return (
<Button
className={cn(
"flex flex-grow cursor-pointer max-w-full bg-sm-shark items-center justify-center gap-[0.625rem] rounded-xl border-[1.5px] border-sm-white px-6 py-5 hover:bg-sm-shark-alt",
"flex flex-grow cursor-pointer max-w-full bg-background items-center justify-center gap-[0.625rem] rounded-xl border-[1.5px] border-border px-6 py-5 hover:bg-accent",
className,
)}
{...props}
>
<span className="aspect-square">{authIcon}</span>
<span className="text-sm-white text-left text-[0.875rem] tracking-[-0.2px] leading-[1.25rem]">
<span className="text-foreground text-left text-[0.875rem] tracking-[-0.2px] leading-[1.25rem]">
Continue with {authProvider}
</span>
</Button>

View file

@ -14,11 +14,11 @@ export function TextSeparator({
className={cn("flex gap-4 items-center justify-center", className)}
{...props}
>
<div className="w-full h-px bg-sm-gray" />
<span className="text-sm-gray text-[0.75rem] uppercase tracking-[-0.2px] leading-[0.875rem]">
<div className="w-full h-px bg-border" />
<span className="text-muted-foreground text-[0.75rem] uppercase tracking-[-0.2px] leading-[0.875rem]">
{text}
</span>
<div className="w-full h-px bg-sm-gray" />
<div className="w-full h-px bg-border" />
</div>
);
}

View file

@ -21,11 +21,11 @@ export function LabeledInput({
}: LabeledInputProps) {
return (
<div className={cn("flex flex-col gap-2", className)} {...props}>
<Label1Regular className="text-sm-white">{label}</Label1Regular>
<Label1Regular className="text-foreground">{label}</Label1Regular>
<Input
className={cn(
"w-full leading-[1.375rem] tracking-[-0.4px] rounded-2xl p-5 placeholder:text-sm-gray text-sm-white border-[1.5px] border-sm-gray disabled:cursor-not-allowed disabled:opacity-50",
"w-full leading-[1.375rem] tracking-[-0.4px] rounded-2xl p-5 placeholder:text-muted-foreground text-foreground border-[1.5px] border-border disabled:cursor-not-allowed disabled:opacity-50",
inputProps?.className,
)}
placeholder={inputPlaceholder}

View file

@ -188,10 +188,10 @@ export function LoginPage({
{submittedEmail ? (
<div className="w-full max-w-md lg:max-w-none lg:col-span-5 flex flex-col gap-4 lg:gap-6 min-h-2/3 ">
<div className="flex flex-col gap-2 text-center lg:text-left">
<Title1Bold className="text-sm-white">Almost there!</Title1Bold>
<HeadingH3Medium className="text-sm-gray">
<Title1Bold className="text-foreground">Almost there!</Title1Bold>
<HeadingH3Medium className="text-muted-foreground">
Click the magic link we've sent to{" "}
<span className="text-sm-white">{submittedEmail}</span>.
<span className="text-foreground">{submittedEmail}</span>.
</HeadingH3Medium>
</div>
@ -221,11 +221,11 @@ export function LoginPage({
) : (
<div className="w-full max-w-md lg:max-w-none lg:col-span-5 flex flex-col gap-4 lg:gap-6 min-h-2/3 ">
<div className="flex flex-col gap-2 text-center lg:text-left md:mb-12">
<Title1Bold className="text-sm-white flex flex-col justify-center md:justify-start md:flex-row items-center gap-3">
<Title1Bold className="text-foreground flex flex-col justify-center md:justify-start md:flex-row items-center gap-3">
<span className="block md:hidden">Welcome to </span>{" "}
<LogoFull className="h-8" />
</Title1Bold>
<HeadingH1Medium className="text-sm-silver-chalice">
<HeadingH1Medium className="text-muted-foreground">
{heroText}
</HeadingH1Medium>
</div>
@ -348,7 +348,7 @@ export function LoginPage({
<ExternalAuthButton
authIcon={
<svg
className="w-4 h-4 sm:w-5 sm:h-5"
className="w-4 h-4 sm:w-5 sm:h-5 text-foreground"
fill="none"
height="25"
viewBox="0 0 26 25"
@ -360,14 +360,14 @@ export function LoginPage({
<path
clipRule="evenodd"
d="M12.9635 0.214844C6.20975 0.214844 0.75 5.71484 0.75 12.5191C0.75 17.9581 4.24825 22.5621 9.10125 24.1916C9.708 24.3141 9.93025 23.9268 9.93025 23.6011C9.93025 23.3158 9.91025 22.3381 9.91025 21.3193C6.51275 22.0528 5.80525 19.8526 5.80525 19.8526C5.25925 18.4266 4.45025 18.0601 4.45025 18.0601C3.33825 17.3063 4.53125 17.3063 4.53125 17.3063C5.76475 17.3878 6.412 18.5693 6.412 18.5693C7.50375 20.4433 9.263 19.9138 9.97075 19.5878C10.0718 18.7933 10.3955 18.2433 10.7393 17.9378C8.0295 17.6526 5.1785 16.5933 5.1785 11.8671C5.1785 10.5226 5.6635 9.42259 6.432 8.56709C6.31075 8.26159 5.886 6.99834 6.5535 5.30759C6.5535 5.30759 7.58475 4.98159 9.91 6.57059C10.9055 6.30126 11.9322 6.16425 12.9635 6.16309C13.9948 6.16309 15.046 6.30584 16.0168 6.57059C18.3423 4.98159 19.3735 5.30759 19.3735 5.30759C20.041 6.99834 19.616 8.26159 19.4948 8.56709C20.2835 9.42259 20.7485 10.5226 20.7485 11.8671C20.7485 16.5933 17.8975 17.6321 15.1675 17.9378C15.6125 18.3248 15.9965 19.0581 15.9965 20.2193C15.9965 21.8693 15.9765 23.1936 15.9765 23.6008C15.9765 23.9268 16.199 24.3141 16.8055 24.1918C21.6585 22.5618 25.1568 17.9581 25.1568 12.5191C25.1768 5.71484 19.697 0.214844 12.9635 0.214844Z"
fill="white"
fill="currentColor"
fillRule="evenodd"
/>
</g>
<defs>
<clipPath id="clip0_2579_3356">
<rect
fill="white"
fill="currentColor"
height="24"
transform="translate(0.75 0.214844)"
width="24.5"
@ -408,18 +408,18 @@ export function LoginPage({
) : null}
</div>
<Label1Regular className="text-sm-gray text-center text-xs sm:text-sm">
<Label1Regular className="text-muted-foreground text-center text-xs sm:text-sm">
By continuing, you agree to our{" "}
<span className="inline-block">
<a
className="text-sm-white hover:underline"
className="text-foreground hover:underline"
href="https://supermemory.ai/terms-of-service"
>
Terms
</a>{" "}
and{" "}
<a
className="text-sm-white hover:underline"
className="text-foreground hover:underline"
href="https://supermemory.ai/privacy-policy"
>
Privacy Policy

View file

@ -10,7 +10,7 @@ export function Label2Medium({
return (
<Comp
className={cn(
"text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-medium leading-[18px] tracking-[-0.4px] text-sm-silver-chalice",
"text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-medium leading-[18px] tracking-[-0.4px] text-muted-foreground",
className,
)}
{...props}

View file

@ -10,7 +10,7 @@ export function Label2Regular({
return (
<Comp
className={cn(
"text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-normal leading-[18px] tracking-[-0.4px] text-sm-silver-chalice",
"text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-normal leading-[18px] tracking-[-0.4px] text-muted-foreground",
className,
)}
{...props}

View file

@ -10,7 +10,7 @@ export function Label3Medium({
return (
<Comp
className={cn(
"text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-medium leading-[16px] tracking-[-0.2px] text-sm-silver-chalice",
"text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-medium leading-[16px] tracking-[-0.2px] text-muted-foreground",
className,
)}
{...props}

View file

@ -10,7 +10,7 @@ export function Label3Regular({
return (
<Comp
className={cn(
"text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-normal leading-[16px] tracking-[-0.2px] text-sm-silver-chalice",
"text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-normal leading-[16px] tracking-[-0.2px] text-muted-foreground",
className,
)}
{...props}