mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Merge branch 'main' of https://github.com/supermemoryai/supermemory
This commit is contained in:
commit
be34a14550
40 changed files with 4894 additions and 168 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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
14
apps/raycast-extension/.gitignore
vendored
Normal 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
|
||||
6
apps/raycast-extension/CHANGELOG.md
Normal file
6
apps/raycast-extension/CHANGELOG.md
Normal 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
|
||||
35
apps/raycast-extension/README.md
Normal file
35
apps/raycast-extension/README.md
Normal 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.
|
||||
BIN
apps/raycast-extension/assets/extension-icon.png
Normal file
BIN
apps/raycast-extension/assets/extension-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
4
apps/raycast-extension/eslint.config.js
Normal file
4
apps/raycast-extension/eslint.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const { defineConfig } = require("eslint/config")
|
||||
const raycastConfig = require("@raycast/eslint-config")
|
||||
|
||||
module.exports = defineConfig([...raycastConfig])
|
||||
BIN
apps/raycast-extension/metadata/supermemory-1.png
Normal file
BIN
apps/raycast-extension/metadata/supermemory-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
apps/raycast-extension/metadata/supermemory-2.png
Normal file
BIN
apps/raycast-extension/metadata/supermemory-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
3233
apps/raycast-extension/package-lock.json
generated
Normal file
3233
apps/raycast-extension/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
62
apps/raycast-extension/package.json
Normal file
62
apps/raycast-extension/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
110
apps/raycast-extension/src/add-memory.tsx
Normal file
110
apps/raycast-extension/src/add-memory.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
227
apps/raycast-extension/src/api.ts
Normal file
227
apps/raycast-extension/src/api.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
253
apps/raycast-extension/src/search-memories.tsx
Normal file
253
apps/raycast-extension/src/search-memories.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
apps/raycast-extension/tsconfig.json
Normal file
16
apps/raycast-extension/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
234
apps/web/components/chrome-extension-button.tsx
Normal file
234
apps/web/components/chrome-extension-button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
apps/web/public/icons/chrome-transparent.svg
Normal file
13
apps/web/public/icons/chrome-transparent.svg
Normal 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 |
BIN
apps/web/public/images/extension-bg.png
Normal file
BIN
apps/web/public/images/extension-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
apps/web/public/images/extension-logo.png
Normal file
BIN
apps/web/public/images/extension-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
|
|
@ -15,6 +15,7 @@
|
|||
"packageManager": "bun@1.2.17",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"!apps/raycast-extension",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue