aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/browser-extension/entrypoints/content/chatgpt.ts1
-rw-r--r--apps/browser-extension/entrypoints/content/selection-search.ts2
-rw-r--r--apps/browser-extension/entrypoints/content/shared.ts5
-rw-r--r--apps/browser-extension/entrypoints/content/twitter.ts21
-rw-r--r--apps/browser-extension/utils/api.ts5
-rw-r--r--apps/browser-extension/utils/constants.ts3
-rw-r--r--apps/browser-extension/utils/storage.ts3
-rw-r--r--apps/docs/style.css2
-rw-r--r--apps/mcp/src/server.ts4
-rw-r--r--apps/mcp/tsconfig.json36
-rw-r--r--apps/mcp/wrangler.jsonc12
-rw-r--r--apps/memory-graph-playground/src/app/page.tsx4
-rw-r--r--apps/raycast-extension/src/api.ts336
-rw-r--r--apps/raycast-extension/src/search-projects.tsx192
-rw-r--r--apps/raycast-extension/src/withSupermemory.tsx84
-rw-r--r--apps/raycast-extension/tsconfig.json28
-rw-r--r--apps/web/app/(navigation)/settings/billing/page.tsx2
-rw-r--r--apps/web/app/(navigation)/settings/integrations/page.tsx2
-rw-r--r--apps/web/app/(navigation)/settings/page.tsx2
-rw-r--r--apps/web/app/api/emails/welcome/route.tsx4
-rw-r--r--apps/web/app/api/onboarding/research/route.ts6
-rw-r--r--apps/web/app/layout.tsx4
-rw-r--r--apps/web/app/manifest.ts4
-rw-r--r--apps/web/app/new/onboarding/setup/page.tsx16
-rw-r--r--apps/web/app/new/onboarding/welcome/page.tsx4
-rw-r--r--apps/web/app/new/page.tsx33
-rw-r--r--apps/web/app/new/settings/page.tsx118
-rw-r--r--apps/web/app/not-found.tsx20
-rw-r--r--apps/web/app/onboarding/bio-form.tsx3
-rw-r--r--apps/web/app/onboarding/floating-orbs.tsx455
-rw-r--r--apps/web/app/onboarding/nav-menu.tsx97
-rw-r--r--apps/web/app/ref/[code]/page.tsx79
-rw-r--r--apps/web/app/ref/page.tsx12
-rw-r--r--apps/web/biome.json2
-rw-r--r--apps/web/components/glass-menu-effect.tsx8
-rw-r--r--apps/web/components/initial-header.tsx4
-rw-r--r--apps/web/components/install-prompt.tsx64
-rw-r--r--apps/web/components/memories-utils/index.tsx2
-rw-r--r--apps/web/components/menu.tsx5
-rw-r--r--apps/web/components/new/add-document/index.tsx172
-rw-r--r--apps/web/components/new/add-space-modal.tsx85
-rw-r--r--apps/web/components/new/chat/index.tsx110
-rw-r--r--apps/web/components/new/document-cards/note-preview.tsx8
-rw-r--r--apps/web/components/new/document-modal/content/notion-doc.tsx22
-rw-r--r--apps/web/components/new/document-modal/content/pdf.tsx3
-rw-r--r--apps/web/components/new/document-modal/content/web-page.tsx27
-rw-r--r--apps/web/components/new/document-modal/index.tsx44
-rw-r--r--apps/web/components/new/header.tsx338
-rw-r--r--apps/web/components/new/mcp-modal/index.tsx6
-rw-r--r--apps/web/components/new/onboarding/setup/chat-sidebar.tsx62
-rw-r--r--apps/web/components/new/onboarding/setup/integrations-step.tsx4
-rw-r--r--apps/web/components/new/space-selector.tsx87
-rw-r--r--apps/web/components/new/text-editor/index.tsx51
-rw-r--r--apps/web/components/new/text-editor/slash-command.tsx16
-rw-r--r--apps/web/components/new/utils.ts3
-rw-r--r--apps/web/components/spinner.tsx6
-rw-r--r--apps/web/components/text-morph.tsx129
-rw-r--r--apps/web/components/text-shimmer.tsx33
-rw-r--r--apps/web/components/views/add-memory/index.tsx9
-rw-r--r--apps/web/components/views/add-memory/project-selection.tsx156
-rw-r--r--apps/web/components/views/add-memory/tab-button.tsx42
-rw-r--r--apps/web/components/views/add-memory/text-editor.tsx511
-rw-r--r--apps/web/components/views/chat/chat-messages.tsx2
-rw-r--r--apps/web/components/views/connections-tab-content.tsx1
-rw-r--r--apps/web/components/views/projects.tsx142
-rw-r--r--apps/web/hooks/use-document-mutations.ts28
-rw-r--r--apps/web/hooks/use-project-name.ts22
-rw-r--r--apps/web/hooks/use-resize-observer.ts18
-rw-r--r--apps/web/instrumentation-client.ts36
-rw-r--r--apps/web/lib/analytics.ts20
-rw-r--r--apps/web/lib/mobile-panel-context.tsx22
-rw-r--r--apps/web/lib/view-mode-context.tsx80
-rw-r--r--apps/web/middleware.ts2
-rw-r--r--apps/web/open-next.config.ts6
-rw-r--r--apps/web/postcss.config.mjs4
-rw-r--r--apps/web/stores/chat.ts5
-rw-r--r--apps/web/stores/highlights.ts30
-rw-r--r--apps/web/stores/indexeddb-storage.ts44
-rw-r--r--apps/web/tsconfig.json36
-rw-r--r--apps/web/wrangler.jsonc58
-rw-r--r--packages/docs-test/run.ts185
-rw-r--r--packages/docs-test/tests/integrations/ai-sdk.ts138
-rw-r--r--packages/docs-test/tests/integrations/claude-memory.ts160
-rw-r--r--packages/docs-test/tests/integrations/openai-sdk.ts114
-rw-r--r--packages/docs-test/tests/typescript/sdk.ts213
-rw-r--r--packages/docs-test/tests/typescript/search.ts176
-rw-r--r--packages/docs-test/tests/typescript/user-profiles.ts116
-rw-r--r--packages/lib/auth-context.tsx6
-rw-r--r--packages/memory-graph/src/components/graph-canvas.tsx78
-rw-r--r--packages/memory-graph/src/components/memory-graph.tsx35
-rw-r--r--packages/memory-graph/src/components/navigation-controls.css.ts2
-rw-r--r--packages/memory-graph/src/components/node-detail-panel.tsx4
-rw-r--r--packages/memory-graph/src/components/node-popover.tsx201
-rw-r--r--packages/memory-graph/src/hooks/use-force-simulation.ts5
-rw-r--r--packages/memory-graph/src/hooks/use-graph-data.ts126
-rw-r--r--packages/memory-graph/src/hooks/use-graph-interactions.ts20
-rw-r--r--packages/tools/src/claude-memory-simple-example.ts90
-rw-r--r--packages/tools/test-supermemory.ts9
-rw-r--r--packages/tools/test/ai-sdk-test.ts2
-rw-r--r--packages/tools/test/anthropic-example.ts496
-rw-r--r--packages/tools/test/chatapp/app/layout.tsx46
-rw-r--r--packages/tools/test/chatapp/app/openai-chat/new/page.tsx2
-rw-r--r--packages/tools/test/chatapp/eslint.config.mjs30
-rw-r--r--packages/tools/test/chatapp/next.config.ts8
-rw-r--r--packages/tools/test/chatapp/postcss.config.mjs10
-rw-r--r--packages/tools/test/chatapp/tsconfig.json64
-rw-r--r--packages/tools/test/claude-memory-examples.ts80
-rw-r--r--packages/tools/test/claude-memory-real-example.ts97
-rw-r--r--packages/tools/test/claude-memory.test.ts9
-rw-r--r--packages/tools/test/test-memory-tool.ts33
-rw-r--r--packages/tools/test/with-supermemory/integration.test.ts205
-rw-r--r--packages/ui/button/external-auth.tsx6
-rw-r--r--packages/ui/components/grid-plus.tsx2
-rw-r--r--packages/ui/components/hover-card.tsx59
-rw-r--r--packages/ui/components/sonner.tsx22
-rw-r--r--packages/ui/input/labeled-input.tsx7
-rw-r--r--packages/ui/memory-graph/memory-graph.tsx76
-rw-r--r--packages/ui/memory-graph/navigation-controls.tsx97
-rw-r--r--packages/ui/pages/login.tsx6
119 files changed, 4068 insertions, 3131 deletions
diff --git a/apps/browser-extension/entrypoints/content/chatgpt.ts b/apps/browser-extension/entrypoints/content/chatgpt.ts
index d7cb9903..51a04736 100644
--- a/apps/browser-extension/entrypoints/content/chatgpt.ts
+++ b/apps/browser-extension/entrypoints/content/chatgpt.ts
@@ -209,7 +209,6 @@ function addSupermemoryButtonToMemoriesDialog() {
if (memoriesDialog.querySelector("#supermemory-save-button")) return
-
const deleteAllContainer = memoriesDialog.querySelector(
".flex.items-center.gap-0\\.5",
)
diff --git a/apps/browser-extension/entrypoints/content/selection-search.ts b/apps/browser-extension/entrypoints/content/selection-search.ts
index 6473b84c..57c87188 100644
--- a/apps/browser-extension/entrypoints/content/selection-search.ts
+++ b/apps/browser-extension/entrypoints/content/selection-search.ts
@@ -4,7 +4,7 @@ import { ELEMENT_IDS, MESSAGE_TYPES, UI_CONFIG } from "../../utils/constants"
let currentQuery = ""
let fabElement: HTMLElement | null = null
let panelElement: HTMLElement | null = null
-let selectedResults: Set<number> = new Set()
+const selectedResults: Set<number> = new Set()
/**
* Get the selection rectangle for positioning the FAB
diff --git a/apps/browser-extension/entrypoints/content/shared.ts b/apps/browser-extension/entrypoints/content/shared.ts
index 428dc0d3..68d117a1 100644
--- a/apps/browser-extension/entrypoints/content/shared.ts
+++ b/apps/browser-extension/entrypoints/content/shared.ts
@@ -117,7 +117,10 @@ export function setupStorageListener() {
}
try {
- await Promise.all([bearerToken.setValue(token), userData.setValue(user)])
+ await Promise.all([
+ bearerToken.setValue(token),
+ userData.setValue(user),
+ ])
} catch {
// Do nothing
}
diff --git a/apps/browser-extension/entrypoints/content/twitter.ts b/apps/browser-extension/entrypoints/content/twitter.ts
index 4ed67315..ffa138af 100644
--- a/apps/browser-extension/entrypoints/content/twitter.ts
+++ b/apps/browser-extension/entrypoints/content/twitter.ts
@@ -257,17 +257,20 @@ async function showOnboardingToast() {
const icon = document.createElement("img")
icon.src = iconUrl
icon.alt = "Supermemory"
- icon.style.cssText = "width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;"
+ icon.style.cssText =
+ "width: 24px; height: 24px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;"
const textContainer = document.createElement("div")
- textContainer.style.cssText = "display: flex; flex-direction: column; gap: 4px; flex: 1;"
+ textContainer.style.cssText =
+ "display: flex; flex-direction: column; gap: 4px; flex: 1;"
const title = document.createElement("span")
title.style.cssText = "font-weight: 600; font-size: 14px; color: #111827;"
title.textContent = "Import X/Twitter Bookmarks"
const description = document.createElement("span")
- description.style.cssText = "font-size: 13px; color: #6b7280; line-height: 1.4;"
+ description.style.cssText =
+ "font-size: 13px; color: #6b7280; line-height: 1.4;"
description.textContent =
"You can import all your Twitter bookmarks to Supermemory with one click."
@@ -362,10 +365,7 @@ async function showOnboardingToast() {
learnMoreButton.style.backgroundColor = "transparent"
})
learnMoreButton.addEventListener("click", () => {
- window.open(
- "https://docs.supermemory.ai/connectors/twitter",
- "_blank",
- )
+ window.open("https://docs.supermemory.ai/connectors/twitter", "_blank")
})
buttonsContainer.appendChild(importButton)
@@ -377,7 +377,10 @@ async function showOnboardingToast() {
progressBarContainer.setAttribute("aria-valuemin", "0")
progressBarContainer.setAttribute("aria-valuemax", "100")
progressBarContainer.setAttribute("aria-valuenow", "0")
- progressBarContainer.setAttribute("aria-label", "Onboarding toast auto-dismiss progress")
+ progressBarContainer.setAttribute(
+ "aria-label",
+ "Onboarding toast auto-dismiss progress",
+ )
progressBarContainer.style.cssText = `
position: absolute;
bottom: 0;
@@ -394,7 +397,7 @@ async function showOnboardingToast() {
transform-origin: left;
animation: smProgressGrow ${duration}ms linear forwards;
`
-
+
// Update progress bar ARIA value as animation progresses
const startTime = Date.now()
const updateProgress = () => {
diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts
index 1a22af04..dd42d078 100644
--- a/apps/browser-extension/utils/api.ts
+++ b/apps/browser-extension/utils/api.ts
@@ -114,7 +114,10 @@ export async function validateAuthToken(): Promise<boolean> {
/**
* Get user data from storage
*/
-export async function getUserData(): Promise<{ email?: string; name?: string } | null> {
+export async function getUserData(): Promise<{
+ email?: string
+ name?: string
+} | null> {
try {
return (await userData.getValue()) || null
} catch (error) {
diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts
index 5d61ffdf..58481706 100644
--- a/apps/browser-extension/utils/constants.ts
+++ b/apps/browser-extension/utils/constants.ts
@@ -33,7 +33,8 @@ export const ELEMENT_IDS = {
*/
export const STORAGE_KEYS = {
TWITTER_BOOKMARKS_ONBOARDING_SEEN: "sm_twitter_bookmarks_onboarding_seen",
- TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL: "sm_twitter_bookmarks_import_intent_until",
+ TWITTER_BOOKMARKS_IMPORT_INTENT_UNTIL:
+ "sm_twitter_bookmarks_import_intent_until",
} as const
/**
diff --git a/apps/browser-extension/utils/storage.ts b/apps/browser-extension/utils/storage.ts
index 974b6474..1c3c4860 100644
--- a/apps/browser-extension/utils/storage.ts
+++ b/apps/browser-extension/utils/storage.ts
@@ -2,7 +2,7 @@
* Centralized storage layer using WXT's built-in storage API
*/
-import { storage } from '#imports';
+import { storage } from "#imports"
import type { Project } from "./types"
/**
@@ -118,4 +118,3 @@ export async function getTokensLogged(): Promise<boolean> {
export async function setTokensLogged(): Promise<void> {
await tokensLogged.setValue(true)
}
-
diff --git a/apps/docs/style.css b/apps/docs/style.css
index b1a5db87..ff7208ae 100644
--- a/apps/docs/style.css
+++ b/apps/docs/style.css
@@ -1,5 +1,5 @@
.dark img[src*="openai.svg"],
.dark img[src*="pipecat.svg"],
.dark img[src*="supermemory.svg"] {
- filter: invert(1);
+ filter: invert(1);
}
diff --git a/apps/mcp/src/server.ts b/apps/mcp/src/server.ts
index d83d2326..6baa9d2c 100644
--- a/apps/mcp/src/server.ts
+++ b/apps/mcp/src/server.ts
@@ -224,7 +224,9 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
}
} catch (error) {
const message =
- error instanceof Error ? error.message : "An unexpected error occurred"
+ error instanceof Error
+ ? error.message
+ : "An unexpected error occurred"
return {
content: [
{
diff --git a/apps/mcp/tsconfig.json b/apps/mcp/tsconfig.json
index dead8a5f..576d1175 100644
--- a/apps/mcp/tsconfig.json
+++ b/apps/mcp/tsconfig.json
@@ -1,20 +1,20 @@
{
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "strict": true,
- "skipLibCheck": true,
- "lib": ["ESNext"],
- "types": ["@cloudflare/workers-types"],
- "jsx": "react-jsx",
- "jsxImportSource": "hono/jsx",
- "esModuleInterop": true,
- "resolveJsonModule": true,
- "outDir": "dist",
- "rootDir": "src",
- "baseUrl": ".",
- },
- "include": ["src/**/*"],
- "exclude": ["node_modules"],
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "skipLibCheck": true,
+ "lib": ["ESNext"],
+ "types": ["@cloudflare/workers-types"],
+ "jsx": "react-jsx",
+ "jsxImportSource": "hono/jsx",
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "baseUrl": "."
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules"]
}
diff --git a/apps/mcp/wrangler.jsonc b/apps/mcp/wrangler.jsonc
index 69996dfa..2260493a 100644
--- a/apps/mcp/wrangler.jsonc
+++ b/apps/mcp/wrangler.jsonc
@@ -9,12 +9,12 @@
"API_URL": "https://api.supermemory.ai"
},
"routes": [
- {
- "pattern": "mcp.supermemory.ai",
- "zone_name": "supermemory.ai",
- "custom_domain": true
- }
- ],
+ {
+ "pattern": "mcp.supermemory.ai",
+ "zone_name": "supermemory.ai",
+ "custom_domain": true
+ }
+ ],
"durable_objects": {
"bindings": [
{
diff --git a/apps/memory-graph-playground/src/app/page.tsx b/apps/memory-graph-playground/src/app/page.tsx
index 581557b6..0682905a 100644
--- a/apps/memory-graph-playground/src/app/page.tsx
+++ b/apps/memory-graph-playground/src/app/page.tsx
@@ -31,7 +31,9 @@ export default function Home() {
// State for slideshow
const [isSlideshowActive, setIsSlideshowActive] = useState(false)
- const [currentSlideshowNode, setCurrentSlideshowNode] = useState<string | null>(null)
+ const [currentSlideshowNode, setCurrentSlideshowNode] = useState<
+ string | null
+ >(null)
const PAGE_SIZE = 500
diff --git a/apps/raycast-extension/src/api.ts b/apps/raycast-extension/src/api.ts
index 55672a77..f36d82fb 100644
--- a/apps/raycast-extension/src/api.ts
+++ b/apps/raycast-extension/src/api.ts
@@ -1,222 +1,222 @@
-import { getPreferenceValues, showToast, Toast } from "@raycast/api";
+import { getPreferenceValues, showToast, Toast } from "@raycast/api"
export interface Project {
- id: string;
- name: string;
- containerTag: string;
- description?: string;
+ id: string
+ name: string
+ containerTag: string
+ description?: string
}
export interface Memory {
- id: string;
- content: string;
- title?: string;
- url?: string;
- containerTag?: string;
- createdAt: string;
+ 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;
+ 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>;
+ content: string
+ containerTags?: string[]
+ title?: string
+ url?: string
+ metadata?: Record<string, unknown>
}
interface AddProjectRequest {
- name: string;
+ name: string
}
export interface SearchRequest {
- q: string;
- containerTags?: string[];
- limit?: number;
+ q: string
+ containerTags?: string[]
+ limit?: number
}
export interface SearchResponse {
- results: SearchResult[];
- timing: number;
- total: number;
+ results: SearchResult[]
+ timing: number
+ total: number
}
-const API_BASE_URL = "https://api.supermemory.ai";
+const API_BASE_URL = "https://api.supermemory.ai"
class SupermemoryAPIError extends Error {
- constructor(
- message: string,
- public status?: number,
- ) {
- super(message);
- this.name = "SupermemoryAPIError";
- }
+ constructor(
+ message: string,
+ public status?: number,
+ ) {
+ super(message)
+ this.name = "SupermemoryAPIError"
+ }
}
class AuthenticationError extends Error {
- constructor(message: string) {
- super(message);
- this.name = "AuthenticationError";
- }
+ constructor(message: string) {
+ super(message)
+ this.name = "AuthenticationError"
+ }
}
function getApiKey(): string {
- const { apiKey } = getPreferenceValues<Preferences>();
- return apiKey;
+ const { apiKey } = getPreferenceValues<Preferences>()
+ return apiKey
}
async function makeAuthenticatedRequest<T>(
- endpoint: string,
- options: RequestInit = {},
+ endpoint: string,
+ options: RequestInit = {},
): Promise<T> {
- const apiKey = 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"}`,
- );
- }
+ const apiKey = 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;
- }
+ 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 addProject(request: AddProjectRequest): Promise<Project> {
- const response = await makeAuthenticatedRequest<Project>("/v3/projects", {
- method: "POST",
- body: JSON.stringify(request),
- });
+ const response = await makeAuthenticatedRequest<Project>("/v3/projects", {
+ method: "POST",
+ body: JSON.stringify(request),
+ })
- await showToast({
- style: Toast.Style.Success,
- title: "Project Added",
- message: "Successfully added project to Supermemory",
- });
+ await showToast({
+ style: Toast.Style.Success,
+ title: "Project Added",
+ message: "Successfully added project to Supermemory",
+ })
- return response;
+ return response
}
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;
- }
+ 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,
+ 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;
- }
+ 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 fetchSettings(): Promise<object> {
- const response = await makeAuthenticatedRequest<object>("/v3/settings");
- return response;
+ const response = await makeAuthenticatedRequest<object>("/v3/settings")
+ return response
}
diff --git a/apps/raycast-extension/src/search-projects.tsx b/apps/raycast-extension/src/search-projects.tsx
index bacd8537..02b423d5 100644
--- a/apps/raycast-extension/src/search-projects.tsx
+++ b/apps/raycast-extension/src/search-projects.tsx
@@ -1,106 +1,106 @@
import {
- ActionPanel,
- List,
- Action,
- Icon,
- Form,
- useNavigation,
-} from "@raycast/api";
-import { useState } from "react";
-import { fetchProjects, addProject } from "./api";
+ ActionPanel,
+ List,
+ Action,
+ Icon,
+ Form,
+ useNavigation,
+} from "@raycast/api"
+import { useState } from "react"
+import { fetchProjects, addProject } from "./api"
import {
- FormValidation,
- showFailureToast,
- useCachedPromise,
- useForm,
-} from "@raycast/utils";
-import { withSupermemory } from "./withSupermemory";
+ FormValidation,
+ showFailureToast,
+ useCachedPromise,
+ useForm,
+} from "@raycast/utils"
+import { withSupermemory } from "./withSupermemory"
-export default withSupermemory(Command);
+export default withSupermemory(Command)
function Command() {
- const { isLoading, data: projects, mutate } = useCachedPromise(fetchProjects);
+ const { isLoading, data: projects, mutate } = useCachedPromise(fetchProjects)
- return (
- <List isLoading={isLoading} searchBarPlaceholder="Search your projects">
- {!isLoading && !projects?.length ? (
- <List.EmptyView
- title="No Projects Found"
- actions={
- <ActionPanel>
- <Action.Push
- icon={Icon.Plus}
- title="Create Project"
- target={<CreateProject />}
- onPop={mutate}
- />
- </ActionPanel>
- }
- />
- ) : (
- projects?.map((project) => (
- <List.Item
- key={project.id}
- icon={Icon.Folder}
- title={project.name}
- subtitle={project.description}
- accessories={[{ tag: project.containerTag }]}
- actions={
- <ActionPanel>
- <Action.Push
- icon={Icon.Plus}
- title="Create Project"
- target={<CreateProject />}
- onPop={mutate}
- />
- </ActionPanel>
- }
- />
- ))
- )}
- </List>
- );
+ return (
+ <List isLoading={isLoading} searchBarPlaceholder="Search your projects">
+ {!isLoading && !projects?.length ? (
+ <List.EmptyView
+ title="No Projects Found"
+ actions={
+ <ActionPanel>
+ <Action.Push
+ icon={Icon.Plus}
+ title="Create Project"
+ target={<CreateProject />}
+ onPop={mutate}
+ />
+ </ActionPanel>
+ }
+ />
+ ) : (
+ projects?.map((project) => (
+ <List.Item
+ key={project.id}
+ icon={Icon.Folder}
+ title={project.name}
+ subtitle={project.description}
+ accessories={[{ tag: project.containerTag }]}
+ actions={
+ <ActionPanel>
+ <Action.Push
+ icon={Icon.Plus}
+ title="Create Project"
+ target={<CreateProject />}
+ onPop={mutate}
+ />
+ </ActionPanel>
+ }
+ />
+ ))
+ )}
+ </List>
+ )
}
function CreateProject() {
- const { pop } = useNavigation();
- const [isLoading, setIsLoading] = useState(false);
- const { handleSubmit, itemProps } = useForm<{ name: string }>({
- async onSubmit(values) {
- setIsLoading(true);
- try {
- await addProject(values);
- pop();
- } catch (error) {
- await showFailureToast(error, { title: "Failed to add project" });
- } finally {
- setIsLoading(false);
- }
- },
- validation: {
- name: FormValidation.Required,
- },
- });
- return (
- <Form
- navigationTitle="Search Projects / Add"
- isLoading={isLoading}
- actions={
- <ActionPanel>
- <Action.SubmitForm
- icon={Icon.Plus}
- title="Create Project"
- onSubmit={handleSubmit}
- />
- </ActionPanel>
- }
- >
- <Form.TextField
- title="Name"
- placeholder="My Awesome Project"
- info="This will help you organize your memories"
- {...itemProps.name}
- />
- </Form>
- );
+ const { pop } = useNavigation()
+ const [isLoading, setIsLoading] = useState(false)
+ const { handleSubmit, itemProps } = useForm<{ name: string }>({
+ async onSubmit(values) {
+ setIsLoading(true)
+ try {
+ await addProject(values)
+ pop()
+ } catch (error) {
+ await showFailureToast(error, { title: "Failed to add project" })
+ } finally {
+ setIsLoading(false)
+ }
+ },
+ validation: {
+ name: FormValidation.Required,
+ },
+ })
+ return (
+ <Form
+ navigationTitle="Search Projects / Add"
+ isLoading={isLoading}
+ actions={
+ <ActionPanel>
+ <Action.SubmitForm
+ icon={Icon.Plus}
+ title="Create Project"
+ onSubmit={handleSubmit}
+ />
+ </ActionPanel>
+ }
+ >
+ <Form.TextField
+ title="Name"
+ placeholder="My Awesome Project"
+ info="This will help you organize your memories"
+ {...itemProps.name}
+ />
+ </Form>
+ )
}
diff --git a/apps/raycast-extension/src/withSupermemory.tsx b/apps/raycast-extension/src/withSupermemory.tsx
index 13c1d7b8..9c789751 100644
--- a/apps/raycast-extension/src/withSupermemory.tsx
+++ b/apps/raycast-extension/src/withSupermemory.tsx
@@ -1,48 +1,48 @@
-import { usePromise } from "@raycast/utils";
-import { fetchSettings } from "./api";
+import { usePromise } from "@raycast/utils"
+import { fetchSettings } from "./api"
import {
- Action,
- ActionPanel,
- Detail,
- Icon,
- List,
- openExtensionPreferences,
-} from "@raycast/api";
-import { ComponentType } from "react";
+ Action,
+ ActionPanel,
+ Detail,
+ Icon,
+ List,
+ openExtensionPreferences,
+} from "@raycast/api"
+import type { ComponentType } from "react"
export function withSupermemory<P extends object>(Component: ComponentType<P>) {
- return function SupermemoryWrappedComponent(props: P) {
- const { isLoading, data } = usePromise(fetchSettings, [], {
- failureToastOptions: {
- title: "Invalid API Key",
- message:
- "Invalid API key. Please check your API key in preferences. Get a new one from https://supermemory.link/raycast",
- },
- });
+ return function SupermemoryWrappedComponent(props: P) {
+ const { isLoading, data } = usePromise(fetchSettings, [], {
+ failureToastOptions: {
+ title: "Invalid API Key",
+ message:
+ "Invalid API key. Please check your API key in preferences. Get a new one from https://supermemory.link/raycast",
+ },
+ })
- if (!data) {
- return isLoading ? (
- <Detail isLoading />
- ) : (
- <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>
- );
- }
+ if (!data) {
+ return isLoading ? (
+ <Detail isLoading />
+ ) : (
+ <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 <Component {...props} />;
- };
+ return <Component {...props} />
+ }
}
diff --git a/apps/raycast-extension/tsconfig.json b/apps/raycast-extension/tsconfig.json
index d33dd46c..cbf072b5 100644
--- a/apps/raycast-extension/tsconfig.json
+++ b/apps/raycast-extension/tsconfig.json
@@ -1,16 +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
- }
+ "$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
+ }
}
diff --git a/apps/web/app/(navigation)/settings/billing/page.tsx b/apps/web/app/(navigation)/settings/billing/page.tsx
index 2b8e6ba0..d260f20e 100644
--- a/apps/web/app/(navigation)/settings/billing/page.tsx
+++ b/apps/web/app/(navigation)/settings/billing/page.tsx
@@ -9,4 +9,4 @@ export default function BillingPage() {
<BillingView />
</div>
)
-} \ No newline at end of file
+}
diff --git a/apps/web/app/(navigation)/settings/integrations/page.tsx b/apps/web/app/(navigation)/settings/integrations/page.tsx
index 7fedd143..d2ad2900 100644
--- a/apps/web/app/(navigation)/settings/integrations/page.tsx
+++ b/apps/web/app/(navigation)/settings/integrations/page.tsx
@@ -7,4 +7,4 @@ export default function IntegrationsPage() {
<IntegrationsView />
</div>
)
-} \ No newline at end of file
+}
diff --git a/apps/web/app/(navigation)/settings/page.tsx b/apps/web/app/(navigation)/settings/page.tsx
index c9046fba..1f806e71 100644
--- a/apps/web/app/(navigation)/settings/page.tsx
+++ b/apps/web/app/(navigation)/settings/page.tsx
@@ -9,4 +9,4 @@ export default function ProfilePage() {
<ProfileView />
</div>
)
-} \ No newline at end of file
+}
diff --git a/apps/web/app/api/emails/welcome/route.tsx b/apps/web/app/api/emails/welcome/route.tsx
index 48883d6b..d8f6c59f 100644
--- a/apps/web/app/api/emails/welcome/route.tsx
+++ b/apps/web/app/api/emails/welcome/route.tsx
@@ -1,5 +1,5 @@
/** biome-ignore-all lint/performance/noImgElement: Not Next.js environment */
-import { ImageResponse } from "next/og";
+import { ImageResponse } from "next/og"
export async function GET() {
return new ImageResponse(
@@ -15,5 +15,5 @@ export async function GET() {
width: 1200,
height: 630,
},
- );
+ )
}
diff --git a/apps/web/app/api/onboarding/research/route.ts b/apps/web/app/api/onboarding/research/route.ts
index d0d6eded..5e9b933e 100644
--- a/apps/web/app/api/onboarding/research/route.ts
+++ b/apps/web/app/api/onboarding/research/route.ts
@@ -64,7 +64,11 @@ export async function POST(req: Request) {
},
{
type: "x",
- includedXHandles: [lowerUrl.replace("https://x.com/", "").replace("https://twitter.com/", "")],
+ includedXHandles: [
+ lowerUrl
+ .replace("https://x.com/", "")
+ .replace("https://twitter.com/", ""),
+ ],
postFavoriteCount: 10,
},
],
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 37e01cd7..88ce9988 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -8,7 +8,7 @@ import { PostHogProvider } from "@lib/posthog"
import { QueryProvider } from "../components/query-client"
import { AutumnProvider } from "autumn-js/react"
import { Suspense } from "react"
-import { Toaster } from "sonner"
+import { Toaster } from "@ui/components/sonner"
import { MobilePanelProvider } from "@/lib/mobile-panel-context"
import { NuqsAdapter } from "nuqs/adapters/next/app"
import { ThemeProvider } from "@/lib/theme-provider"
@@ -66,7 +66,7 @@ export default function RootLayout({
<ErrorTrackingProvider>
<NuqsAdapter>
<Suspense>{children}</Suspense>
- <Toaster richColors />
+ <Toaster />
</NuqsAdapter>
</ErrorTrackingProvider>
</PostHogProvider>
diff --git a/apps/web/app/manifest.ts b/apps/web/app/manifest.ts
index 9077d287..01381381 100644
--- a/apps/web/app/manifest.ts
+++ b/apps/web/app/manifest.ts
@@ -1,4 +1,4 @@
-import type { MetadataRoute } from "next";
+import type { MetadataRoute } from "next"
export default function manifest(): MetadataRoute.Manifest {
return {
@@ -16,5 +16,5 @@ export default function manifest(): MetadataRoute.Manifest {
type: "image/png",
},
],
- };
+ }
}
diff --git a/apps/web/app/new/onboarding/setup/page.tsx b/apps/web/app/new/onboarding/setup/page.tsx
index d601970d..e53d447b 100644
--- a/apps/web/app/new/onboarding/setup/page.tsx
+++ b/apps/web/app/new/onboarding/setup/page.tsx
@@ -8,6 +8,7 @@ import { IntegrationsStep } from "@/components/new/onboarding/setup/integrations
import { SetupHeader } from "@/components/new/onboarding/setup/header"
import { ChatSidebar } from "@/components/new/onboarding/setup/chat-sidebar"
import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
+import { useIsMobile } from "@hooks/use-mobile"
import { useSetupContext, type SetupStep } from "./layout"
@@ -32,6 +33,7 @@ function StepNotFound({ goToStep }: { goToStep: (step: SetupStep) => void }) {
export default function SetupPage() {
const { memoryFormData, currentStep, goToStep } = useSetupContext()
+ const isMobile = useIsMobile()
const renderStep = () => {
switch (currentStep) {
@@ -52,17 +54,21 @@ export default function SetupPage() {
<main className="relative min-h-screen">
<div className="relative z-10">
- <div className="flex flex-row h-[calc(100vh-90px)] relative">
- <div className="flex-1 flex flex-col items-center justify-start p-8">
+ <div className="flex flex-col lg:flex-row h-[calc(100vh-90px)] relative">
+ <div className="flex-1 flex flex-col items-center justify-start p-4 md:p-8">
<AnimatePresence mode="wait">{renderStep()}</AnimatePresence>
</div>
- <AnimatePresence mode="popLayout">
- <ChatSidebar formData={memoryFormData} />
- </AnimatePresence>
+ {!isMobile && (
+ <AnimatePresence mode="popLayout">
+ <ChatSidebar formData={memoryFormData} />
+ </AnimatePresence>
+ )}
</div>
</div>
</main>
+
+ {isMobile && <ChatSidebar formData={memoryFormData} />}
</div>
)
}
diff --git a/apps/web/app/new/onboarding/welcome/page.tsx b/apps/web/app/new/onboarding/welcome/page.tsx
index 5b579144..706cd40a 100644
--- a/apps/web/app/new/onboarding/welcome/page.tsx
+++ b/apps/web/app/new/onboarding/welcome/page.tsx
@@ -81,13 +81,13 @@ export default function WelcomePage() {
localStorage.setItem("username", name)
if (name.trim()) {
setIsSubmitting(true)
-
+
try {
await authClient.updateUser({ displayUsername: name.trim() })
} catch (error) {
console.error("Failed to update displayUsername:", error)
}
-
+
goToStep("greeting")
setIsSubmitting(false)
}
diff --git a/apps/web/app/new/page.tsx b/apps/web/app/new/page.tsx
index aad09a28..2417ccb7 100644
--- a/apps/web/app/new/page.tsx
+++ b/apps/web/app/new/page.tsx
@@ -10,32 +10,45 @@ import { MCPModal } from "@/components/new/mcp-modal"
import { HotkeysProvider } from "react-hotkeys-hook"
import { useHotkeys } from "react-hotkeys-hook"
import { AnimatePresence } from "motion/react"
+import { useIsMobile } from "@hooks/use-mobile"
+import { analytics } from "@/lib/analytics"
export default function NewPage() {
+ const isMobile = useIsMobile()
const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)
const [isMCPModalOpen, setIsMCPModalOpen] = useState(false)
- useHotkeys("c", () => setIsAddDocumentOpen(true))
- const [isChatOpen, setIsChatOpen] = useState(true)
+ useHotkeys("c", () => {
+ analytics.addDocumentModalOpened()
+ setIsAddDocumentOpen(true)
+ })
+ const [isChatOpen, setIsChatOpen] = useState(!isMobile)
return (
<HotkeysProvider>
- <div className="bg-black">
+ <div className="bg-black min-h-screen">
<AnimatedGradientBackground
topPosition="15%"
animateFromBottom={false}
/>
<Header
- onAddMemory={() => setIsAddDocumentOpen(true)}
- onOpenMCP={() => setIsMCPModalOpen(true)}
+ onAddMemory={() => {
+ analytics.addDocumentModalOpened()
+ setIsAddDocumentOpen(true)
+ }}
+ onOpenMCP={() => {
+ analytics.mcpModalOpened()
+ setIsMCPModalOpen(true)
+ }}
+ onOpenChat={() => setIsChatOpen(true)}
/>
<main
key={`main-container-${isChatOpen}`}
- className="z-10 flex flex-row relative"
+ className="z-10 flex flex-col md:flex-row relative"
>
- <div className="flex-1 p-6 pr-0">
+ <div className="flex-1 p-4 md:p-6 md:pr-0">
<MemoriesGrid isChatOpen={isChatOpen} />
</div>
- <div className="sticky top-0 h-screen">
+ <div className="hidden md:block md:sticky md:top-0 md:h-screen">
<AnimatePresence mode="popLayout">
<ChatSidebar
isChatOpen={isChatOpen}
@@ -45,6 +58,10 @@ export default function NewPage() {
</div>
</main>
+ {isMobile && (
+ <ChatSidebar isChatOpen={isChatOpen} setIsChatOpen={setIsChatOpen} />
+ )}
+
<AddDocumentModal
isOpen={isAddDocumentOpen}
onClose={() => setIsAddDocumentOpen(false)}
diff --git a/apps/web/app/new/settings/page.tsx b/apps/web/app/new/settings/page.tsx
index 2b5df3ca..f1972bb9 100644
--- a/apps/web/app/new/settings/page.tsx
+++ b/apps/web/app/new/settings/page.tsx
@@ -12,6 +12,7 @@ import Integrations from "@/components/new/settings/integrations"
import ConnectionsMCP from "@/components/new/settings/connections-mcp"
import Support from "@/components/new/settings/support"
import { useRouter } from "next/navigation"
+import { useIsMobile } from "@hooks/use-mobile"
const TABS = ["account", "integrations", "connections", "support"] as const
type SettingsTab = (typeof TABS)[number]
@@ -148,6 +149,7 @@ export default function SettingsPage() {
const [activeTab, setActiveTab] = useState<SettingsTab>("account")
const hasInitialized = useRef(false)
const router = useRouter()
+ const isMobile = useIsMobile()
useEffect(() => {
if (hasInitialized.current) return
@@ -174,7 +176,7 @@ export default function SettingsPage() {
}, [])
return (
<div className="h-screen flex flex-col overflow-hidden">
- <header className="flex justify-between items-center px-6 py-3 shrink-0">
+ <header className="flex justify-between items-center px-4 md:px-6 py-3 shrink-0">
<button
type="button"
onClick={() => router.push("/new")}
@@ -191,55 +193,77 @@ export default function SettingsPage() {
)}
</div>
</header>
- <main className="max-w-2xl mx-auto space-x-12 flex justify-center pt-4 flex-1 min-h-0">
- <div className="min-w-xs">
- <motion.div
- animate={{
- scale: 1,
- padding: 48,
- paddingTop: 0,
- }}
- transition={{
- duration: 0.8,
- ease: "easeOut",
- delay: 0.2,
- }}
- className="relative flex items-center justify-center"
- >
- <NovaOrb size={175} className="blur-[3px]!" />
- <UserSupermemory name={user?.name ?? ""} />
- </motion.div>
- <nav className={cn("flex flex-col gap-2", dmSansClassName())}>
- {NAV_ITEMS.map((item) => (
- <button
- key={item.id}
- type="button"
- onClick={() => {
- window.location.hash = item.id
- setActiveTab(item.id)
+ <main className="flex-1 min-h-0 overflow-y-auto md:overflow-hidden">
+ <div className="flex flex-col md:flex-row md:justify-center gap-4 md:gap-8 lg:gap-12 px-4 md:px-6 pt-4 pb-6 md:h-full">
+ <div className="w-full md:w-auto md:min-w-[280px] shrink-0">
+ {!isMobile && (
+ <motion.div
+ animate={{
+ scale: 1,
+ padding: 48,
+ paddingTop: 0,
}}
- className={`text-left p-4 rounded-xl transition-colors flex items-start gap-3 ${
- activeTab === item.id
- ? "bg-[#14161A] text-white shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]"
- : "text-white/60 hover:text-white hover:bg-[#14161A] hover:shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]"
- }`}
+ transition={{
+ duration: 0.8,
+ ease: "easeOut",
+ delay: 0.2,
+ }}
+ className="relative flex items-center justify-center"
>
- <span className="mt-0.5 shrink-0">{item.icon}</span>
- <div className="flex flex-col gap-0.5">
- <span className="font-medium">{item.label}</span>
- <span className="text-sm text-white/50">
- {item.description}
+ <NovaOrb size={175} className="blur-[3px]!" />
+ <UserSupermemory name={user?.name ?? ""} />
+ </motion.div>
+ )}
+ <nav
+ className={cn(
+ "flex gap-2",
+ isMobile
+ ? "flex-row overflow-x-auto pb-2 scrollbar-thin"
+ : "flex-col",
+ dmSansClassName(),
+ )}
+ >
+ {NAV_ITEMS.map((item) => (
+ <button
+ key={item.id}
+ type="button"
+ onClick={() => {
+ window.location.hash = item.id
+ setActiveTab(item.id)
+ }}
+ className={cn(
+ "rounded-xl transition-colors flex items-start gap-3 shrink-0",
+ isMobile ? "px-3 py-2 text-sm" : "text-left p-4",
+ activeTab === item.id
+ ? "bg-[#14161A] text-white shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]"
+ : "text-white/60 hover:text-white hover:bg-[#14161A] hover:shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]",
+ )}
+ >
+ <span className={cn("shrink-0", !isMobile && "mt-0.5")}>
+ {item.icon}
</span>
- </div>
- </button>
- ))}
- </nav>
- </div>
- <div className="flex flex-col gap-4 overflow-y-auto min-w-2xl [scrollbar-gutter:stable] pr-[17px]">
- {activeTab === "account" && <Account />}
- {activeTab === "integrations" && <Integrations />}
- {activeTab === "connections" && <ConnectionsMCP />}
- {activeTab === "support" && <Support />}
+ {isMobile ? (
+ <span className="font-medium whitespace-nowrap">
+ {item.label}
+ </span>
+ ) : (
+ <div className="flex flex-col gap-0.5">
+ <span className="font-medium">{item.label}</span>
+ <span className="text-sm text-white/50">
+ {item.description}
+ </span>
+ </div>
+ )}
+ </button>
+ ))}
+ </nav>
+ </div>
+ <div className="flex-1 flex flex-col gap-4 md:overflow-y-auto md:max-w-2xl [scrollbar-gutter:stable] md:pr-[17px]">
+ {activeTab === "account" && <Account />}
+ {activeTab === "integrations" && <Integrations />}
+ {activeTab === "connections" && <ConnectionsMCP />}
+ {activeTab === "support" && <Support />}
+ </div>
</div>
</main>
</div>
diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx
index d37d1e7c..98cf6b58 100644
--- a/apps/web/app/not-found.tsx
+++ b/apps/web/app/not-found.tsx
@@ -1,20 +1,20 @@
-"use client"; // Error boundaries must be Client Components
+"use client" // Error boundaries must be Client Components
-import { Button } from "@ui/components/button";
-import { Title1Bold } from "@ui/text/title/title-1-bold";
-import { useRouter } from "next/navigation";
-import { useEffect } from "react";
+import { Button } from "@ui/components/button"
+import { Title1Bold } from "@ui/text/title/title-1-bold"
+import { useRouter } from "next/navigation"
+import { useEffect } from "react"
export default function NotFound({
error,
}: {
- error: Error & { digest?: string };
+ error: Error & { digest?: string }
}) {
- const router = useRouter();
+ const router = useRouter()
useEffect(() => {
// Log the error to an error reporting service
- console.error(error);
- }, [error]);
+ console.error(error)
+ }, [error])
return (
<html lang="en">
@@ -23,5 +23,5 @@ export default function NotFound({
<Button onClick={() => router.back()}>Go back</Button>
</body>
</html>
- );
+ )
}
diff --git a/apps/web/app/onboarding/bio-form.tsx b/apps/web/app/onboarding/bio-form.tsx
index 97be3d9e..b9082535 100644
--- a/apps/web/app/onboarding/bio-form.tsx
+++ b/apps/web/app/onboarding/bio-form.tsx
@@ -81,7 +81,8 @@ export function BioForm() {
Tell Supermemory about yourself
</h1>
<p className="text-lg md:text-xl text-white/80">
- share with Supermemory what you do, who you are, and what you're interested in
+ share with Supermemory what you do, who you are, and what you're
+ interested in
</p>
</div>
<Textarea
diff --git a/apps/web/app/onboarding/floating-orbs.tsx b/apps/web/app/onboarding/floating-orbs.tsx
index 6b1f9f8d..1af8f962 100644
--- a/apps/web/app/onboarding/floating-orbs.tsx
+++ b/apps/web/app/onboarding/floating-orbs.tsx
@@ -1,229 +1,246 @@
-"use client";
+"use client"
-import { motion, useReducedMotion } from "motion/react";
-import { useEffect, useMemo, useState, memo } from "react";
-import { useOnboarding } from "./onboarding-context";
+import { motion, useReducedMotion } from "motion/react"
+import { useEffect, useMemo, useState, memo } from "react"
+import { useOnboarding } from "./onboarding-context"
interface OrbProps {
- size: number;
- initialX: number;
- initialY: number;
- duration: number;
- delay: number;
- revealDelay: number;
- shouldReveal: boolean;
- color: {
- primary: string;
- secondary: string;
- tertiary: string;
- };
+ size: number
+ initialX: number
+ initialY: number
+ duration: number
+ delay: number
+ revealDelay: number
+ shouldReveal: boolean
+ color: {
+ primary: string
+ secondary: string
+ tertiary: string
+ }
}
-function FloatingOrb({ size, initialX, initialY, duration, delay, revealDelay, shouldReveal, color }: OrbProps) {
- const blurPixels = Math.min(64, Math.max(24, Math.floor(size * 0.08)));
-
- const gradient = useMemo(() => {
- return `radial-gradient(circle, ${color.primary} 0%, ${color.secondary} 40%, ${color.tertiary} 70%, transparent 100%)`;
- }, [color.primary, color.secondary, color.tertiary]);
-
- const style = useMemo(() => {
- return {
- width: size,
- height: size,
- background: gradient,
- filter: `blur(${blurPixels}px)`,
- willChange: "transform, opacity",
- mixBlendMode: "plus-lighter",
- } as any;
- }, [size, gradient, blurPixels]);
-
- const initial = useMemo(() => {
- return {
- x: initialX,
- y: initialY,
- scale: 0,
- opacity: 0,
- };
- }, [initialX, initialY]);
-
- const animate = useMemo(() => {
- if (!shouldReveal) {
- return {
- x: initialX,
- y: initialY,
- scale: 0,
- opacity: 0,
- };
- }
- return {
- x: [initialX, initialX + 200, initialX - 150, initialX + 100, initialX],
- y: [initialY, initialY - 180, initialY + 120, initialY - 80, initialY],
- scale: [0.8, 1.2, 0.9, 1.1, 0.8],
- opacity: 0.7,
- };
- }, [shouldReveal, initialX, initialY]);
-
- const transition = useMemo(() => {
- return {
- x: {
- duration: shouldReveal ? duration : 0,
- repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
- ease: [0.42, 0, 0.58, 1],
- delay: shouldReveal ? delay + revealDelay : 0,
- },
- y: {
- duration: shouldReveal ? duration : 0,
- repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
- ease: [0.42, 0, 0.58, 1],
- delay: shouldReveal ? delay + revealDelay : 0,
- },
- scale: {
- duration: shouldReveal ? duration : 0.8,
- repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
- ease: shouldReveal ? [0.42, 0, 0.58, 1] : [0, 0, 0.58, 1],
- delay: shouldReveal ? delay + revealDelay : revealDelay,
- },
- opacity: {
- duration: 1.2,
- ease: [0, 0, 0.58, 1],
- delay: shouldReveal ? revealDelay : 0,
- },
- } as any;
- }, [shouldReveal, duration, delay, revealDelay]);
-
- return (
- <motion.div
- className="absolute rounded-full"
- style={style}
- initial={initial}
- animate={animate}
- transition={transition}
- />
- );
+function FloatingOrb({
+ size,
+ initialX,
+ initialY,
+ duration,
+ delay,
+ revealDelay,
+ shouldReveal,
+ color,
+}: OrbProps) {
+ const blurPixels = Math.min(64, Math.max(24, Math.floor(size * 0.08)))
+
+ const gradient = useMemo(() => {
+ return `radial-gradient(circle, ${color.primary} 0%, ${color.secondary} 40%, ${color.tertiary} 70%, transparent 100%)`
+ }, [color.primary, color.secondary, color.tertiary])
+
+ const style = useMemo(() => {
+ return {
+ width: size,
+ height: size,
+ background: gradient,
+ filter: `blur(${blurPixels}px)`,
+ willChange: "transform, opacity",
+ mixBlendMode: "plus-lighter",
+ } as any
+ }, [size, gradient, blurPixels])
+
+ const initial = useMemo(() => {
+ return {
+ x: initialX,
+ y: initialY,
+ scale: 0,
+ opacity: 0,
+ }
+ }, [initialX, initialY])
+
+ const animate = useMemo(() => {
+ if (!shouldReveal) {
+ return {
+ x: initialX,
+ y: initialY,
+ scale: 0,
+ opacity: 0,
+ }
+ }
+ return {
+ x: [initialX, initialX + 200, initialX - 150, initialX + 100, initialX],
+ y: [initialY, initialY - 180, initialY + 120, initialY - 80, initialY],
+ scale: [0.8, 1.2, 0.9, 1.1, 0.8],
+ opacity: 0.7,
+ }
+ }, [shouldReveal, initialX, initialY])
+
+ const transition = useMemo(() => {
+ return {
+ x: {
+ duration: shouldReveal ? duration : 0,
+ repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
+ ease: [0.42, 0, 0.58, 1],
+ delay: shouldReveal ? delay + revealDelay : 0,
+ },
+ y: {
+ duration: shouldReveal ? duration : 0,
+ repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
+ ease: [0.42, 0, 0.58, 1],
+ delay: shouldReveal ? delay + revealDelay : 0,
+ },
+ scale: {
+ duration: shouldReveal ? duration : 0.8,
+ repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
+ ease: shouldReveal ? [0.42, 0, 0.58, 1] : [0, 0, 0.58, 1],
+ delay: shouldReveal ? delay + revealDelay : revealDelay,
+ },
+ opacity: {
+ duration: 1.2,
+ ease: [0, 0, 0.58, 1],
+ delay: shouldReveal ? revealDelay : 0,
+ },
+ } as any
+ }, [shouldReveal, duration, delay, revealDelay])
+
+ return (
+ <motion.div
+ className="absolute rounded-full"
+ style={style}
+ initial={initial}
+ animate={animate}
+ transition={transition}
+ />
+ )
}
-const MemoFloatingOrb = memo(FloatingOrb);
+const MemoFloatingOrb = memo(FloatingOrb)
export function FloatingOrbs() {
- const { orbsRevealed } = useOnboarding();
- const reduceMotion = useReducedMotion();
- const [mounted, setMounted] = useState(false);
- const [orbs, setOrbs] = useState<Array<{
- id: number;
- size: number;
- initialX: number;
- initialY: number;
- duration: number;
- delay: number;
- revealDelay: number;
- color: {
- primary: string;
- secondary: string;
- tertiary: string;
- };
- }>>([]);
-
- useEffect(() => {
- setMounted(true);
-
- const screenWidth = typeof window !== "undefined" ? window.innerWidth : 1200;
- const screenHeight = typeof window !== "undefined" ? window.innerHeight : 800;
-
- // Define edge zones (avoiding center)
- const edgeThickness = Math.min(screenWidth, screenHeight) * 0.25; // 25% of smaller dimension
-
- // Define rainbow color palette
- const colorPalette = [
- { // Magenta
- primary: "rgba(255, 0, 150, 0.6)",
- secondary: "rgba(255, 100, 200, 0.4)",
- tertiary: "rgba(255, 150, 220, 0.1)"
- },
- { // Yellow
- primary: "rgba(255, 235, 59, 0.6)",
- secondary: "rgba(255, 245, 120, 0.4)",
- tertiary: "rgba(255, 250, 180, 0.1)"
- },
- { // Light Blue
- primary: "rgba(100, 181, 246, 0.6)",
- secondary: "rgba(144, 202, 249, 0.4)",
- tertiary: "rgba(187, 222, 251, 0.1)"
- },
- { // Orange (keeping original)
- primary: "rgba(255, 154, 0, 0.6)",
- secondary: "rgba(255, 206, 84, 0.4)",
- tertiary: "rgba(255, 154, 0, 0.1)"
- },
- { // Very Light Red/Pink
- primary: "rgba(255, 138, 128, 0.6)",
- secondary: "rgba(255, 171, 145, 0.4)",
- tertiary: "rgba(255, 205, 210, 0.1)"
- }
- ];
-
- // Generate orb configurations positioned along edges
- const newOrbs = Array.from({ length: 8 }, (_, i) => {
- let x: number;
- let y: number;
- const zone = i % 4; // Rotate through 4 zones: top, right, bottom, left
-
- switch (zone) {
- case 0: // Top edge
- x = Math.random() * screenWidth;
- y = Math.random() * edgeThickness;
- break;
- case 1: // Right edge
- x = screenWidth - edgeThickness + Math.random() * edgeThickness;
- y = Math.random() * screenHeight;
- break;
- case 2: // Bottom edge
- x = Math.random() * screenWidth;
- y = screenHeight - edgeThickness + Math.random() * edgeThickness;
- break;
- case 3: // Left edge
- x = Math.random() * edgeThickness;
- y = Math.random() * screenHeight;
- break;
- default:
- x = Math.random() * screenWidth;
- y = Math.random() * screenHeight;
- }
-
- return {
- id: i,
- size: Math.random() * 300 + 200, // 200px to 500px
- initialX: x,
- initialY: y,
- duration: Math.random() * 20 + 15, // 15-35 seconds (longer for more gentle movement)
- delay: i * 0.4, // Staggered start for floating animation
- revealDelay: i * 0.2, // Faster staggered reveal
- color: colorPalette[i % colorPalette.length]!, // Cycle through rainbow colors
- };
- });
-
- setOrbs(newOrbs);
- }, []);
-
- if (!mounted || orbs.length === 0) return null;
-
- return (
- <div
- className="fixed inset-0 pointer-events-none overflow-hidden"
- style={{ isolation: "isolate", contain: "paint" }}
- >
- {orbs.map((orb) => (
- <MemoFloatingOrb
- key={orb.id}
- size={orb.size}
- initialX={orb.initialX}
- initialY={orb.initialY}
- duration={reduceMotion ? 0 : orb.duration}
- delay={orb.delay}
- revealDelay={orb.revealDelay}
- shouldReveal={reduceMotion ? false : orbsRevealed}
- color={orb.color}
- />
- ))}
- </div>
- );
+ const { orbsRevealed } = useOnboarding()
+ const reduceMotion = useReducedMotion()
+ const [mounted, setMounted] = useState(false)
+ const [orbs, setOrbs] = useState<
+ Array<{
+ id: number
+ size: number
+ initialX: number
+ initialY: number
+ duration: number
+ delay: number
+ revealDelay: number
+ color: {
+ primary: string
+ secondary: string
+ tertiary: string
+ }
+ }>
+ >([])
+
+ useEffect(() => {
+ setMounted(true)
+
+ const screenWidth = typeof window !== "undefined" ? window.innerWidth : 1200
+ const screenHeight =
+ typeof window !== "undefined" ? window.innerHeight : 800
+
+ // Define edge zones (avoiding center)
+ const edgeThickness = Math.min(screenWidth, screenHeight) * 0.25 // 25% of smaller dimension
+
+ // Define rainbow color palette
+ const colorPalette = [
+ {
+ // Magenta
+ primary: "rgba(255, 0, 150, 0.6)",
+ secondary: "rgba(255, 100, 200, 0.4)",
+ tertiary: "rgba(255, 150, 220, 0.1)",
+ },
+ {
+ // Yellow
+ primary: "rgba(255, 235, 59, 0.6)",
+ secondary: "rgba(255, 245, 120, 0.4)",
+ tertiary: "rgba(255, 250, 180, 0.1)",
+ },
+ {
+ // Light Blue
+ primary: "rgba(100, 181, 246, 0.6)",
+ secondary: "rgba(144, 202, 249, 0.4)",
+ tertiary: "rgba(187, 222, 251, 0.1)",
+ },
+ {
+ // Orange (keeping original)
+ primary: "rgba(255, 154, 0, 0.6)",
+ secondary: "rgba(255, 206, 84, 0.4)",
+ tertiary: "rgba(255, 154, 0, 0.1)",
+ },
+ {
+ // Very Light Red/Pink
+ primary: "rgba(255, 138, 128, 0.6)",
+ secondary: "rgba(255, 171, 145, 0.4)",
+ tertiary: "rgba(255, 205, 210, 0.1)",
+ },
+ ]
+
+ // Generate orb configurations positioned along edges
+ const newOrbs = Array.from({ length: 8 }, (_, i) => {
+ let x: number
+ let y: number
+ const zone = i % 4 // Rotate through 4 zones: top, right, bottom, left
+
+ switch (zone) {
+ case 0: // Top edge
+ x = Math.random() * screenWidth
+ y = Math.random() * edgeThickness
+ break
+ case 1: // Right edge
+ x = screenWidth - edgeThickness + Math.random() * edgeThickness
+ y = Math.random() * screenHeight
+ break
+ case 2: // Bottom edge
+ x = Math.random() * screenWidth
+ y = screenHeight - edgeThickness + Math.random() * edgeThickness
+ break
+ case 3: // Left edge
+ x = Math.random() * edgeThickness
+ y = Math.random() * screenHeight
+ break
+ default:
+ x = Math.random() * screenWidth
+ y = Math.random() * screenHeight
+ }
+
+ return {
+ id: i,
+ size: Math.random() * 300 + 200, // 200px to 500px
+ initialX: x,
+ initialY: y,
+ duration: Math.random() * 20 + 15, // 15-35 seconds (longer for more gentle movement)
+ delay: i * 0.4, // Staggered start for floating animation
+ revealDelay: i * 0.2, // Faster staggered reveal
+ color: colorPalette[i % colorPalette.length]!, // Cycle through rainbow colors
+ }
+ })
+
+ setOrbs(newOrbs)
+ }, [])
+
+ if (!mounted || orbs.length === 0) return null
+
+ return (
+ <div
+ className="fixed inset-0 pointer-events-none overflow-hidden"
+ style={{ isolation: "isolate", contain: "paint" }}
+ >
+ {orbs.map((orb) => (
+ <MemoFloatingOrb
+ key={orb.id}
+ size={orb.size}
+ initialX={orb.initialX}
+ initialY={orb.initialY}
+ duration={reduceMotion ? 0 : orb.duration}
+ delay={orb.delay}
+ revealDelay={orb.revealDelay}
+ shouldReveal={reduceMotion ? false : orbsRevealed}
+ color={orb.color}
+ />
+ ))}
+ </div>
+ )
}
diff --git a/apps/web/app/onboarding/nav-menu.tsx b/apps/web/app/onboarding/nav-menu.tsx
index 957cbbef..0bafcfd9 100644
--- a/apps/web/app/onboarding/nav-menu.tsx
+++ b/apps/web/app/onboarding/nav-menu.tsx
@@ -1,44 +1,61 @@
-"use client";
+"use client"
import {
- HoverCard,
- HoverCardContent,
- HoverCardTrigger,
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
} from "@ui/components/hover-card"
-import { useOnboarding, type OnboardingStep } from "./onboarding-context";
-import { useState } from "react";
-import { cn } from "@lib/utils";
+import { useOnboarding, type OnboardingStep } from "./onboarding-context"
+import { useState } from "react"
+import { cn } from "@lib/utils"
export function NavMenu({ children }: { children: React.ReactNode }) {
- const { setStep, currentStep, visibleSteps, getStepNumberFor } = useOnboarding();
- const [open, setOpen] = useState(false);
- const LABELS: Record<OnboardingStep, string> = {
- intro: "Intro",
- name: "Name",
- bio: "About you",
- // connections: "Connections",
- mcp: "MCP",
- extension: "Extension",
- welcome: "Welcome",
- };
- const navigableSteps = visibleSteps.filter(step => step !== "intro" && step !== "welcome");
- return (
- <HoverCard openDelay={100} open={open} onOpenChange={setOpen}>
- <HoverCardTrigger className="w-fit" asChild>{children}</HoverCardTrigger>
- <HoverCardContent align="start" side="left" sideOffset={24} className="origin-top-right bg-white border border-zinc-200 text-zinc-900">
- <h2 className="text-zinc-900 text-sm font-medium">Go to step</h2>
- <ul className="text-sm mt-2">
- {navigableSteps.map((step) => (
- <li key={step}>
- <button type="button" className={cn("py-1.5 px-2 rounded-md hover:bg-zinc-100 w-full text-left", currentStep === step && "bg-zinc-100")} onClick={() => {
- setStep(step);
- setOpen(false);
- }}>
- {getStepNumberFor(step)}. {LABELS[step]}
- </button>
- </li>
- ))}
- </ul>
- </HoverCardContent>
- </HoverCard>
- );
-} \ No newline at end of file
+ const { setStep, currentStep, visibleSteps, getStepNumberFor } =
+ useOnboarding()
+ const [open, setOpen] = useState(false)
+ const LABELS: Record<OnboardingStep, string> = {
+ intro: "Intro",
+ name: "Name",
+ bio: "About you",
+ // connections: "Connections",
+ mcp: "MCP",
+ extension: "Extension",
+ welcome: "Welcome",
+ }
+ const navigableSteps = visibleSteps.filter(
+ (step) => step !== "intro" && step !== "welcome",
+ )
+ return (
+ <HoverCard openDelay={100} open={open} onOpenChange={setOpen}>
+ <HoverCardTrigger className="w-fit" asChild>
+ {children}
+ </HoverCardTrigger>
+ <HoverCardContent
+ align="start"
+ side="left"
+ sideOffset={24}
+ className="origin-top-right bg-white border border-zinc-200 text-zinc-900"
+ >
+ <h2 className="text-zinc-900 text-sm font-medium">Go to step</h2>
+ <ul className="text-sm mt-2">
+ {navigableSteps.map((step) => (
+ <li key={step}>
+ <button
+ type="button"
+ className={cn(
+ "py-1.5 px-2 rounded-md hover:bg-zinc-100 w-full text-left",
+ currentStep === step && "bg-zinc-100",
+ )}
+ onClick={() => {
+ setStep(step)
+ setOpen(false)
+ }}
+ >
+ {getStepNumberFor(step)}. {LABELS[step]}
+ </button>
+ </li>
+ ))}
+ </ul>
+ </HoverCardContent>
+ </HoverCard>
+ )
+}
diff --git a/apps/web/app/ref/[code]/page.tsx b/apps/web/app/ref/[code]/page.tsx
index 98afcfe5..2086d6b4 100644
--- a/apps/web/app/ref/[code]/page.tsx
+++ b/apps/web/app/ref/[code]/page.tsx
@@ -1,40 +1,40 @@
-"use client";
+"use client"
-import { $fetch } from "@lib/api";
-import { Button } from "@ui/components/button";
+import { $fetch } from "@lib/api"
+import { Button } from "@ui/components/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
-} from "@ui/components/card";
-import { CheckIcon, CopyIcon, LoaderIcon, ShareIcon } from "lucide-react";
-import Link from "next/link";
-import { useParams, useRouter } from "next/navigation";
-import { useEffect, useState } from "react";
-import { toast } from "sonner";
+} from "@ui/components/card"
+import { CheckIcon, CopyIcon, LoaderIcon, ShareIcon } from "lucide-react"
+import Link from "next/link"
+import { useParams, useRouter } from "next/navigation"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
export default function ReferralPage() {
- const router = useRouter();
- const params = useParams();
- const referralCode = params.code as string;
+ const router = useRouter()
+ const params = useParams()
+ const referralCode = params.code as string
- const [isLoading, setIsLoading] = useState(true);
+ const [isLoading, setIsLoading] = useState(true)
const [referralData, setReferralData] = useState<{
- referrerName?: string;
- valid: boolean;
- } | null>(null);
- const [copiedLink, setCopiedLink] = useState(false);
+ referrerName?: string
+ valid: boolean
+ } | null>(null)
+ const [copiedLink, setCopiedLink] = useState(false)
- const referralLink = `https://supermemory.ai/ref/${referralCode}`;
+ const referralLink = `https://supermemory.ai/ref/${referralCode}`
// Verify referral code and get referrer info
useEffect(() => {
async function checkReferral() {
if (!referralCode) {
- setIsLoading(false);
- return;
+ setIsLoading(false)
+ return
}
try {
@@ -43,30 +43,28 @@ export default function ReferralPage() {
setReferralData({
valid: true,
referrerName: "A supermemory user", // Placeholder - should come from API
- });
+ })
} catch (error) {
- console.error("Error checking referral:", error);
- setReferralData({ valid: false });
+ console.error("Error checking referral:", error)
+ setReferralData({ valid: false })
} finally {
- setIsLoading(false);
+ setIsLoading(false)
}
}
- checkReferral();
- }, [referralCode]);
-
-
+ checkReferral()
+ }, [referralCode])
const handleCopyLink = async () => {
try {
- await navigator.clipboard.writeText(referralLink);
- setCopiedLink(true);
- toast.success("Referral link copied!");
- setTimeout(() => setCopiedLink(false), 2000);
+ await navigator.clipboard.writeText(referralLink)
+ setCopiedLink(true)
+ toast.success("Referral link copied!")
+ setTimeout(() => setCopiedLink(false), 2000)
} catch (error) {
- toast.error("Failed to copy link");
+ toast.error("Failed to copy link")
}
- };
+ }
const handleShare = () => {
if (navigator.share) {
@@ -74,11 +72,11 @@ export default function ReferralPage() {
title: "Join supermemory",
text: "I'm excited about supermemory - it's going to change how we store and interact with our memories!",
url: referralLink,
- });
+ })
} else {
- handleCopyLink();
+ handleCopyLink()
}
- };
+ }
if (isLoading) {
return (
@@ -88,7 +86,7 @@ export default function ReferralPage() {
<p className="text-white/60">Checking invitation...</p>
</div>
</div>
- );
+ )
}
if (!referralData?.valid) {
@@ -112,7 +110,7 @@ export default function ReferralPage() {
</CardContent>
</Card>
</div>
- );
+ )
}
return (
@@ -146,7 +144,6 @@ export default function ReferralPage() {
</p>
</div>
-
<div className="text-center">
<Link
href="https://supermemory.ai"
@@ -204,5 +201,5 @@ export default function ReferralPage() {
</Card>
</div>
</div>
- );
+ )
}
diff --git a/apps/web/app/ref/page.tsx b/apps/web/app/ref/page.tsx
index 78373693..853ed900 100644
--- a/apps/web/app/ref/page.tsx
+++ b/apps/web/app/ref/page.tsx
@@ -1,15 +1,15 @@
-"use client";
+"use client"
-import { Button } from "@ui/components/button";
+import { Button } from "@ui/components/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
-} from "@ui/components/card";
-import { ShareIcon } from "lucide-react";
-import Link from "next/link";
+} from "@ui/components/card"
+import { ShareIcon } from "lucide-react"
+import Link from "next/link"
export default function ReferralHomePage() {
return (
@@ -52,5 +52,5 @@ export default function ReferralHomePage() {
</CardContent>
</Card>
</div>
- );
+ )
}
diff --git a/apps/web/biome.json b/apps/web/biome.json
index 8ab5697e..78be5eb3 100644
--- a/apps/web/biome.json
+++ b/apps/web/biome.json
@@ -16,4 +16,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/apps/web/components/glass-menu-effect.tsx b/apps/web/components/glass-menu-effect.tsx
index 3914ac7b..9d4d4b68 100644
--- a/apps/web/components/glass-menu-effect.tsx
+++ b/apps/web/components/glass-menu-effect.tsx
@@ -1,8 +1,8 @@
-import { motion } from "motion/react";
+import { motion } from "motion/react"
interface GlassMenuEffectProps {
- rounded?: string;
- className?: string;
+ rounded?: string
+ className?: string
}
export function GlassMenuEffect({
@@ -33,5 +33,5 @@ export function GlassMenuEffect({
}}
/>
</motion.div>
- );
+ )
}
diff --git a/apps/web/components/initial-header.tsx b/apps/web/components/initial-header.tsx
index 3861b95d..91edfa57 100644
--- a/apps/web/components/initial-header.tsx
+++ b/apps/web/components/initial-header.tsx
@@ -15,7 +15,9 @@ export function InitialHeader({
<Logo className="h-7" />
{showUserSupermemory && (
<div className="flex flex-col items-start justify-center ml-2">
- <p className="text-[#8B8B8B] text-[11px] leading-tight">{userName}</p>
+ <p className="text-[#8B8B8B] text-[11px] leading-tight">
+ {userName}
+ </p>
<p className="text-white font-bold text-xl leading-none -mt-1">
supermemory
</p>
diff --git a/apps/web/components/install-prompt.tsx b/apps/web/components/install-prompt.tsx
index 3bbeb071..29fb2e3d 100644
--- a/apps/web/components/install-prompt.tsx
+++ b/apps/web/components/install-prompt.tsx
@@ -1,69 +1,69 @@
-import { Button } from "@repo/ui/components/button";
-import { Download, Share, X } from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
-import { useEffect, useState } from "react";
+import { Button } from "@repo/ui/components/button"
+import { Download, Share, X } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import { useEffect, useState } from "react"
export function InstallPrompt() {
- const [isIOS, setIsIOS] = useState(false);
- const [showPrompt, setShowPrompt] = useState(false);
- const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
+ const [isIOS, setIsIOS] = useState(false)
+ const [showPrompt, setShowPrompt] = useState(false)
+ const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
useEffect(() => {
const isIOSDevice =
- /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream;
+ /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
const isInStandaloneMode = window.matchMedia(
"(display-mode: standalone)",
- ).matches;
+ ).matches
const hasSeenPrompt =
- localStorage.getItem("install-prompt-dismissed") === "true";
+ localStorage.getItem("install-prompt-dismissed") === "true"
- setIsIOS(isIOSDevice);
+ setIsIOS(isIOSDevice)
- const isDevelopment = process.env.NODE_ENV === "development";
+ const isDevelopment = process.env.NODE_ENV === "development"
setShowPrompt(
!hasSeenPrompt &&
(isDevelopment ||
(!isInStandaloneMode &&
(isIOSDevice || "serviceWorker" in navigator))),
- );
+ )
const handleBeforeInstallPrompt = (e: Event) => {
- e.preventDefault();
- setDeferredPrompt(e);
+ e.preventDefault()
+ setDeferredPrompt(e)
if (!hasSeenPrompt) {
- setShowPrompt(true);
+ setShowPrompt(true)
}
- };
+ }
- window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
+ window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt)
return () => {
window.removeEventListener(
"beforeinstallprompt",
handleBeforeInstallPrompt,
- );
- };
- }, []);
+ )
+ }
+ }, [])
const handleInstall = async () => {
if (deferredPrompt) {
- deferredPrompt.prompt();
- const { outcome } = await deferredPrompt.userChoice;
+ deferredPrompt.prompt()
+ const { outcome } = await deferredPrompt.userChoice
if (outcome === "accepted") {
- localStorage.setItem("install-prompt-dismissed", "true");
- setShowPrompt(false);
+ localStorage.setItem("install-prompt-dismissed", "true")
+ setShowPrompt(false)
}
- setDeferredPrompt(null);
+ setDeferredPrompt(null)
}
- };
+ }
const handleDismiss = () => {
- localStorage.setItem("install-prompt-dismissed", "true");
- setShowPrompt(false);
- };
+ localStorage.setItem("install-prompt-dismissed", "true")
+ setShowPrompt(false)
+ }
if (!showPrompt) {
- return null;
+ return null
}
return (
@@ -128,5 +128,5 @@ export function InstallPrompt() {
</div>
</motion.div>
</AnimatePresence>
- );
+ )
}
diff --git a/apps/web/components/memories-utils/index.tsx b/apps/web/components/memories-utils/index.tsx
index fca4d367..8b0328c6 100644
--- a/apps/web/components/memories-utils/index.tsx
+++ b/apps/web/components/memories-utils/index.tsx
@@ -48,7 +48,7 @@ export const getSourceUrl = (document: DocumentWithMemories) => {
if (document.type === "google_slide" && document.customId) {
return `https://docs.google.com/presentation/d/${document.customId}`
}
- if(document.metadata?.website_url) {
+ if (document.metadata?.website_url) {
return document.metadata?.website_url as string
}
// Fallback to existing URL for all other document types
diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx
index e59f3839..7d7dffe5 100644
--- a/apps/web/components/menu.tsx
+++ b/apps/web/components/menu.tsx
@@ -66,7 +66,10 @@ function Menu({ id }: { id?: string }) {
const autumn = useCustomer()
const { setIsOpen } = useChatOpen()
- 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
diff --git a/apps/web/components/new/add-document/index.tsx b/apps/web/components/new/add-document/index.tsx
index 282150e4..0e433ea5 100644
--- a/apps/web/components/new/add-document/index.tsx
+++ b/apps/web/components/new/add-document/index.tsx
@@ -16,6 +16,7 @@ import { useDocumentMutations } from "../../../hooks/use-document-mutations"
import { useCustomer } from "autumn-js/react"
import { useMemoriesUsage } from "@/hooks/use-memories-usage"
import { SpaceSelector } from "../space-selector"
+import { useIsMobile } from "@hooks/use-mobile"
type TabType = "note" | "link" | "file" | "connect"
@@ -30,11 +31,16 @@ export function AddDocumentModal({
onClose,
defaultTab,
}: AddDocumentModalProps) {
+ const isMobile = useIsMobile()
+
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent
className={cn(
- "w-[80%]! max-w-[1000px]! h-[80%]! max-h-[800px]! border-none bg-[#1B1F24] flex flex-col p-4 gap-3 rounded-[22px]",
+ "border-none bg-[#1B1F24] flex flex-col p-3 md:p-4 gap-3",
+ isMobile
+ ? "w-[calc(100vw-1rem)]! h-[calc(100dvh-1rem)]! max-w-none! max-h-none! rounded-xl"
+ : "w-[80%]! max-w-[1000px]! h-[80%]! max-h-[800px]! rounded-[22px]",
dmSansClassName(),
)}
style={{
@@ -93,6 +99,7 @@ export function AddDocument({
onClose: () => void
isOpen?: boolean
}) {
+ const isMobile = useIsMobile()
const [activeTab, setActiveTab] = useState<TabType>(defaultTab ?? "note")
const { selectedProject: globalSelectedProject } = useProject()
const [localSelectedProject, setLocalSelectedProject] = useState<string>(
@@ -208,9 +215,21 @@ export function AddDocument({
noteMutation.isPending || linkMutation.isPending || fileMutation.isPending
return (
- <div className="h-full flex flex-row text-white space-x-5">
- <div className="w-1/3 flex flex-col justify-between">
- <div className="flex flex-col gap-1">
+ <div className="h-full flex flex-col md:flex-row text-white md:space-x-5 space-y-3 md:space-y-0">
+ <div
+ className={cn(
+ "flex flex-col justify-between",
+ isMobile ? "w-full" : "w-1/3",
+ )}
+ >
+ <div
+ className={cn(
+ "flex gap-1",
+ isMobile
+ ? "flex-row overflow-x-auto pb-2 scrollbar-thin"
+ : "flex-col",
+ )}
+ >
{tabs.map((tab) => (
<TabButton
key={tab.id}
@@ -220,47 +239,55 @@ export function AddDocument({
title={tab.title}
description={tab.description}
isPro={tab.isPro}
+ compact={isMobile}
/>
))}
</div>
- <div
- data-testid="memories-counter"
- className="bg-[#1B1F24] rounded-2xl p-4 mr-4"
- style={{
- boxShadow:
- "0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
- }}
- >
- <div className="flex justify-between items-center">
- <span
- className={cn(
- "text-white text-[16px] font-medium",
- dmSansClassName(),
- )}
- >
- Memories
- </span>
- <span className={cn("text-[#737373] text-sm", dmSansClassName())}>
- {isLoadingMemories
- ? "โ€ฆ"
- : hasProProduct
- ? "Unlimited"
- : `${memoriesUsed}/${memoriesLimit}`}
- </span>
- </div>
- {!hasProProduct && (
- <div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden mt-2">
- <div
- className="h-full bg-[#2261CA] rounded-full"
- style={{ width: `${usagePercent}%` }}
- />
+ {!isMobile && (
+ <div
+ data-testid="memories-counter"
+ className="bg-[#1B1F24] rounded-2xl p-4 mr-4"
+ style={{
+ boxShadow:
+ "0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
+ }}
+ >
+ <div className="flex justify-between items-center">
+ <span
+ className={cn(
+ "text-white text-[16px] font-medium",
+ dmSansClassName(),
+ )}
+ >
+ Memories
+ </span>
+ <span className={cn("text-[#737373] text-sm", dmSansClassName())}>
+ {isLoadingMemories
+ ? "โ€ฆ"
+ : hasProProduct
+ ? "Unlimited"
+ : `${memoriesUsed}/${memoriesLimit}`}
+ </span>
</div>
- )}
- </div>
+ {!hasProProduct && (
+ <div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden mt-2">
+ <div
+ className="h-full bg-[#2261CA] rounded-full"
+ style={{ width: `${usagePercent}%` }}
+ />
+ </div>
+ )}
+ </div>
+ )}
</div>
- <div className="w-2/3 overflow-auto flex flex-col justify-between px-1 scrollbar-thin">
+ <div
+ className={cn(
+ "overflow-auto flex flex-col justify-between px-1 scrollbar-thin flex-1",
+ isMobile ? "w-full" : "w-2/3",
+ )}
+ >
{activeTab === "note" && (
<NoteContent
onSubmit={handleNoteSubmit}
@@ -288,13 +315,22 @@ export function AddDocument({
{activeTab === "connect" && (
<ConnectContent selectedProject={localSelectedProject} />
)}
- <div className="flex justify-between">
- <SpaceSelector
- value={localSelectedProject}
- onValueChange={setLocalSelectedProject}
- variant="insideOut"
- />
- <div className="flex items-center gap-2">
+ <div
+ className={cn(
+ "flex gap-2 pt-3",
+ isMobile ? "flex-col" : "justify-between",
+ )}
+ >
+ {!isMobile && (
+ <SpaceSelector
+ value={localSelectedProject}
+ onValueChange={setLocalSelectedProject}
+ variant="insideOut"
+ />
+ )}
+ <div
+ className={cn("flex items-center gap-2", isMobile && "justify-end")}
+ >
<Button
variant="ghost"
onClick={onClose}
@@ -317,14 +353,16 @@ export function AddDocument({
) : (
<>
+ Add {activeTab}{" "}
- <span
- className={cn(
- "bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm px-1 py-0.5 text-[10px] flex items-center justify-center",
- dmSansClassName(),
- )}
- >
- โŒ˜+Enter
- </span>
+ {!isMobile && (
+ <span
+ className={cn(
+ "bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm px-1 py-0.5 text-[10px] flex items-center justify-center",
+ dmSansClassName(),
+ )}
+ >
+ โŒ˜+Enter
+ </span>
+ )}
</>
)}
</Button>
@@ -343,6 +381,7 @@ function TabButton({
title,
description,
isPro,
+ compact,
}: {
active: boolean
onClick: () => void
@@ -350,7 +389,34 @@ function TabButton({
title: string
description: string
isPro?: boolean
+ compact?: boolean
}) {
+ if (compact) {
+ return (
+ <button
+ type="button"
+ onClick={onClick}
+ className={cn(
+ "flex items-center gap-2 px-3 py-2 rounded-full text-left transition-colors whitespace-nowrap focus:outline-none focus:ring-0 shrink-0",
+ active ? "bg-[#14161A] shadow-inside-out" : "hover:bg-[#14161A]/50",
+ dmSansClassName(),
+ )}
+ >
+ <Icon className={cn("size-4 shrink-0 text-white")} />
+ <span
+ className={cn("font-medium text-white text-sm", dmSansClassName())}
+ >
+ {title.split(" ")[0]}
+ </span>
+ {isPro && (
+ <span className="bg-[#4BA0FA] text-black text-[8px] font-semibold px-1 py-0.5 rounded">
+ PRO
+ </span>
+ )}
+ </button>
+ )
+ }
+
return (
<button
type="button"
diff --git a/apps/web/components/new/add-space-modal.tsx b/apps/web/components/new/add-space-modal.tsx
index 69e1563b..63ee5e96 100644
--- a/apps/web/components/new/add-space-modal.tsx
+++ b/apps/web/components/new/add-space-modal.tsx
@@ -8,19 +8,57 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon, Loader2 } from "lucide-react"
import { Button } from "@ui/components/button"
import { useProjectMutations } from "@/hooks/use-project-mutations"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@ui/components/popover"
+import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
const EMOJI_LIST = [
- "๐Ÿ“", "๐Ÿ“‚", "๐Ÿ—‚๏ธ", "๐Ÿ“š", "๐Ÿ“–", "๐Ÿ“", "โœ๏ธ", "๐Ÿ“Œ",
- "๐ŸŽฏ", "๐Ÿš€", "๐Ÿ’ก", "โญ", "๐Ÿ”ฅ", "๐Ÿ’Ž", "๐ŸŽจ", "๐ŸŽต",
- "๐Ÿ ", "๐Ÿ’ผ", "๐Ÿ› ๏ธ", "โš™๏ธ", "๐Ÿ”ง", "๐Ÿ“Š", "๐Ÿ“ˆ", "๐Ÿ’ฐ",
- "๐ŸŒŸ", "โœจ", "๐ŸŒˆ", "๐ŸŒธ", "๐ŸŒบ", "๐Ÿ€", "๐ŸŒฟ", "๐ŸŒด",
- "๐Ÿถ", "๐Ÿฑ", "๐ŸฆŠ", "๐Ÿฆ", "๐Ÿผ", "๐Ÿจ", "๐Ÿฆ„", "๐Ÿ",
- "โค๏ธ", "๐Ÿ’œ", "๐Ÿ’™", "๐Ÿ’š", "๐Ÿ’›", "๐Ÿงก", "๐Ÿ–ค", "๐Ÿค",
+ "๐Ÿ“",
+ "๐Ÿ“‚",
+ "๐Ÿ—‚๏ธ",
+ "๐Ÿ“š",
+ "๐Ÿ“–",
+ "๐Ÿ“",
+ "โœ๏ธ",
+ "๐Ÿ“Œ",
+ "๐ŸŽฏ",
+ "๐Ÿš€",
+ "๐Ÿ’ก",
+ "โญ",
+ "๐Ÿ”ฅ",
+ "๐Ÿ’Ž",
+ "๐ŸŽจ",
+ "๐ŸŽต",
+ "๐Ÿ ",
+ "๐Ÿ’ผ",
+ "๐Ÿ› ๏ธ",
+ "โš™๏ธ",
+ "๐Ÿ”ง",
+ "๐Ÿ“Š",
+ "๐Ÿ“ˆ",
+ "๐Ÿ’ฐ",
+ "๐ŸŒŸ",
+ "โœจ",
+ "๐ŸŒˆ",
+ "๐ŸŒธ",
+ "๐ŸŒบ",
+ "๐Ÿ€",
+ "๐ŸŒฟ",
+ "๐ŸŒด",
+ "๐Ÿถ",
+ "๐Ÿฑ",
+ "๐ŸฆŠ",
+ "๐Ÿฆ",
+ "๐Ÿผ",
+ "๐Ÿจ",
+ "๐Ÿฆ„",
+ "๐Ÿ",
+ "โค๏ธ",
+ "๐Ÿ’œ",
+ "๐Ÿ’™",
+ "๐Ÿ’š",
+ "๐Ÿ’›",
+ "๐Ÿงก",
+ "๐Ÿ–ค",
+ "๐Ÿค",
]
export function AddSpaceModal({
@@ -56,7 +94,11 @@ export function AddSpaceModal({
}
const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && spaceName.trim() && !createProjectMutation.isPending) {
+ if (
+ e.key === "Enter" &&
+ spaceName.trim() &&
+ !createProjectMutation.isPending
+ ) {
e.preventDefault()
handleCreate()
}
@@ -83,11 +125,21 @@ export function AddSpaceModal({
<div className="flex flex-col gap-4">
<div className="flex justify-between items-start gap-4">
<div className="pl-1 space-y-1 flex-1">
- <p className={cn("font-semibold text-[#fafafa]", dmSans125ClassName())}>
+ <p
+ className={cn(
+ "font-semibold text-[#fafafa]",
+ dmSans125ClassName(),
+ )}
+ >
Create new space
</p>
- <p className={cn("text-[#737373] font-medium text-[16px] leading-[1.35]")}>
- Create spaces to organize your memories and documents and create a context rich environment
+ <p
+ className={cn(
+ "text-[#737373] font-medium text-[16px] leading-[1.35]",
+ )}
+ >
+ Create spaces to organize your memories and documents and create
+ a context rich environment
</p>
</div>
<DialogPrimitive.Close
@@ -133,7 +185,8 @@ export function AddSpaceModal({
onClick={() => handleEmojiSelect(e)}
className={cn(
"size-8 flex items-center justify-center rounded-md text-lg cursor-pointer transition-colors hover:bg-[#1B1F24]",
- emoji === e && "bg-[#1B1F24] ring-1 ring-[rgba(115,115,115,0.3)]",
+ emoji === e &&
+ "bg-[#1B1F24] ring-1 ring-[rgba(115,115,115,0.3)]",
)}
>
{e}
diff --git a/apps/web/components/new/chat/index.tsx b/apps/web/components/new/chat/index.tsx
index 634c0bb1..8c57aeae 100644
--- a/apps/web/components/new/chat/index.tsx
+++ b/apps/web/components/new/chat/index.tsx
@@ -12,6 +12,7 @@ import {
PanelRightCloseIcon,
SearchIcon,
SquarePenIcon,
+ XIcon,
} from "lucide-react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
@@ -24,6 +25,8 @@ import { SuperLoader } from "../../superloader"
import { UserMessage } from "./message/user-message"
import { AgentMessage } from "./message/agent-message"
import { ChainOfThought } from "./input/chain-of-thought"
+import { useIsMobile } from "@hooks/use-mobile"
+import { analytics } from "@/lib/analytics"
function ChatEmptyStatePlaceholder({
onSuggestionClick,
@@ -78,6 +81,7 @@ export function ChatSidebar({
isChatOpen: boolean
setIsChatOpen: (open: boolean) => void
}) {
+ const isMobile = useIsMobile()
const [input, setInput] = useState("")
const [selectedModel, setSelectedModel] = useState<ModelId>("gemini-2.5-pro")
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null)
@@ -100,8 +104,10 @@ export function ChatSidebar({
const { selectedProject } = useProject()
const { setCurrentChatId } = usePersistentChat()
- // Adjust chat height based on scroll position
+ // Adjust chat height based on scroll position (desktop only)
useEffect(() => {
+ if (isMobile) return
+
const handleWindowScroll = () => {
const scrollThreshold = 80
const scrollY = window.scrollY
@@ -114,7 +120,7 @@ export function ChatSidebar({
handleWindowScroll()
return () => window.removeEventListener("scroll", handleWindowScroll)
- }, [])
+ }, [isMobile])
const { messages, sendMessage, status, setMessages, stop } = useChat({
transport: new DefaultChatTransport({
@@ -293,6 +299,7 @@ export function ChatSidebar({
}, [])
const handleNewChat = useCallback(() => {
+ analytics.newChatCreated()
const newId = crypto.randomUUID()
setCurrentChatId(newId)
setMessages([])
@@ -374,39 +381,59 @@ export function ChatSidebar({
<motion.div
key="closed"
className={cn(
- "absolute top-0 right-0 flex items-start justify-start m-4",
+ "flex items-start justify-start",
+ isMobile
+ ? "fixed bottom-4 right-4 z-50"
+ : "absolute top-0 right-0 m-4",
dmSansClassName(),
)}
layoutId="chat-toggle-button"
>
<motion.button
onClick={toggleChat}
- className="flex items-center gap-3 rounded-full px-3 py-1.5 text-sm font-medium border border-[#17181A] text-white cursor-pointer whitespace-nowrap"
+ className={cn(
+ "flex items-center gap-3 rounded-full px-3 py-1.5 text-sm font-medium border border-[#17181A] text-white cursor-pointer whitespace-nowrap shadow-lg",
+ isMobile && "px-4 py-2",
+ )}
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
<NovaOrb size={24} className="blur-[0.6px]! z-10" />
- Chat with Nova
+ {!isMobile && "Chat with Nova"}
</motion.button>
</motion.div>
) : (
<motion.div
key="open"
className={cn(
- "w-[450px] bg-[#05070A] backdrop-blur-md flex flex-col rounded-2xl m-4 mt-2 border border-[#17181AB2] relative pt-4",
+ "bg-[#05070A] backdrop-blur-md flex flex-col border border-[#17181AB2] relative pt-4",
+ isMobile
+ ? "fixed inset-0 z-50 w-full h-dvh rounded-none m-0"
+ : "w-[450px] rounded-2xl m-4 mt-2",
dmSansClassName(),
)}
- style={{
- height: `calc(100vh - ${heightOffset}px)`,
- }}
- initial={{ x: "100px", opacity: 0 }}
- animate={{ x: 0, opacity: 1 }}
- exit={{ x: "100px", opacity: 0 }}
+ style={
+ isMobile
+ ? undefined
+ : {
+ height: `calc(100vh - ${heightOffset}px)`,
+ }
+ }
+ initial={
+ isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
+ }
+ animate={{ x: 0, y: 0, opacity: 1 }}
+ exit={
+ isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
+ }
transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }}
>
<div
- className="absolute top-0 left-0 right-0 flex items-center justify-between pt-4 px-4 rounded-t-2xl"
+ className={cn(
+ "absolute top-0 left-0 right-0 flex items-center justify-between pt-4 px-4",
+ !isMobile && "rounded-t-2xl",
+ )}
style={{
background:
"linear-gradient(180deg, #0A0E14 40.49%, rgba(10, 14, 20, 0.00) 100%)",
@@ -417,15 +444,17 @@ export function ChatSidebar({
onModelChange={setSelectedModel}
/>
<div className="flex items-center gap-2">
- <Button
- variant="headers"
- className="rounded-full text-base gap-2 h-10! border-[#73737333] bg-[#0D121A]"
- style={{
- boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
- }}
- >
- <HistoryIcon className="size-4 text-[#737373]" />
- </Button>
+ {!isMobile && (
+ <Button
+ variant="headers"
+ className="rounded-full text-base gap-2 h-10! border-[#73737333] bg-[#0D121A]"
+ style={{
+ boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
+ }}
+ >
+ <HistoryIcon className="size-4 text-[#737373]" />
+ </Button>
+ )}
<Button
variant="headers"
className="rounded-full text-base gap-3 h-10! border-[#73737333] bg-[#0D121A] cursor-pointer"
@@ -436,21 +465,38 @@ export function ChatSidebar({
title="New chat (T)"
>
<SquarePenIcon className="size-4 text-[#737373]" />
- <span
- className={cn(
- "bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
- dmSansClassName(),
- )}
- >
- T
- </span>
+ {!isMobile && (
+ <span
+ className={cn(
+ "bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
+ dmSansClassName(),
+ )}
+ >
+ T
+ </span>
+ )}
</Button>
<motion.button
onClick={toggleChat}
- className="flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer"
+ className={cn(
+ "flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer",
+ isMobile && "bg-[#0D121A] border border-[#73737333]",
+ )}
+ style={
+ isMobile
+ ? {
+ boxShadow:
+ "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
+ }
+ : undefined
+ }
layoutId="chat-toggle-button"
>
- <PanelRightCloseIcon className="size-4" />
+ {isMobile ? (
+ <XIcon className="size-4" />
+ ) : (
+ <PanelRightCloseIcon className="size-4" />
+ )}
</motion.button>
</div>
</div>
diff --git a/apps/web/components/new/document-cards/note-preview.tsx b/apps/web/components/new/document-cards/note-preview.tsx
index 2becc237..c2b767b0 100644
--- a/apps/web/components/new/document-cards/note-preview.tsx
+++ b/apps/web/components/new/document-cards/note-preview.tsx
@@ -42,7 +42,13 @@ function extractTextFromNode(node: TipTapNode): string {
}
}
- const blockTypes = ["paragraph", "heading", "listItem", "blockquote", "codeBlock"]
+ const blockTypes = [
+ "paragraph",
+ "heading",
+ "listItem",
+ "blockquote",
+ "codeBlock",
+ ]
if (blockTypes.includes(node.type)) {
return `${texts.join("")}\n`
}
diff --git a/apps/web/components/new/document-modal/content/notion-doc.tsx b/apps/web/components/new/document-modal/content/notion-doc.tsx
index 45a7dab8..379c78af 100644
--- a/apps/web/components/new/document-modal/content/notion-doc.tsx
+++ b/apps/web/components/new/document-modal/content/notion-doc.tsx
@@ -1,9 +1,27 @@
+import React from "react"
import { Streamdown } from "streamdown"
+const components = {
+ p: ({ children, ...props }: React.ComponentPropsWithoutRef<"p">) => {
+ const hasDiv = React.Children.toArray(children).some(
+ (child) =>
+ React.isValidElement(child) &&
+ typeof child.type === "string" &&
+ child.type === "div",
+ )
+
+ if (hasDiv) {
+ return <div {...props}>{children}</div>
+ }
+
+ return <p {...props}>{children}</p>
+ },
+} as const
+
export function NotionDoc({ content }: { content: string }) {
return (
<div className="p-4 overflow-y-auto flex-1 scrollbar-thin">
- <Streamdown>{content}</Streamdown>
+ <Streamdown components={components}>{content}</Streamdown>
</div>
)
-} \ No newline at end of file
+}
diff --git a/apps/web/components/new/document-modal/content/pdf.tsx b/apps/web/components/new/document-modal/content/pdf.tsx
index a5870682..a025cf61 100644
--- a/apps/web/components/new/document-modal/content/pdf.tsx
+++ b/apps/web/components/new/document-modal/content/pdf.tsx
@@ -54,7 +54,8 @@ export function PdfViewer({ url }: PdfViewerProps) {
<div className="flex-1 overflow-auto w-full">
<Document
file={
- url || "https://corsproxy.io/?" +
+ url ||
+ "https://corsproxy.io/?" +
encodeURIComponent("http://www.pdf995.com/samples/pdf.pdf")
}
onLoadSuccess={onDocumentLoadSuccess}
diff --git a/apps/web/components/new/document-modal/content/web-page.tsx b/apps/web/components/new/document-modal/content/web-page.tsx
new file mode 100644
index 00000000..a2c479ac
--- /dev/null
+++ b/apps/web/components/new/document-modal/content/web-page.tsx
@@ -0,0 +1,27 @@
+import React from "react"
+import { Streamdown } from "streamdown"
+
+const components = {
+ p: ({ children, ...props }: React.ComponentPropsWithoutRef<"p">) => {
+ const hasDiv = React.Children.toArray(children).some(
+ (child) =>
+ React.isValidElement(child) &&
+ typeof child.type === "string" &&
+ child.type === "div",
+ )
+
+ if (hasDiv) {
+ return <div {...props}>{children}</div>
+ }
+
+ return <p {...props}>{children}</p>
+ },
+} as const
+
+export function WebPageContent({ content }: { content: string }) {
+ return (
+ <div className="p-4 overflow-y-auto flex-1 scrollbar-thin">
+ <Streamdown components={components}>{content}</Streamdown>
+ </div>
+ )
+}
diff --git a/apps/web/components/new/document-modal/index.tsx b/apps/web/components/new/document-modal/index.tsx
index 8d4aac87..8cf5d195 100644
--- a/apps/web/components/new/document-modal/index.tsx
+++ b/apps/web/components/new/document-modal/index.tsx
@@ -2,7 +2,13 @@
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
-import { ArrowUpRightIcon, XIcon, Loader2, Trash2Icon, CheckIcon } from "lucide-react"
+import {
+ ArrowUpRightIcon,
+ XIcon,
+ Loader2,
+ Trash2Icon,
+ CheckIcon,
+} from "lucide-react"
import type { z } from "zod"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@lib/utils"
@@ -22,6 +28,8 @@ import { Button } from "@repo/ui/components/button"
import { useDocumentMutations } from "@/hooks/use-document-mutations"
import type { UseMutationResult } from "@tanstack/react-query"
import { toast } from "sonner"
+import { WebPageContent } from "./content/web-page"
+import { useIsMobile } from "@hooks/use-mobile"
// Dynamically importing to prevent DOMMatrix error
const PdfViewer = dynamic(
@@ -61,7 +69,11 @@ function isTemporaryId(id: string | null | undefined): boolean {
return id.startsWith("temp-") || id.startsWith("temp-file-")
}
-function DeleteButton({ documentId, customId, deleteMutation }: DeleteButtonProps) {
+function DeleteButton({
+ documentId,
+ customId,
+ deleteMutation,
+}: DeleteButtonProps) {
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const handleDelete = useCallback(() => {
@@ -140,6 +152,7 @@ export function DocumentModal({
isOpen,
onClose,
}: DocumentModalProps) {
+ const isMobile = useIsMobile()
const { updateMutation, deleteMutation } = useDocumentMutations({ onClose })
const { initialEditorContent, initialEditorString } = useMemo(() => {
@@ -181,7 +194,9 @@ export function DocumentModal({
if (!_document?.id) return
updateMutation.mutate(
{ documentId: _document.id, content: draftContentString },
- { onSuccess: (_data, variables) => setLastSavedContent(variables.content) },
+ {
+ onSuccess: (_data, variables) => setLastSavedContent(variables.content),
+ },
)
}, [_document?.id, draftContentString, updateMutation])
@@ -189,7 +204,10 @@ export function DocumentModal({
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent
className={cn(
- "w-[80%]! max-w-[1158px]! h-[86%]! max-h-[684px]! p-0 border-none bg-[#1B1F24] flex flex-col px-4 pt-3 pb-4 gap-3 rounded-[22px]",
+ "p-0 border-none bg-[#1B1F24] flex flex-col px-3 md:px-4 pt-3 pb-4 gap-3",
+ isMobile
+ ? "w-[calc(100vw-1rem)]! h-[calc(100dvh-1rem)]! max-w-none! max-h-none! rounded-xl"
+ : "w-[80%]! max-w-[1158px]! h-[86%]! max-h-[684px]! rounded-[22px]",
dmSansClassName(),
)}
style={{
@@ -201,7 +219,7 @@ export function DocumentModal({
<DialogTitle className="sr-only">
{_document?.title} - Document
</DialogTitle>
- <div className="flex items-center justify-between h-fit gap-4">
+ <div className="flex items-center justify-between h-fit gap-2 md:gap-4">
<div className="flex-1 min-w-0">
<Title
title={_document?.title}
@@ -209,7 +227,7 @@ export function DocumentModal({
url={_document?.url}
/>
</div>
- <div className="flex items-center gap-2 shrink-0">
+ <div className="flex items-center gap-1.5 md:gap-2 shrink-0">
<DeleteButton
documentId={_document?.id}
customId={_document?.customId}
@@ -220,9 +238,14 @@ export function DocumentModal({
href={_document.url}
target="_blank"
rel="noopener noreferrer"
- className="line-clamp-1 px-3 py-2 flex items-center gap-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
+ className={cn(
+ "flex items-center gap-1 bg-[#0D121A] rounded-full shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]",
+ isMobile ? "w-7 h-7 justify-center" : "px-3 py-2",
+ )}
>
- Visit source
+ {!isMobile && (
+ <span className="line-clamp-1">Visit source</span>
+ )}
<ArrowUpRightIcon className="w-4 h-4 text-[#737373]" />
</a>
)}
@@ -237,7 +260,7 @@ export function DocumentModal({
</DialogPrimitive.Close>
</div>
</div>
- <div className="flex-1 grid grid-cols-[2fr_1fr] gap-3 overflow-hidden min-h-0">
+ <div className="flex-1 grid grid-cols-1 md:grid-cols-[2fr_1fr] gap-3 overflow-hidden min-h-0">
<div
id="document-preview"
className={cn(
@@ -322,6 +345,9 @@ export function DocumentModal({
{_document?.url?.includes("youtube.com") && (
<YoutubeVideo url={_document.url} />
)}
+ {_document?.type === "webpage" && (
+ <WebPageContent content={_document.content ?? ""} />
+ )}
</div>
<div
id="document-memories-summary"
diff --git a/apps/web/components/new/header.tsx b/apps/web/components/new/header.tsx
index da84b5e2..0a88459d 100644
--- a/apps/web/components/new/header.tsx
+++ b/apps/web/components/new/header.tsx
@@ -12,6 +12,9 @@ import {
Home,
Code2,
ExternalLink,
+ HelpCircle,
+ MenuIcon,
+ MessageCircleIcon,
} from "lucide-react"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
@@ -21,6 +24,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@ui/components/dropdown-menu"
import { authClient } from "@lib/auth"
@@ -29,17 +33,20 @@ import { useProject } from "@/stores"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { SpaceSelector } from "./space-selector"
+import { useIsMobile } from "@hooks/use-mobile"
interface HeaderProps {
onAddMemory?: () => void
onOpenMCP?: () => void
+ onOpenChat?: () => void
}
-export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
+export function Header({ onAddMemory, onOpenMCP, onOpenChat }: HeaderProps) {
const { user } = useAuth()
const { selectedProject } = useProject()
const { switchProject } = useProjectMutations()
const router = useRouter()
+ const isMobile = useIsMobile()
const displayName =
user?.displayUsername ||
@@ -48,16 +55,16 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
""
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
return (
- <div className="flex p-4 justify-between items-center">
- <div className="flex items-center justify-center gap-4 z-10!">
+ <div className="flex p-3 md:p-4 justify-between items-center gap-2">
+ <div className="flex items-center justify-center gap-2 md:gap-4 z-10! min-w-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
- className="flex items-center rounded-lg px-2 py-1.5 -ml-2 cursor-pointer hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors"
+ className="flex items-center rounded-lg px-2 py-1.5 -ml-2 cursor-pointer hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors shrink-0"
>
<Logo className="h-7" />
- {userName && (
+ {!isMobile && userName && (
<div className="flex flex-col items-start justify-center ml-2">
<p className="text-[#8B8B8B] text-[11px] leading-tight">
{userName}
@@ -71,143 +78,276 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
- className="w-56 bg-[#0D121A] rounded-xl border-none p-1.5 ml-4 shadow-[0_0_20px_rgba(255,255,255,0.15)]"
+ className={cn(
+ "min-w-[200px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
+ dmSansClassName(),
+ )}
+ style={{
+ background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
+ }}
>
<DropdownMenuItem
asChild
- className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
<Link href="/new">
- <Home className="h-4 w-4" />
+ <Home className="h-4 w-4 text-[#737373]" />
Home
</Link>
</DropdownMenuItem>
<DropdownMenuItem
asChild
- className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
<a
href="https://console.supermemory.ai"
target="_blank"
rel="noreferrer"
>
- <Code2 className="h-4 w-4" />
+ <Code2 className="h-4 w-4 text-[#737373]" />
Developer console
</a>
</DropdownMenuItem>
<DropdownMenuItem
asChild
- className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
<a href="https://supermemory.ai" target="_blank" rel="noreferrer">
- <ExternalLink className="h-4 w-4" />
+ <ExternalLink className="h-4 w-4 text-[#737373]" />
supermemory.ai
</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
- <div className="self-stretch w-px bg-[#FFFFFF33]" />
- <SpaceSelector
- value={selectedProject}
- onValueChange={switchProject}
- showChevron
- enableDelete
- />
+ <div className="self-stretch w-px bg-[#FFFFFF33] hidden md:block" />
+ {!isMobile && (
+ <SpaceSelector
+ value={selectedProject}
+ onValueChange={switchProject}
+ showChevron
+ enableDelete
+ />
+ )}
</div>
- <Tabs defaultValue="grid">
- <TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
- <TabsTrigger
- value="grid"
- className={cn(
- "rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
- dmSansClassName(),
- )}
- >
- <LayoutGridIcon className="size-4" />
- Grid
- </TabsTrigger>
- <TabsTrigger
- value="graph"
- className={cn(
- "rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
- dmSansClassName(),
- )}
- >
- <LayoutGridIcon className="size-4" />
- Graph
- </TabsTrigger>
- </TabsList>
- </Tabs>
+ {!isMobile && (
+ <Tabs defaultValue="grid">
+ <TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
+ <TabsTrigger
+ value="grid"
+ className={cn(
+ "rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
+ dmSansClassName(),
+ )}
+ >
+ <LayoutGridIcon className="size-4" />
+ Grid
+ </TabsTrigger>
+ <TabsTrigger
+ value="graph"
+ className={cn(
+ "rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
+ dmSansClassName(),
+ )}
+ >
+ <LayoutGridIcon className="size-4" />
+ Graph
+ </TabsTrigger>
+ </TabsList>
+ </Tabs>
+ )}
<div className="flex items-center gap-2 z-10!">
- <Button
- variant="headers"
- className="rounded-full text-base gap-2 h-10!"
- onClick={onAddMemory}
- >
- <div className="flex items-center gap-2">
- <Plus className="size-4" />
- Add memory
- </div>
- <span
- className={cn(
- "bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
- dmSansClassName(),
- )}
- >
- C
- </span>
- </Button>
- <Button
- variant="headers"
- className="rounded-full text-base gap-2 h-10!"
- onClick={onOpenMCP}
- >
- <div className="flex items-center gap-2">MCP</div>
- </Button>
- <Button
- variant="headers"
- className="rounded-full text-base gap-2 h-10!"
- >
- <SearchIcon className="size-4" />
- <span className="bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm text-[10px] flex items-center justify-center gap-0.5 px-1">
- <svg
- className="size-[7.5px]"
- viewBox="0 0 9 9"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
+ {isMobile ? (
+ <>
+ <SpaceSelector
+ value={selectedProject}
+ onValueChange={switchProject}
+ showChevron
+ enableDelete
+ compact
+ />
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="headers"
+ className="rounded-full text-base gap-2 h-10!"
+ >
+ <MenuIcon className="size-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent
+ align="end"
+ className={cn(
+ "min-w-[200px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
+ dmSansClassName(),
+ )}
+ style={{
+ background:
+ "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
+ }}
+ >
+ <DropdownMenuItem
+ onClick={onAddMemory}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+ <Plus className="h-4 w-4 text-[#737373]" />
+ Add memory
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onClick={onOpenMCP}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+ <Code2 className="h-4 w-4 text-[#737373]" />
+ MCP
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onClick={onOpenChat}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+ <MessageCircleIcon className="h-4 w-4 text-[#737373]" />
+ Chat with Nova
+ </DropdownMenuItem>
+ <DropdownMenuSeparator className="bg-[#2E3033]" />
+ <DropdownMenuItem
+ onClick={() => router.push("/new/settings")}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+ <Settings className="h-4 w-4 text-[#737373]" />
+ Settings
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </>
+ ) : (
+ <>
+ <Button
+ variant="headers"
+ className="rounded-full text-base gap-2 h-10!"
+ onClick={onAddMemory}
+ >
+ <div className="flex items-center gap-2">
+ <Plus className="size-4" />
+ Add memory
+ </div>
+ <span
+ className={cn(
+ "bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm size-4 text-[10px] flex items-center justify-center",
+ dmSansClassName(),
+ )}
+ >
+ C
+ </span>
+ </Button>
+ <Button
+ variant="headers"
+ className="rounded-full text-base gap-2 h-10!"
+ onClick={onOpenMCP}
>
- <title>Search Icon</title>
- <path
- d="M6.66663 0.416626C6.33511 0.416626 6.01716 0.548322 5.78274 0.782743C5.54832 1.01716 5.41663 1.33511 5.41663 1.66663V6.66663C5.41663 6.99815 5.54832 7.31609 5.78274 7.55051C6.01716 7.78493 6.33511 7.91663 6.66663 7.91663C6.99815 7.91663 7.31609 7.78493 7.55051 7.55051C7.78493 7.31609 7.91663 6.99815 7.91663 6.66663C7.91663 6.33511 7.78493 6.01716 7.55051 5.78274C7.31609 5.54832 6.99815 5.41663 6.66663 5.41663H1.66663C1.33511 5.41663 1.01716 5.54832 0.782743 5.78274C0.548322 6.01716 0.416626 6.33511 0.416626 6.66663C0.416626 6.99815 0.548322 7.31609 0.782743 7.55051C1.01716 7.78493 1.33511 7.91663 1.66663 7.91663C1.99815 7.91663 2.31609 7.78493 2.55051 7.55051C2.78493 7.31609 2.91663 6.99815 2.91663 6.66663V1.66663C2.91663 1.33511 2.78493 1.01716 2.55051 0.782743C2.31609 0.548322 1.99815 0.416626 1.66663 0.416626C1.33511 0.416626 1.01716 0.548322 0.782743 0.782743C0.548322 1.01716 0.416626 1.33511 0.416626 1.66663C0.416626 1.99815 0.548322 2.31609 0.782743 2.55051C1.01716 2.78493 1.33511 2.91663 1.66663 2.91663H6.66663C6.99815 2.91663 7.31609 2.78493 7.55051 2.55051C7.78493 2.31609 7.91663 1.99815 7.91663 1.66663C7.91663 1.33511 7.78493 1.01716 7.55051 0.782743C7.31609 0.548322 6.99815 0.416626 6.66663 0.416626Z"
- stroke="#737373"
- strokeWidth="0.833333"
- strokeLinecap="round"
- strokeLinejoin="round"
- />
- </svg>
- <span className={cn(dmSansClassName())}>K</span>
- </span>
- </Button>
+ <div className="flex items-center gap-2">MCP</div>
+ </Button>
+ <Button
+ variant="headers"
+ className="rounded-full text-base gap-2 h-10!"
+ >
+ <SearchIcon className="size-4" />
+ <span className="bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm text-[10px] flex items-center justify-center gap-0.5 px-1">
+ <svg
+ className="size-[7.5px]"
+ viewBox="0 0 9 9"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <title>Search Icon</title>
+ <path
+ d="M6.66663 0.416626C6.33511 0.416626 6.01716 0.548322 5.78274 0.782743C5.54832 1.01716 5.41663 1.33511 5.41663 1.66663V6.66663C5.41663 6.99815 5.54832 7.31609 5.78274 7.55051C6.01716 7.78493 6.33511 7.91663 6.66663 7.91663C6.99815 7.91663 7.31609 7.78493 7.55051 7.55051C7.78493 7.31609 7.91663 6.99815 7.91663 6.66663C7.91663 6.33511 7.78493 6.01716 7.55051 5.78274C7.31609 5.54832 6.99815 5.41663 6.66663 5.41663H1.66663C1.33511 5.41663 1.01716 5.54832 0.782743 5.78274C0.548322 6.01716 0.416626 6.33511 0.416626 6.66663C0.416626 6.99815 0.548322 7.31609 0.782743 7.55051C1.01716 7.78493 1.33511 7.91663 1.66663 7.91663C1.99815 7.91663 2.31609 7.78493 2.55051 7.55051C2.78493 7.31609 2.91663 6.99815 2.91663 6.66663V1.66663C2.91663 1.33511 2.78493 1.01716 2.55051 0.782743C2.31609 0.548322 1.99815 0.416626 1.66663 0.416626C1.33511 0.416626 1.01716 0.548322 0.782743 0.782743C0.548322 1.01716 0.416626 1.33511 0.416626 1.66663C0.416626 1.99815 0.548322 2.31609 0.782743 2.55051C1.01716 2.78493 1.33511 2.91663 1.66663 2.91663H6.66663C6.99815 2.91663 7.31609 2.78493 7.55051 2.55051C7.78493 2.31609 7.91663 1.99815 7.91663 1.66663C7.91663 1.33511 7.78493 1.01716 7.55051 0.782743C7.31609 0.548322 6.99815 0.416626 6.66663 0.416626Z"
+ stroke="#737373"
+ strokeWidth="0.833333"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ />
+ </svg>
+ <span className={cn(dmSansClassName())}>K</span>
+ </span>
+ </Button>
+ </>
+ )}
{user && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
- <Avatar className="border border-border h-8 w-8 md:h-10 md:w-10 cursor-pointer">
- <AvatarImage src={user?.image ?? ""} />
- <AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
- </Avatar>
+ <button
+ type="button"
+ className="rounded-full cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-transform hover:scale-105"
+ >
+ <Avatar className="border border-[#2E3033] h-8 w-8 md:h-10 md:w-10">
+ <AvatarImage src={user?.image ?? ""} />
+ <AvatarFallback className="bg-[#0D121A] text-white">
+ {user?.name?.charAt(0)}
+ </AvatarFallback>
+ </Avatar>
+ </button>
</DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem onClick={() => router.push("/new/settings")}>
- <Settings className="h-4 w-4" />
+ <DropdownMenuContent
+ align="end"
+ className={cn(
+ "min-w-[220px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
+ dmSansClassName(),
+ )}
+ style={{
+ background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
+ }}
+ >
+ <div id="user-info" className="px-3 py-2.5">
+ <p className="text-white text-sm font-medium truncate">
+ {user?.name}
+ </p>
+ <p className="text-[#737373] text-xs truncate">{user?.email}</p>
+ </div>
+ <DropdownMenuSeparator className="bg-[#2E3033]" />
+ <DropdownMenuItem
+ onClick={() => router.push("/new/settings")}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+ <Settings className="h-4 w-4 text-[#737373]" />
Settings
</DropdownMenuItem>
+ <DropdownMenuSeparator className="bg-[#2E3033]" />
+ <DropdownMenuItem
+ asChild
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+ <a href="mailto:[email protected]">
+ <HelpCircle className="h-4 w-4 text-[#737373]" />
+ Help & Support
+ </a>
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ asChild
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
+ >
+ <a
+ href="https://supermemory.link/discord"
+ target="_blank"
+ rel="noreferrer"
+ >
+ <svg
+ className="h-4 w-4 text-[#737373]"
+ viewBox="0 0 24 24"
+ fill="currentColor"
+ >
+ <title>Discord</title>
+ <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
+ </svg>
+ Discord
+ </a>
+ </DropdownMenuItem>
+ <DropdownMenuSeparator className="bg-[#2E3033]" />
<DropdownMenuItem
onClick={() => {
authClient.signOut()
router.push("/login/new")
}}
+ className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
- <LogOut className="h-4 w-4" />
+ <LogOut className="h-4 w-4 text-[#737373]" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
diff --git a/apps/web/components/new/mcp-modal/index.tsx b/apps/web/components/new/mcp-modal/index.tsx
index 4a5cc0b1..08fa15e0 100644
--- a/apps/web/components/new/mcp-modal/index.tsx
+++ b/apps/web/components/new/mcp-modal/index.tsx
@@ -67,7 +67,11 @@ export function MCPModal({
Migrate from MCP v1
</Button>
</div>
- <Button variant="insideOut" className="px-6 py-[10px]" onClick={onClose}>
+ <Button
+ variant="insideOut"
+ className="px-6 py-[10px]"
+ onClick={onClose}
+ >
Done
</Button>
</DialogFooter>
diff --git a/apps/web/components/new/onboarding/setup/chat-sidebar.tsx b/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
index dd6eeb23..22e8cae1 100644
--- a/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
+++ b/apps/web/components/new/onboarding/setup/chat-sidebar.tsx
@@ -14,6 +14,7 @@ import { dmSansClassName } from "@/lib/fonts"
import { useAuth } from "@lib/auth-context"
import { useProject } from "@/stores"
import { Streamdown } from "streamdown"
+import { useIsMobile } from "@hooks/use-mobile"
interface ChatSidebarProps {
formData: {
@@ -35,8 +36,9 @@ interface DraftDoc {
export function ChatSidebar({ formData }: ChatSidebarProps) {
const { user } = useAuth()
const { selectedProject } = useProject()
+ const isMobile = useIsMobile()
const [message, setMessage] = useState("")
- const [isChatOpen, setIsChatOpen] = useState(true)
+ const [isChatOpen, setIsChatOpen] = useState(!isMobile)
const [timelineMessages, setTimelineMessages] = useState<
{
message: string
@@ -405,44 +407,73 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
<motion.div
key="closed"
className={cn(
- "absolute top-0 right-0 flex items-start justify-start m-4",
+ "flex items-start justify-start",
+ isMobile
+ ? "fixed bottom-4 right-4 z-50"
+ : "absolute top-0 right-0 m-4",
dmSansClassName(),
)}
layoutId="chat-toggle-button"
>
<motion.button
onClick={toggleChat}
- className="flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer"
+ className={cn(
+ "flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer shadow-lg",
+ isMobile && "px-4 py-2",
+ )}
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
<NovaOrb size={24} className="blur-none! z-10" />
- Chat with Nova
+ {!isMobile && "Chat with Nova"}
</motion.button>
</motion.div>
) : (
<motion.div
key="open"
className={cn(
- "w-[450px] h-[calc(100vh-110px)] bg-[#0A0E14] backdrop-blur-md flex flex-col rounded-2xl m-4",
+ "bg-[#0A0E14] backdrop-blur-md flex flex-col",
+ isMobile
+ ? "fixed inset-0 z-50 w-full h-dvh rounded-none m-0"
+ : "w-[450px] h-[calc(100vh-110px)] rounded-2xl m-4",
dmSansClassName(),
)}
- initial={{ x: "100px", opacity: 0 }}
- animate={{ x: 0, opacity: 1 }}
- exit={{ x: "100px", opacity: 0 }}
+ initial={
+ isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
+ }
+ animate={{ x: 0, y: 0, opacity: 1 }}
+ exit={
+ isMobile ? { y: "100%", opacity: 0 } : { x: "100px", opacity: 0 }
+ }
transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }}
>
<motion.button
onClick={toggleChat}
- className="absolute top-4 right-4 flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer"
- style={{
- background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
- }}
+ className={cn(
+ "absolute top-4 right-4 flex items-center gap-2 rounded-full p-2 text-xs text-white cursor-pointer",
+ isMobile && "bg-[#0D121A] border border-[#73737333]",
+ )}
+ style={
+ isMobile
+ ? {
+ boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
+ }
+ : {
+ background:
+ "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
+ }
+ }
layoutId="chat-toggle-button"
>
- <PanelRightCloseIcon className="size-4" />
- Close chat
+ {isMobile ? (
+ <XIcon className="size-4" />
+ ) : (
+ <>
+ <PanelRightCloseIcon className="size-4" />
+ Close chat
+ </>
+ )}
</motion.button>
<div className="flex-1 flex flex-col px-4 space-y-3 pb-4 justify-end overflow-y-auto scrollbar-thin">
{timelineMessages.map((msg, i) => (
@@ -661,7 +692,8 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
xResearchStatus === "correct"
? "bg-green-500/20 text-green-400 border border-green-500/40"
: "bg-[#1B1F24] text-white/50 hover:text-white/70",
- (isConfirmed || isLoading) && "opacity-50 cursor-not-allowed",
+ (isConfirmed || isLoading) &&
+ "opacity-50 cursor-not-allowed",
)}
>
<CheckIcon className="size-3" />
diff --git a/apps/web/components/new/onboarding/setup/integrations-step.tsx b/apps/web/components/new/onboarding/setup/integrations-step.tsx
index 8f83bdfe..75e0d782 100644
--- a/apps/web/components/new/onboarding/setup/integrations-step.tsx
+++ b/apps/web/components/new/onboarding/setup/integrations-step.tsx
@@ -164,9 +164,7 @@ export function IntegrationsStep() {
<Button
variant="link"
className="text-white hover:text-gray-300 hover:no-underline cursor-pointer"
- onClick={() =>
- router.push("/new/onboarding/setup?step=relatable")
- }
+ onClick={() => router.push("/new/onboarding/setup?step=relatable")}
>
โ† Back
</Button>
diff --git a/apps/web/components/new/space-selector.tsx b/apps/web/components/new/space-selector.tsx
index adfbdee1..be2fcb50 100644
--- a/apps/web/components/new/space-selector.tsx
+++ b/apps/web/components/new/space-selector.tsx
@@ -37,6 +37,7 @@ export interface SpaceSelectorProps {
contentClassName?: string
showNewSpace?: boolean
enableDelete?: boolean
+ compact?: boolean
}
const triggerVariants = {
@@ -53,6 +54,7 @@ export function SpaceSelector({
contentClassName,
showNewSpace = true,
enableDelete = false,
+ compact = false,
}: SpaceSelectorProps) {
const [isOpen, setIsOpen] = useState(false)
const [showCreateDialog, setShowCreateDialog] = useState(false)
@@ -87,7 +89,9 @@ export function SpaceSelector({
const selectedProject = useMemo(() => {
if (value === DEFAULT_PROJECT_ID) return { name: "My Space", emoji: "๐Ÿ“" }
const found = projects.find((p: Project) => p.containerTag === value)
- return found ? { name: found.name, emoji: found.emoji } : { name: value, emoji: undefined }
+ return found
+ ? { name: found.name, emoji: found.emoji }
+ : { name: value, emoji: undefined }
}, [projects, value])
const selectedProjectName = selectedProject.name
@@ -125,7 +129,9 @@ export function SpaceSelector({
projectId: deleteDialog.project.id,
action: deleteDialog.action,
targetProjectId:
- deleteDialog.action === "move" ? deleteDialog.targetProjectId : undefined,
+ deleteDialog.action === "move"
+ ? deleteDialog.targetProjectId
+ : undefined,
},
{
onSuccess: () => {
@@ -156,14 +162,14 @@ export function SpaceSelector({
p.id !== deleteDialog.project?.id &&
p.containerTag !== deleteDialog.project?.containerTag,
)
-
+
const defaultProject = projects.find(
(p: Project) => p.containerTag === DEFAULT_PROJECT_ID,
)
-
+
const isDefaultProjectBeingDeleted =
deleteDialog.project?.containerTag === DEFAULT_PROJECT_ID
-
+
if (defaultProject && !isDefaultProjectBeingDeleted) {
const defaultProjectIncluded = filtered.some(
(p: Project) => p.containerTag === DEFAULT_PROJECT_ID,
@@ -172,7 +178,7 @@ export function SpaceSelector({
return [defaultProject, ...filtered]
}
}
-
+
return filtered
}, [projects, deleteDialog.project])
@@ -189,10 +195,14 @@ export function SpaceSelector({
triggerClassName,
)}
>
- <span className="text-sm font-bold tracking-[-0.98px]">{selectedProjectEmoji}</span>
- <span className="text-sm font-medium text-white">
- {isLoading ? "..." : selectedProjectName}
+ <span className="text-sm font-bold tracking-[-0.98px]">
+ {selectedProjectEmoji}
</span>
+ {!compact && (
+ <span className="text-sm font-medium text-white">
+ {isLoading ? "..." : selectedProjectName}
+ </span>
+ )}
{showChevron && (
<ChevronsLeftRight className="size-4 rotate-90 text-white/70" />
)}
@@ -201,7 +211,7 @@ export function SpaceSelector({
<DropdownMenuContent
align="start"
className={cn(
- "min-w-[200px] p-3 rounded-[11px] border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
+ "min-w-[200px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
dmSansClassName(),
contentClassName,
)}
@@ -209,16 +219,16 @@ export function SpaceSelector({
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
>
- <div className="flex flex-col gap-3">
+ <div className="flex flex-col gap-2">
<div className="flex flex-col">
{/* Default Project - no delete allowed */}
<DropdownMenuItem
onClick={() => handleSelect(DEFAULT_PROJECT_ID)}
className={cn(
- "flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer text-white text-sm font-medium",
+ "flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium",
value === DEFAULT_PROJECT_ID
- ? "bg-[#161E2B] border border-[rgba(115,115,115,0.1)]"
- : "opacity-50 hover:opacity-100 hover:bg-[#161E2B]/50",
+ ? "bg-[#293952]/40"
+ : "opacity-60 hover:opacity-100 hover:bg-[#293952]/40",
)}
>
<span className="font-bold tracking-[-0.98px]">๐Ÿ“</span>
@@ -233,13 +243,15 @@ export function SpaceSelector({
key={project.id}
onClick={() => handleSelect(project.containerTag)}
className={cn(
- "flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer text-white text-sm font-medium group",
+ "flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer text-white text-sm font-medium group",
value === project.containerTag
- ? "bg-[#161E2B] border border-[rgba(115,115,115,0.1)]"
- : "opacity-50 hover:opacity-100 hover:bg-[#161E2B]/50",
+ ? "bg-[#293952]/40"
+ : "opacity-60 hover:opacity-100 hover:bg-[#293952]/40",
)}
>
- <span className="font-bold tracking-[-0.98px]">{project.emoji || "๐Ÿ“"}</span>
+ <span className="font-bold tracking-[-0.98px]">
+ {project.emoji || "๐Ÿ“"}
+ </span>
<span className="truncate flex-1">{project.name}</span>
{enableDelete && (
<button
@@ -309,9 +321,17 @@ export function SpaceSelector({
showCloseButton={false}
>
<div className="flex flex-col gap-4">
- <div id="delete-dialog-header" className="flex justify-between items-start gap-4">
+ <div
+ id="delete-dialog-header"
+ className="flex justify-between items-start gap-4"
+ >
<div className="pl-1 space-y-1 flex-1">
- <p className={cn("font-semibold text-[#fafafa]", dmSans125ClassName())}>
+ <p
+ className={cn(
+ "font-semibold text-[#fafafa]",
+ dmSans125ClassName(),
+ )}
+ >
Delete space
</p>
<p className="text-[#737373] font-medium text-[16px] leading-[1.35]">
@@ -325,7 +345,8 @@ export function SpaceSelector({
<DialogPrimitive.Close
className="bg-[#0D121A] w-7 h-7 flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 border border-[rgba(115,115,115,0.2)] shrink-0"
style={{
- boxShadow: "inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
+ boxShadow:
+ "inset 1.313px 1.313px 3.938px 0px rgba(0,0,0,0.7)",
}}
>
<XIcon stroke="#737373" />
@@ -337,7 +358,9 @@ export function SpaceSelector({
<button
id="move-option"
type="button"
- onClick={() => setDeleteDialog((prev) => ({ ...prev, action: "move" }))}
+ onClick={() =>
+ setDeleteDialog((prev) => ({ ...prev, action: "move" }))
+ }
className={cn(
"flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors w-full text-left",
deleteDialog.action === "move"
@@ -415,7 +438,9 @@ export function SpaceSelector({
<span className="flex items-center gap-2">
<span>{p.emoji || "๐Ÿ“"}</span>
<span>
- {p.containerTag === DEFAULT_PROJECT_ID ? "My Space" : p.name}
+ {p.containerTag === DEFAULT_PROJECT_ID
+ ? "My Space"
+ : p.name}
</span>
</span>
</SelectItem>
@@ -428,7 +453,9 @@ export function SpaceSelector({
<button
id="delete-option"
type="button"
- onClick={() => setDeleteDialog((prev) => ({ ...prev, action: "delete" }))}
+ onClick={() =>
+ setDeleteDialog((prev) => ({ ...prev, action: "delete" }))
+ }
className={cn(
"flex items-center gap-3 p-3 rounded-[12px] cursor-pointer transition-colors w-full text-left",
deleteDialog.action === "delete"
@@ -470,7 +497,10 @@ export function SpaceSelector({
)}
</div>
- <div id="delete-dialog-footer" className="flex items-center justify-end gap-[22px]">
+ <div
+ id="delete-dialog-footer"
+ className="flex items-center justify-end gap-[22px]"
+ >
<button
type="button"
onClick={handleDeleteCancel}
@@ -487,7 +517,8 @@ export function SpaceSelector({
onClick={handleDeleteConfirm}
disabled={
deleteProjectMutation.isPending ||
- (deleteDialog.action === "move" && !deleteDialog.targetProjectId)
+ (deleteDialog.action === "move" &&
+ !deleteDialog.targetProjectId)
}
className={cn(
"px-4 py-[10px] rounded-full",
@@ -498,7 +529,9 @@ export function SpaceSelector({
{deleteProjectMutation.isPending ? (
<>
<Loader2 className="size-4 animate-spin mr-2" />
- {deleteDialog.action === "move" ? "Moving..." : "Deleting..."}
+ {deleteDialog.action === "move"
+ ? "Moving..."
+ : "Deleting..."}
</>
) : deleteDialog.action === "move" ? (
"Move & Delete"
diff --git a/apps/web/components/new/text-editor/index.tsx b/apps/web/components/new/text-editor/index.tsx
index 6446863d..d99a0aea 100644
--- a/apps/web/components/new/text-editor/index.tsx
+++ b/apps/web/components/new/text-editor/index.tsx
@@ -82,28 +82,25 @@ export function TextEditor({
}
}, [editor, initialContent])
- const handleClick = useCallback(
- (e: React.MouseEvent<HTMLDivElement>) => {
- const target = e.target as HTMLElement
- if (target.closest(".ProseMirror")) {
- return
- }
- if (target.closest("button, a")) {
- return
- }
+ const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
+ const target = e.target as HTMLElement
+ if (target.closest(".ProseMirror")) {
+ return
+ }
+ if (target.closest("button, a")) {
+ return
+ }
- const proseMirror = containerRef.current?.querySelector(
- ".ProseMirror",
- ) as HTMLElement
- if (proseMirror && editorRef.current) {
- setTimeout(() => {
- proseMirror.focus()
- editorRef.current?.commands.focus("end")
- }, 0)
- }
- },
- [],
- )
+ const proseMirror = containerRef.current?.querySelector(
+ ".ProseMirror",
+ ) as HTMLElement
+ if (proseMirror && editorRef.current) {
+ setTimeout(() => {
+ proseMirror.focus()
+ editorRef.current?.commands.focus("end")
+ }, 0)
+ }
+ }, [])
useEffect(() => {
return () => {
@@ -132,9 +129,7 @@ export function TextEditor({
<div className="flex items-center gap-1 rounded-[8px] bg-[#1b1f24] p-2 shadow-[0px_4px_20px_0px_rgba(0,0,0,0.25),inset_1px_1px_1px_0px_rgba(255,255,255,0.1)]">
<button
type="button"
- onClick={() =>
- editor.chain().focus().toggleBold().run()
- }
+ onClick={() => editor.chain().focus().toggleBold().run()}
className={cn(
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
editor.isActive("bold") && "bg-[#2e353d]",
@@ -144,9 +139,7 @@ export function TextEditor({
</button>
<button
type="button"
- onClick={() =>
- editor.chain().focus().toggleItalic().run()
- }
+ onClick={() => editor.chain().focus().toggleItalic().run()}
className={cn(
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
editor.isActive("italic") && "bg-[#2e353d]",
@@ -156,9 +149,7 @@ export function TextEditor({
</button>
<button
type="button"
- onClick={() =>
- editor.chain().focus().toggleCode().run()
- }
+ onClick={() => editor.chain().focus().toggleCode().run()}
className={cn(
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
editor.isActive("code") && "bg-[#2e353d]",
diff --git a/apps/web/components/new/text-editor/slash-command.tsx b/apps/web/components/new/text-editor/slash-command.tsx
index 31991093..3b6f3a33 100644
--- a/apps/web/components/new/text-editor/slash-command.tsx
+++ b/apps/web/components/new/text-editor/slash-command.tsx
@@ -169,9 +169,7 @@ export function createSlashCommand(items: SuggestionItem[]) {
onStart: (props) => {
selectedIndex = 0
currentItems = props.items as SuggestionItem[]
- currentCommand = props.command as (
- item: SuggestionItem,
- ) => void
+ currentCommand = props.command as (item: SuggestionItem) => void
currentClientRect = props.clientRect ?? null
const element = document.createElement("div")
@@ -208,9 +206,7 @@ export function createSlashCommand(items: SuggestionItem[]) {
onUpdate: (props) => {
currentItems = props.items as SuggestionItem[]
- currentCommand = props.command as (
- item: SuggestionItem,
- ) => void
+ currentCommand = props.command as (item: SuggestionItem) => void
currentClientRect = props.clientRect ?? null
if (selectedIndex >= currentItems.length) {
@@ -238,9 +234,7 @@ export function createSlashCommand(items: SuggestionItem[]) {
if (event.key === "ArrowUp") {
selectedIndex =
- selectedIndex <= 0
- ? currentItems.length - 1
- : selectedIndex - 1
+ selectedIndex <= 0 ? currentItems.length - 1 : selectedIndex - 1
if (currentCommand) {
component?.updateProps({
items: currentItems,
@@ -254,9 +248,7 @@ export function createSlashCommand(items: SuggestionItem[]) {
if (event.key === "ArrowDown") {
selectedIndex =
- selectedIndex >= currentItems.length - 1
- ? 0
- : selectedIndex + 1
+ selectedIndex >= currentItems.length - 1 ? 0 : selectedIndex + 1
if (currentCommand) {
component?.updateProps({
items: currentItems,
diff --git a/apps/web/components/new/utils.ts b/apps/web/components/new/utils.ts
index b830a2a3..cd0cbc39 100644
--- a/apps/web/components/new/utils.ts
+++ b/apps/web/components/new/utils.ts
@@ -65,13 +65,12 @@ export function useYouTubeChannelName(url: string | undefined | null) {
})
}
-
export function getAbsoluteUrl(url: string): string {
try {
const urlObj = new URL(url)
return urlObj.host.replace(/^www\./, "")
} catch {
- const match = url.match(/^https?:\/\/([^\/]+)/)
+ const match = url.match(/^https?:\/\/([^/]+)/)
const host = match?.[1] ?? url.replace(/^https?:\/\//, "")
return host.replace(/^www\./, "")
}
diff --git a/apps/web/components/spinner.tsx b/apps/web/components/spinner.tsx
index 6e14eb5b..46bc6c58 100644
--- a/apps/web/components/spinner.tsx
+++ b/apps/web/components/spinner.tsx
@@ -1,6 +1,6 @@
-import { cn } from "@lib/utils";
-import { Loader2 } from "lucide-react";
+import { cn } from "@lib/utils"
+import { Loader2 } from "lucide-react"
export function Spinner({ className }: { className?: string }) {
- return <Loader2 className={cn("size-4 animate-spin", className)} />;
+ return <Loader2 className={cn("size-4 animate-spin", className)} />
}
diff --git a/apps/web/components/text-morph.tsx b/apps/web/components/text-morph.tsx
index 467dc999..65dab8c7 100644
--- a/apps/web/components/text-morph.tsx
+++ b/apps/web/components/text-morph.tsx
@@ -1,74 +1,79 @@
-'use client';
-import { cn } from '@lib/utils';
-import { AnimatePresence, motion, type Transition, type Variants } from 'motion/react';
-import { useMemo, useId } from 'react';
+"use client"
+import { cn } from "@lib/utils"
+import {
+ AnimatePresence,
+ motion,
+ type Transition,
+ type Variants,
+} from "motion/react"
+import { useMemo, useId } from "react"
export type TextMorphProps = {
- children: string;
- as?: React.ElementType;
- className?: string;
- style?: React.CSSProperties;
- variants?: Variants;
- transition?: Transition;
-};
+ children: string
+ as?: React.ElementType
+ className?: string
+ style?: React.CSSProperties
+ variants?: Variants
+ transition?: Transition
+}
export function TextMorph({
- children,
- as: Component = 'p',
- className,
- style,
- variants,
- transition,
+ children,
+ as: Component = "p",
+ className,
+ style,
+ variants,
+ transition,
}: TextMorphProps) {
- const uniqueId = useId();
+ const uniqueId = useId()
- const characters = useMemo(() => {
- const charCounts: Record<string, number> = {};
+ const characters = useMemo(() => {
+ const charCounts: Record<string, number> = {}
- return children.split('').map((char) => {
- const lowerChar = char.toLowerCase();
- charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;
+ return children.split("").map((char) => {
+ const lowerChar = char.toLowerCase()
+ charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1
- return {
- id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
- label: char === ' ' ? '\u00A0' : char,
- };
- });
- }, [children, uniqueId]);
+ return {
+ id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
+ label: char === " " ? "\u00A0" : char,
+ }
+ })
+ }, [children, uniqueId])
- const defaultVariants: Variants = {
- initial: { opacity: 0 },
- animate: { opacity: 1 },
- exit: { opacity: 0 },
- };
+ const defaultVariants: Variants = {
+ initial: { opacity: 0 },
+ animate: { opacity: 1 },
+ exit: { opacity: 0 },
+ }
- const defaultTransition: Transition = {
- type: 'spring',
- stiffness: 280,
- damping: 18,
- mass: 0.3,
- };
+ const defaultTransition: Transition = {
+ type: "spring",
+ stiffness: 280,
+ damping: 18,
+ mass: 0.3,
+ }
- return (
- // @ts-expect-error - style is optional
- <Component className={cn(className)} aria-label={children} style={style}>
- <AnimatePresence mode='popLayout' initial={false}>
- {characters.map((character) => (
- <motion.span
- key={character.id}
- layoutId={character.id}
- className='inline-block'
- aria-hidden='true'
- initial='initial'
- animate='animate'
- exit='exit'
- variants={variants || defaultVariants}
- transition={transition || defaultTransition}
- >
- {character.label}
- </motion.span>
- ))}
- </AnimatePresence>
- </Component>
- );
+ return (
+ // @ts-expect-error - style is optional
+ <Component className={cn(className)} aria-label={children} style={style}>
+ <AnimatePresence mode="popLayout" initial={false}>
+ {characters.map((character) => (
+ <motion.span
+ key={character.id}
+ layoutId={character.id}
+ className="inline-block"
+ aria-hidden="true"
+ initial="initial"
+ animate="animate"
+ exit="exit"
+ variants={variants || defaultVariants}
+ transition={transition || defaultTransition}
+ >
+ {character.label}
+ </motion.span>
+ ))}
+ </AnimatePresence>
+ </Component>
+ )
}
diff --git a/apps/web/components/text-shimmer.tsx b/apps/web/components/text-shimmer.tsx
index 815200fe..a9abd870 100644
--- a/apps/web/components/text-shimmer.tsx
+++ b/apps/web/components/text-shimmer.tsx
@@ -1,15 +1,15 @@
-"use client";
-import { cn } from "@lib/utils";
-import { motion } from "motion/react";
-import React, { type JSX, useMemo } from "react";
+"use client"
+import { cn } from "@lib/utils"
+import { motion } from "motion/react"
+import React, { type JSX, useMemo } from "react"
export type TextShimmerProps = {
- children: string;
- as?: React.ElementType;
- className?: string;
- duration?: number;
- spread?: number;
-};
+ children: string
+ as?: React.ElementType
+ className?: string
+ duration?: number
+ spread?: number
+}
function TextShimmerComponent({
children,
@@ -20,11 +20,11 @@ function TextShimmerComponent({
}: TextShimmerProps) {
const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements,
- );
+ )
const dynamicSpread = useMemo(() => {
- return children.length * spread;
- }, [children, spread]);
+ return children.length * spread
+ }, [children, spread])
return (
<MotionComponent
@@ -45,13 +45,14 @@ function TextShimmerComponent({
style={
{
"--spread": `${dynamicSpread}px`,
- backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`,
+ backgroundImage:
+ "var(--bg), linear-gradient(var(--base-color), var(--base-color))",
} as React.CSSProperties
}
>
{children}
</MotionComponent>
- );
+ )
}
-export const TextShimmer = React.memo(TextShimmerComponent);
+export const TextShimmer = React.memo(TextShimmerComponent)
diff --git a/apps/web/components/views/add-memory/index.tsx b/apps/web/components/views/add-memory/index.tsx
index 4e250b53..1a4469f9 100644
--- a/apps/web/components/views/add-memory/index.tsx
+++ b/apps/web/components/views/add-memory/index.tsx
@@ -420,9 +420,12 @@ export function AddMemoryView({
const formData = new FormData()
formData.append("file", file)
formData.append("containerTags", JSON.stringify([project]))
- formData.append("metadata", JSON.stringify({
- sm_source: "consumer",
- }))
+ formData.append(
+ "metadata",
+ JSON.stringify({
+ sm_source: "consumer",
+ }),
+ )
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/documents/file`,
diff --git a/apps/web/components/views/add-memory/project-selection.tsx b/apps/web/components/views/add-memory/project-selection.tsx
index 6500cd3d..be533689 100644
--- a/apps/web/components/views/add-memory/project-selection.tsx
+++ b/apps/web/components/views/add-memory/project-selection.tsx
@@ -1,90 +1,90 @@
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@repo/ui/components/select';
-import { Plus } from 'lucide-react';
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@repo/ui/components/select"
+import { Plus } from "lucide-react"
interface Project {
- id?: string;
- containerTag: string;
- name: string;
+ id?: string
+ containerTag: string
+ name: string
}
interface ProjectSelectionProps {
- projects: Project[];
- selectedProject: string;
- onProjectChange: (value: string) => void;
- onCreateProject: () => void;
- disabled?: boolean;
- isLoading?: boolean;
- className?: string;
- id?: string;
+ projects: Project[]
+ selectedProject: string
+ onProjectChange: (value: string) => void
+ onCreateProject: () => void
+ disabled?: boolean
+ isLoading?: boolean
+ className?: string
+ id?: string
}
export function ProjectSelection({
- projects,
- selectedProject,
- onProjectChange,
- onCreateProject,
- disabled = false,
- isLoading = false,
- className = '',
- id = 'project-select',
+ projects,
+ selectedProject,
+ onProjectChange,
+ onCreateProject,
+ disabled = false,
+ isLoading = false,
+ className = "",
+ id = "project-select",
}: ProjectSelectionProps) {
- const handleValueChange = (value: string) => {
- if (value === 'create-new-project') {
- onCreateProject();
- } else {
- onProjectChange(value);
- }
- };
+ const handleValueChange = (value: string) => {
+ if (value === "create-new-project") {
+ onCreateProject()
+ } else {
+ onProjectChange(value)
+ }
+ }
- return (
- <Select
- key={`${id}-${selectedProject}`}
- disabled={isLoading || disabled}
- onValueChange={handleValueChange}
- value={selectedProject}
- >
- <SelectTrigger
- className={`bg-foreground/5 border-foreground/10 cursor-pointer ${className}`}
- id={id}
- >
- <SelectValue placeholder="Select a project" />
- </SelectTrigger>
- <SelectContent position="popper" sideOffset={5} className="z-[90]">
- <SelectItem
- className="hover:bg-foreground/10"
- key="default"
- value="sm_project_default"
- >
- Default Project
- </SelectItem>
- {projects
- .filter((p) => p.containerTag !== 'sm_project_default' && p.id)
- .map((project) => (
- <SelectItem
- className="hover:bg-foreground/10"
- key={project.id || project.containerTag}
- value={project.containerTag}
- >
- {project.name}
- </SelectItem>
- ))}
- <SelectItem
- className="hover:bg-foreground/10 border-t border-foreground/10 mt-1"
- key="create-new"
- value="create-new-project"
- >
- <div className="flex items-center gap-2">
- <Plus className="h-4 w-4" />
- <span>Create new project</span>
- </div>
- </SelectItem>
- </SelectContent>
- </Select>
- );
+ return (
+ <Select
+ key={`${id}-${selectedProject}`}
+ disabled={isLoading || disabled}
+ onValueChange={handleValueChange}
+ value={selectedProject}
+ >
+ <SelectTrigger
+ className={`bg-foreground/5 border-foreground/10 cursor-pointer ${className}`}
+ id={id}
+ >
+ <SelectValue placeholder="Select a project" />
+ </SelectTrigger>
+ <SelectContent position="popper" sideOffset={5} className="z-[90]">
+ <SelectItem
+ className="hover:bg-foreground/10"
+ key="default"
+ value="sm_project_default"
+ >
+ Default Project
+ </SelectItem>
+ {projects
+ .filter((p) => p.containerTag !== "sm_project_default" && p.id)
+ .map((project) => (
+ <SelectItem
+ className="hover:bg-foreground/10"
+ key={project.id || project.containerTag}
+ value={project.containerTag}
+ >
+ {project.name}
+ </SelectItem>
+ ))}
+ <SelectItem
+ className="hover:bg-foreground/10 border-t border-foreground/10 mt-1"
+ key="create-new"
+ value="create-new-project"
+ >
+ <div className="flex items-center gap-2">
+ <Plus className="h-4 w-4" />
+ <span>Create new project</span>
+ </div>
+ </SelectItem>
+ </SelectContent>
+ </Select>
+ )
}
diff --git a/apps/web/components/views/add-memory/tab-button.tsx b/apps/web/components/views/add-memory/tab-button.tsx
index 72dfbbd7..7713cf31 100644
--- a/apps/web/components/views/add-memory/tab-button.tsx
+++ b/apps/web/components/views/add-memory/tab-button.tsx
@@ -1,28 +1,28 @@
-import type { LucideIcon } from 'lucide-react';
+import type { LucideIcon } from "lucide-react"
interface TabButtonProps {
- icon: LucideIcon;
- label: string;
- isActive: boolean;
- onClick: () => void;
+ icon: LucideIcon
+ label: string
+ isActive: boolean
+ onClick: () => void
}
export function TabButton({
- icon: Icon,
- label,
- isActive,
- onClick,
+ icon: Icon,
+ label,
+ isActive,
+ onClick,
}: TabButtonProps) {
- return (
- <button
- className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
- isActive ? 'bg-white/10' : 'hover:bg-white/5'
- }`}
- onClick={onClick}
- type="button"
- >
- <Icon className="h-4 w-4 sm:h-3 sm:w-3" />
- {label}
- </button>
- );
+ return (
+ <button
+ className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
+ isActive ? "bg-white/10" : "hover:bg-white/5"
+ }`}
+ onClick={onClick}
+ type="button"
+ >
+ <Icon className="h-4 w-4 sm:h-3 sm:w-3" />
+ {label}
+ </button>
+ )
}
diff --git a/apps/web/components/views/add-memory/text-editor.tsx b/apps/web/components/views/add-memory/text-editor.tsx
index 08d45c42..e3b9bcd2 100644
--- a/apps/web/components/views/add-memory/text-editor.tsx
+++ b/apps/web/components/views/add-memory/text-editor.tsx
@@ -1,8 +1,8 @@
-"use client";
+"use client"
-import { cn } from "@lib/utils";
-import { Button } from "@repo/ui/components/button";
-import isHotkey from "is-hotkey";
+import { cn } from "@lib/utils"
+import { Button } from "@repo/ui/components/button"
+import isHotkey from "is-hotkey"
import {
Bold,
Code,
@@ -12,16 +12,16 @@ import {
Italic,
List,
Quote,
-} from "lucide-react";
-import { useCallback, useMemo, useState } from "react";
+} from "lucide-react"
+import { useCallback, useMemo, useState } from "react"
import {
type BaseEditor,
createEditor,
type Descendant,
Editor,
Transforms,
-} from "slate";
-import type { ReactEditor as ReactEditorType } from "slate-react";
+} from "slate"
+import type { ReactEditor as ReactEditorType } from "slate-react"
import {
Editable,
ReactEditor,
@@ -29,51 +29,51 @@ import {
type RenderLeafProps,
Slate,
withReact,
-} from "slate-react";
+} from "slate-react"
-type CustomEditor = BaseEditor & ReactEditorType;
+type CustomEditor = BaseEditor & ReactEditorType
type ParagraphElement = {
- type: "paragraph";
- children: CustomText[];
-};
+ type: "paragraph"
+ children: CustomText[]
+}
type HeadingElement = {
- type: "heading";
- level: number;
- children: CustomText[];
-};
+ type: "heading"
+ level: number
+ children: CustomText[]
+}
type ListItemElement = {
- type: "list-item";
- children: CustomText[];
-};
+ type: "list-item"
+ children: CustomText[]
+}
type BlockQuoteElement = {
- type: "block-quote";
- children: CustomText[];
-};
+ type: "block-quote"
+ children: CustomText[]
+}
type CustomElement =
| ParagraphElement
| HeadingElement
| ListItemElement
- | BlockQuoteElement;
+ | BlockQuoteElement
type FormattedText = {
- text: string;
- bold?: true;
- italic?: true;
- code?: true;
-};
+ text: string
+ bold?: true
+ italic?: true
+ code?: true
+}
-type CustomText = FormattedText;
+type CustomText = FormattedText
declare module "slate" {
interface CustomTypes {
- Editor: CustomEditor;
- Element: CustomElement;
- Text: CustomText;
+ Editor: CustomEditor
+ Element: CustomElement
+ Text: CustomText
}
}
@@ -82,16 +82,16 @@ const HOTKEYS: Record<string, keyof CustomText> = {
"mod+b": "bold",
"mod+i": "italic",
"mod+`": "code",
-};
+}
interface TextEditorProps {
- value?: string;
- onChange?: (value: string) => void;
- onBlur?: () => void;
- placeholder?: string;
- disabled?: boolean;
- className?: string;
- containerClassName?: string;
+ value?: string
+ onChange?: (value: string) => void
+ onBlur?: () => void
+ placeholder?: string
+ disabled?: boolean
+ className?: string
+ containerClassName?: string
}
const initialValue: Descendant[] = [
@@ -99,114 +99,114 @@ const initialValue: Descendant[] = [
type: "paragraph",
children: [{ text: "" }],
},
-];
+]
const serialize = (nodes: Descendant[]): string => {
- return nodes.map((n) => serializeNode(n)).join("\n");
-};
+ return nodes.map((n) => serializeNode(n)).join("\n")
+}
const serializeNode = (node: CustomElement | CustomText): string => {
if ("text" in node) {
- let text = node.text;
- if (node.bold) text = `**${text}**`;
- if (node.italic) text = `*${text}*`;
- if (node.code) text = `\`${text}\``;
- return text;
+ let text = node.text
+ if (node.bold) text = `**${text}**`
+ if (node.italic) text = `*${text}*`
+ if (node.code) text = `\`${text}\``
+ return text
}
const children = node.children
? node.children.map(serializeNode).join("")
- : "";
+ : ""
switch (node.type) {
case "paragraph":
- return children;
+ return children
case "heading":
- return `${"#".repeat(node.level || 1)} ${children}`;
+ return `${"#".repeat(node.level || 1)} ${children}`
case "list-item":
- return `- ${children}`;
+ return `- ${children}`
case "block-quote":
- return `> ${children}`;
+ return `> ${children}`
default:
- return children;
+ return children
}
-};
+}
const deserialize = (text: string): Descendant[] => {
if (!text.trim()) {
- return initialValue;
+ return initialValue
}
- const lines = text.split("\n");
- const nodes: Descendant[] = [];
+ const lines = text.split("\n")
+ const nodes: Descendant[] = []
for (const line of lines) {
- const trimmedLine = line.trim();
+ const trimmedLine = line.trim()
if (trimmedLine.startsWith("# ")) {
nodes.push({
type: "heading",
level: 1,
children: [{ text: trimmedLine.slice(2) }],
- });
+ })
} else if (trimmedLine.startsWith("## ")) {
nodes.push({
type: "heading",
level: 2,
children: [{ text: trimmedLine.slice(3) }],
- });
+ })
} else if (trimmedLine.startsWith("### ")) {
nodes.push({
type: "heading",
level: 3,
children: [{ text: trimmedLine.slice(4) }],
- });
+ })
} else if (trimmedLine.startsWith("- ")) {
nodes.push({
type: "list-item",
children: [{ text: trimmedLine.slice(2) }],
- });
+ })
} else if (trimmedLine.startsWith("> ")) {
nodes.push({
type: "block-quote",
children: [{ text: trimmedLine.slice(2) }],
- });
+ })
} else {
nodes.push({
type: "paragraph",
children: [{ text: line }],
- });
+ })
}
}
- return nodes.length > 0 ? nodes : initialValue;
-};
+ return nodes.length > 0 ? nodes : initialValue
+}
const isMarkActive = (editor: CustomEditor, format: keyof CustomText) => {
- const marks = Editor.marks(editor);
- return marks ? marks[format as keyof typeof marks] === true : false;
-};
+ const marks = Editor.marks(editor)
+ return marks ? marks[format as keyof typeof marks] === true : false
+}
const toggleMark = (editor: CustomEditor, format: keyof CustomText) => {
- const isActive = isMarkActive(editor, format);
+ const isActive = isMarkActive(editor, format)
if (isActive) {
- Editor.removeMark(editor, format);
+ Editor.removeMark(editor, format)
} else {
- Editor.addMark(editor, format, true);
+ Editor.addMark(editor, format, true)
}
// Focus back to editor after toggling
- ReactEditor.focus(editor);
-};
+ ReactEditor.focus(editor)
+}
const isBlockActive = (
editor: CustomEditor,
format: string,
level?: number,
) => {
- const { selection } = editor;
- if (!selection) return false;
+ const { selection } = editor
+ if (!selection) return false
const [match] = Array.from(
Editor.nodes(editor, {
@@ -216,26 +216,26 @@ const isBlockActive = (
(n as CustomElement).type === format &&
(level === undefined || (n as HeadingElement).level === level),
}),
- );
+ )
- return !!match;
-};
+ return !!match
+}
const toggleBlock = (editor: CustomEditor, format: string, level?: number) => {
- const isActive = isBlockActive(editor, format, level);
+ const isActive = isBlockActive(editor, format, level)
const newProperties: any = {
type: isActive ? "paragraph" : format,
- };
+ }
if (format === "heading" && level && !isActive) {
- newProperties.level = level;
+ newProperties.level = level
}
- Transforms.setNodes(editor, newProperties);
+ Transforms.setNodes(editor, newProperties)
// Focus back to editor after toggling
- ReactEditor.focus(editor);
-};
+ ReactEditor.focus(editor)
+}
export function TextEditor({
value = "",
@@ -246,23 +246,23 @@ export function TextEditor({
className,
containerClassName,
}: TextEditorProps) {
- const editor = useMemo(() => withReact(createEditor()) as CustomEditor, []);
+ const editor = useMemo(() => withReact(createEditor()) as CustomEditor, [])
const [editorValue, setEditorValue] = useState<Descendant[]>(() =>
deserialize(value),
- );
- const [selection, setSelection] = useState(editor.selection);
+ )
+ const [selection, setSelection] = useState(editor.selection)
const renderElement = useCallback((props: RenderElementProps) => {
switch (props.element.type) {
case "heading": {
- const element = props.element as HeadingElement;
+ const element = props.element as HeadingElement
const HeadingTag = `h${element.level || 1}` as
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
- | "h6";
+ | "h6"
return (
<HeadingTag
{...props.attributes}
@@ -275,14 +275,14 @@ export function TextEditor({
>
{props.children}
</HeadingTag>
- );
+ )
}
case "list-item":
return (
<li {...props.attributes} className="ml-4 list-disc">
{props.children}
</li>
- );
+ )
case "block-quote":
return (
<blockquote
@@ -291,88 +291,90 @@ export function TextEditor({
>
{props.children}
</blockquote>
- );
+ )
default:
return (
<p {...props.attributes} className="mb-2">
{props.children}
</p>
- );
+ )
}
- }, []);
+ }, [])
const renderLeaf = useCallback((props: RenderLeafProps) => {
- let { attributes, children, leaf } = props;
+ let { attributes, children, leaf } = props
if (leaf.bold) {
- children = <strong>{children}</strong>;
+ children = <strong>{children}</strong>
}
if (leaf.italic) {
- children = <em>{children}</em>;
+ children = <em>{children}</em>
}
if (leaf.code) {
children = (
- <code className="bg-foreground/10 px-1 rounded text-sm">{children}</code>
- );
+ <code className="bg-foreground/10 px-1 rounded text-sm">
+ {children}
+ </code>
+ )
}
- return <span {...attributes}>{children}</span>;
- }, []);
+ return <span {...attributes}>{children}</span>
+ }, [])
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
// Handle hotkeys for formatting
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event)) {
- event.preventDefault();
- const mark = HOTKEYS[hotkey];
+ event.preventDefault()
+ const mark = HOTKEYS[hotkey]
if (mark) {
- toggleMark(editor, mark);
+ toggleMark(editor, mark)
}
- return;
+ return
}
}
// Handle block formatting hotkeys
if (isHotkey("mod+shift+1", event)) {
- event.preventDefault();
- toggleBlock(editor, "heading", 1);
- return;
+ event.preventDefault()
+ toggleBlock(editor, "heading", 1)
+ return
}
if (isHotkey("mod+shift+2", event)) {
- event.preventDefault();
- toggleBlock(editor, "heading", 2);
- return;
+ event.preventDefault()
+ toggleBlock(editor, "heading", 2)
+ return
}
if (isHotkey("mod+shift+3", event)) {
- event.preventDefault();
- toggleBlock(editor, "heading", 3);
- return;
+ event.preventDefault()
+ toggleBlock(editor, "heading", 3)
+ return
}
if (isHotkey("mod+shift+8", event)) {
- event.preventDefault();
- toggleBlock(editor, "list-item");
- return;
+ event.preventDefault()
+ toggleBlock(editor, "list-item")
+ return
}
if (isHotkey("mod+shift+.", event)) {
- event.preventDefault();
- toggleBlock(editor, "block-quote");
- return;
+ event.preventDefault()
+ toggleBlock(editor, "block-quote")
+ return
}
},
[editor],
- );
+ )
const handleSlateChange = useCallback(
(newValue: Descendant[]) => {
- setEditorValue(newValue);
- const serializedValue = serialize(newValue);
- onChange?.(serializedValue);
+ setEditorValue(newValue)
+ const serializedValue = serialize(newValue)
+ onChange?.(serializedValue)
},
[onChange],
- );
+ )
// Memoized active states that update when selection changes
const activeStates = useMemo(
@@ -387,7 +389,7 @@ export function TextEditor({
blockQuote: isBlockActive(editor, "block-quote"),
}),
[editor, selection],
- );
+ )
const ToolbarButton = ({
icon: Icon,
@@ -395,10 +397,10 @@ export function TextEditor({
onMouseDown,
title,
}: {
- icon: React.ComponentType<{ className?: string }>;
- isActive: boolean;
- onMouseDown: (event: React.MouseEvent) => void;
- title: string;
+ icon: React.ComponentType<{ className?: string }>
+ isActive: boolean
+ onMouseDown: (event: React.MouseEvent) => void
+ title: string
}) => (
<Button
variant="ghost"
@@ -420,131 +422,136 @@ export function TextEditor({
)}
/>
</Button>
- );
+ )
return (
- <div className={cn("bg-foreground/5 border border-foreground/10 rounded-md", containerClassName)}>
+ <div
+ className={cn(
+ "bg-foreground/5 border border-foreground/10 rounded-md",
+ containerClassName,
+ )}
+ >
<div className={cn("flex flex-col", className)}>
- <div className="flex-1 min-h-48 overflow-y-auto">
- <Slate
- editor={editor}
- initialValue={editorValue}
- onValueChange={handleSlateChange}
- onSelectionChange={() => setSelection(editor.selection)}
- >
- <Editable
- renderElement={renderElement}
- renderLeaf={renderLeaf}
- placeholder={placeholder}
- renderPlaceholder={({ children, attributes }) => {
- return (
- <div {...attributes} className="mt-2">
- {children}
- </div>
- );
- }}
- onKeyDown={handleKeyDown}
- onBlur={onBlur}
- readOnly={disabled}
- className={cn(
- "outline-none w-full h-full placeholder:text-foreground/50",
- disabled && "opacity-50 cursor-not-allowed",
- )}
- style={{
- minHeight: "23rem",
- maxHeight: "23rem",
- padding: "12px",
- overflowX: "hidden",
- }}
- />
- </Slate>
- </div>
-
- {/* Toolbar */}
- <div className="p-1 flex items-center gap-2 bg-foreground/5 backdrop-blur-sm rounded-b-md">
- <div className="flex items-center gap-1">
- {/* Text formatting */}
- <ToolbarButton
- icon={Bold}
- isActive={activeStates.bold}
- onMouseDown={(event) => {
- event.preventDefault();
- toggleMark(editor, "bold");
- }}
- title="Bold (Ctrl/Cmd+B)"
- />
- <ToolbarButton
- icon={Italic}
- isActive={activeStates.italic}
- onMouseDown={(event) => {
- event.preventDefault();
- toggleMark(editor, "italic");
- }}
- title="Italic (Ctrl/Cmd+I)"
- />
- <ToolbarButton
- icon={Code}
- isActive={activeStates.code}
- onMouseDown={(event) => {
- event.preventDefault();
- toggleMark(editor, "code");
- }}
- title="Code (Ctrl/Cmd+`)"
- />
+ <div className="flex-1 min-h-48 overflow-y-auto">
+ <Slate
+ editor={editor}
+ initialValue={editorValue}
+ onValueChange={handleSlateChange}
+ onSelectionChange={() => setSelection(editor.selection)}
+ >
+ <Editable
+ renderElement={renderElement}
+ renderLeaf={renderLeaf}
+ placeholder={placeholder}
+ renderPlaceholder={({ children, attributes }) => {
+ return (
+ <div {...attributes} className="mt-2">
+ {children}
+ </div>
+ )
+ }}
+ onKeyDown={handleKeyDown}
+ onBlur={onBlur}
+ readOnly={disabled}
+ className={cn(
+ "outline-none w-full h-full placeholder:text-foreground/50",
+ disabled && "opacity-50 cursor-not-allowed",
+ )}
+ style={{
+ minHeight: "23rem",
+ maxHeight: "23rem",
+ padding: "12px",
+ overflowX: "hidden",
+ }}
+ />
+ </Slate>
</div>
- <div className="w-px h-6 bg-foreground/30 mx-2" />
-
- <div className="flex items-center gap-1">
- {/* Block formatting */}
- <ToolbarButton
- icon={Heading1}
- isActive={activeStates.heading1}
- onMouseDown={(event) => {
- event.preventDefault();
- toggleBlock(editor, "heading", 1);
- }}
- title="Heading 1 (Ctrl/Cmd+Shift+1)"
- />
- <ToolbarButton
- icon={Heading2}
- isActive={activeStates.heading2}
- onMouseDown={(event) => {
- event.preventDefault();
- toggleBlock(editor, "heading", 2);
- }}
- title="Heading 2 (Ctrl/Cmd+Shift+2)"
- />
- <ToolbarButton
- icon={Heading3}
- isActive={activeStates.heading3}
- onMouseDown={(event) => {
- event.preventDefault();
- toggleBlock(editor, "heading", 3);
- }}
- title="Heading 3"
- />
- <ToolbarButton
- icon={List}
- isActive={activeStates.listItem}
- onMouseDown={(event) => {
- event.preventDefault();
- toggleBlock(editor, "list-item");
- }}
- title="Bullet List"
- />
- <ToolbarButton
- icon={Quote}
- isActive={activeStates.blockQuote}
- onMouseDown={(event) => {
- event.preventDefault();
- toggleBlock(editor, "block-quote");
- }}
- title="Quote"
- />
+ {/* Toolbar */}
+ <div className="p-1 flex items-center gap-2 bg-foreground/5 backdrop-blur-sm rounded-b-md">
+ <div className="flex items-center gap-1">
+ {/* Text formatting */}
+ <ToolbarButton
+ icon={Bold}
+ isActive={activeStates.bold}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ toggleMark(editor, "bold")
+ }}
+ title="Bold (Ctrl/Cmd+B)"
+ />
+ <ToolbarButton
+ icon={Italic}
+ isActive={activeStates.italic}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ toggleMark(editor, "italic")
+ }}
+ title="Italic (Ctrl/Cmd+I)"
+ />
+ <ToolbarButton
+ icon={Code}
+ isActive={activeStates.code}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ toggleMark(editor, "code")
+ }}
+ title="Code (Ctrl/Cmd+`)"
+ />
+ </div>
+
+ <div className="w-px h-6 bg-foreground/30 mx-2" />
+
+ <div className="flex items-center gap-1">
+ {/* Block formatting */}
+ <ToolbarButton
+ icon={Heading1}
+ isActive={activeStates.heading1}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ toggleBlock(editor, "heading", 1)
+ }}
+ title="Heading 1 (Ctrl/Cmd+Shift+1)"
+ />
+ <ToolbarButton
+ icon={Heading2}
+ isActive={activeStates.heading2}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ toggleBlock(editor, "heading", 2)
+ }}
+ title="Heading 2 (Ctrl/Cmd+Shift+2)"
+ />
+ <ToolbarButton
+ icon={Heading3}
+ isActive={activeStates.heading3}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ toggleBlock(editor, "heading", 3)
+ }}
+ title="Heading 3"
+ />
+ <ToolbarButton
+ icon={List}
+ isActive={activeStates.listItem}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ toggleBlock(editor, "list-item")
+ }}
+ title="Bullet List"
+ />
+ <ToolbarButton
+ icon={Quote}
+ isActive={activeStates.blockQuote}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ toggleBlock(editor, "block-quote")
+ }}
+ title="Quote"
+ />
+ </div>
</div>
</div>
- </div>
</div>
- );
+ )
}
diff --git a/apps/web/components/views/chat/chat-messages.tsx b/apps/web/components/views/chat/chat-messages.tsx
index 3e55dc23..304db7aa 100644
--- a/apps/web/components/views/chat/chat-messages.tsx
+++ b/apps/web/components/views/chat/chat-messages.tsx
@@ -12,7 +12,7 @@ import {
Copy,
RotateCcw,
X,
- Square
+ Square,
} from "lucide-react"
import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner"
diff --git a/apps/web/components/views/connections-tab-content.tsx b/apps/web/components/views/connections-tab-content.tsx
index 9a63d6fd..48ab0452 100644
--- a/apps/web/components/views/connections-tab-content.tsx
+++ b/apps/web/components/views/connections-tab-content.tsx
@@ -111,7 +111,6 @@ export function ConnectionsTabContent() {
// Add connection mutation
const addConnectionMutation = useMutation({
mutationFn: async (provider: ConnectorProvider) => {
-
// Check if user can add connections
if (!canAddConnection && !isProUser) {
throw new Error(
diff --git a/apps/web/components/views/projects.tsx b/apps/web/components/views/projects.tsx
index 45e51f6c..fd3aef0f 100644
--- a/apps/web/components/views/projects.tsx
+++ b/apps/web/components/views/projects.tsx
@@ -1,7 +1,7 @@
-"use client";
+"use client"
-import { $fetch } from "@lib/api";
-import { Button } from "@repo/ui/components/button";
+import { $fetch } from "@lib/api"
+import { Button } from "@repo/ui/components/button"
import {
Dialog,
@@ -10,57 +10,57 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@repo/ui/components/dialog";
+} from "@repo/ui/components/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
-} from "@repo/ui/components/dropdown-menu";
-import { Input } from "@repo/ui/components/input";
-import { Label } from "@repo/ui/components/label";
+} from "@repo/ui/components/dropdown-menu"
+import { Input } from "@repo/ui/components/input"
+import { Label } from "@repo/ui/components/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from "@repo/ui/components/select";
-import { Skeleton } from "@repo/ui/components/skeleton";
+} from "@repo/ui/components/select"
+import { Skeleton } from "@repo/ui/components/skeleton"
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
-import { FolderIcon, Loader2, MoreVertical, Plus, Trash2 } from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
+import { FolderIcon, Loader2, MoreVertical, Plus, Trash2 } from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
-import { useState } from "react";
-import { toast } from "sonner";
-import { useProject } from "@/stores";
+import { useState } from "react"
+import { toast } from "sonner"
+import { useProject } from "@/stores"
// Projects View Component
export function ProjectsView() {
- const queryClient = useQueryClient();
- const { selectedProject, setSelectedProject } = useProject();
- const [showCreateDialog, setShowCreateDialog] = useState(false);
- const [projectName, setProjectName] = useState("");
+ const queryClient = useQueryClient()
+ const { selectedProject, setSelectedProject } = useProject()
+ const [showCreateDialog, setShowCreateDialog] = useState(false)
+ const [projectName, setProjectName] = useState("")
const [deleteDialog, setDeleteDialog] = useState<{
- open: boolean;
- project: null | { id: string; name: string; containerTag: string };
- action: "move" | "delete";
- targetProjectId: string;
+ open: boolean
+ project: null | { id: string; name: string; containerTag: string }
+ action: "move" | "delete"
+ targetProjectId: string
}>({
open: false,
project: null,
action: "move",
targetProjectId: "",
- });
+ })
const [expDialog, setExpDialog] = useState<{
- open: boolean;
- projectId: string;
+ open: boolean
+ projectId: string
}>({
open: false,
projectId: "",
- });
+ })
// Fetch projects
const {
@@ -70,42 +70,42 @@ export function ProjectsView() {
} = useQuery({
queryKey: ["projects"],
queryFn: async () => {
- const response = await $fetch("@get/projects");
+ const response = await $fetch("@get/projects")
if (response.error) {
- throw new Error(response.error?.message || "Failed to load projects");
+ throw new Error(response.error?.message || "Failed to load projects")
}
- return response.data?.projects || [];
+ return response.data?.projects || []
},
staleTime: 30 * 1000,
- });
+ })
// Create project mutation
const createProjectMutation = useMutation({
mutationFn: async (name: string) => {
const response = await $fetch("@post/projects", {
body: { name },
- });
+ })
if (response.error) {
- throw new Error(response.error?.message || "Failed to create project");
+ throw new Error(response.error?.message || "Failed to create project")
}
- return response.data;
+ return response.data
},
onSuccess: () => {
- toast.success("Project created successfully!");
- setShowCreateDialog(false);
- setProjectName("");
- queryClient.invalidateQueries({ queryKey: ["projects"] });
+ toast.success("Project created successfully!")
+ setShowCreateDialog(false)
+ setProjectName("")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
},
onError: (error) => {
toast.error("Failed to create project", {
description: error instanceof Error ? error.message : "Unknown error",
- });
+ })
},
- });
+ })
// Delete project mutation
const deleteProjectMutation = useMutation({
@@ -114,72 +114,72 @@ export function ProjectsView() {
action,
targetProjectId,
}: {
- projectId: string;
- action: "move" | "delete";
- targetProjectId?: string;
+ projectId: string
+ action: "move" | "delete"
+ targetProjectId?: string
}) => {
const response = await $fetch(`@delete/projects/${projectId}`, {
body: { action, targetProjectId },
- });
+ })
if (response.error) {
- throw new Error(response.error?.message || "Failed to delete project");
+ throw new Error(response.error?.message || "Failed to delete project")
}
- return response.data;
+ return response.data
},
onSuccess: () => {
- toast.success("Project deleted successfully");
+ toast.success("Project deleted successfully")
setDeleteDialog({
open: false,
project: null,
action: "move",
targetProjectId: "",
- });
- queryClient.invalidateQueries({ queryKey: ["projects"] });
+ })
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
// If we deleted the selected project, switch to default
if (deleteDialog.project?.containerTag === selectedProject) {
- setSelectedProject("sm_project_default");
+ setSelectedProject("sm_project_default")
}
},
onError: (error) => {
toast.error("Failed to delete project", {
description: error instanceof Error ? error.message : "Unknown error",
- });
+ })
},
- });
+ })
// Enable experimental mode mutation
const enableExperimentalMutation = useMutation({
mutationFn: async (projectId: string) => {
const response = await $fetch(
`@post/projects/${projectId}/enable-experimental`,
- );
+ )
if (response.error) {
throw new Error(
response.error?.message || "Failed to enable experimental mode",
- );
+ )
}
- return response.data;
+ return response.data
},
onSuccess: () => {
- toast.success("Experimental mode enabled for project");
- queryClient.invalidateQueries({ queryKey: ["projects"] });
- setExpDialog({ open: false, projectId: "" });
+ toast.success("Experimental mode enabled for project")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+ setExpDialog({ open: false, projectId: "" })
},
onError: (error) => {
toast.error("Failed to enable experimental mode", {
description: error instanceof Error ? error.message : "Unknown error",
- });
+ })
},
- });
+ })
// Handle project selection
const handleProjectSelect = (containerTag: string) => {
- setSelectedProject(containerTag);
- toast.success("Project switched successfully");
- };
+ setSelectedProject(containerTag)
+ toast.success("Project switched successfully")
+ }
return (
<div className="space-y-4">
@@ -344,11 +344,11 @@ export function ProjectsView() {
<DropdownMenuItem
className="text-blue-400 hover:text-blue-300 cursor-pointer"
onClick={(e) => {
- e.stopPropagation();
+ e.stopPropagation()
setExpDialog({
open: true,
projectId: project.id,
- });
+ })
}}
>
<div className="h-4 w-4 mr-2 rounded border border-blue-400" />
@@ -367,7 +367,7 @@ export function ProjectsView() {
<DropdownMenuItem
className="text-red-400 hover:text-red-300 cursor-pointer"
onClick={(e) => {
- e.stopPropagation();
+ e.stopPropagation()
setDeleteDialog({
open: true,
project: {
@@ -377,7 +377,7 @@ export function ProjectsView() {
},
action: "move",
targetProjectId: "",
- });
+ })
}}
>
<Trash2 className="h-4 w-4 mr-2" />
@@ -436,8 +436,8 @@ export function ProjectsView() {
<Button
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
onClick={() => {
- setShowCreateDialog(false);
- setProjectName("");
+ setShowCreateDialog(false)
+ setProjectName("")
}}
type="button"
variant="outline"
@@ -642,7 +642,7 @@ export function ProjectsView() {
deleteDialog.action === "move"
? deleteDialog.targetProjectId
: undefined,
- });
+ })
}
}}
type="button"
@@ -745,5 +745,5 @@ export function ProjectsView() {
)}
</AnimatePresence>
</div>
- );
+ )
}
diff --git a/apps/web/hooks/use-document-mutations.ts b/apps/web/hooks/use-document-mutations.ts
index bafa0e58..ea3c3783 100644
--- a/apps/web/hooks/use-document-mutations.ts
+++ b/apps/web/hooks/use-document-mutations.ts
@@ -5,6 +5,7 @@ import { toast } from "sonner"
import { $fetch } from "@lib/api"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
+import { analytics } from "@/lib/analytics"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
@@ -29,7 +30,9 @@ interface UseDocumentMutationsOptions {
onClose?: () => void
}
-export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions = {}) {
+export function useDocumentMutations({
+ onClose,
+}: UseDocumentMutationsOptions = {}) {
const queryClient = useQueryClient()
const noteMutation = useMutation({
@@ -111,6 +114,10 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
})
},
onSuccess: (_data, variables) => {
+ analytics.documentAdded({
+ type: "note",
+ project_id: variables.project,
+ })
toast.success("Note added successfully!", {
description: "Your note is being processed",
})
@@ -194,6 +201,10 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
})
},
onSuccess: (_data, variables) => {
+ analytics.documentAdded({
+ type: "link",
+ project_id: variables.project,
+ })
toast.success("Link added successfully!", {
description: "Your link is being processed",
})
@@ -311,6 +322,10 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
})
},
onSuccess: (_data, variables) => {
+ analytics.documentAdded({
+ type: "file",
+ project_id: variables.project,
+ })
toast.success("File uploaded successfully!", {
description: "Your file is being processed",
})
@@ -392,7 +407,8 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
return {
...page,
documents: page.documents.filter(
- (doc) => doc.id !== documentId && doc.customId !== documentId,
+ (doc) =>
+ doc.id !== documentId && doc.customId !== documentId,
),
pagination: page.pagination
? {
@@ -412,11 +428,9 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions =
const queryData = old as DocumentsQueryData
return {
...queryData,
- documents: queryData.documents.filter(
- (doc: DocumentWithId) => {
- return doc.id !== documentId && doc.customId !== documentId
- },
- ),
+ documents: queryData.documents.filter((doc: DocumentWithId) => {
+ return doc.id !== documentId && doc.customId !== documentId
+ }),
totalCount: Math.max(0, (queryData.totalCount ?? 0) - 1),
}
}
diff --git a/apps/web/hooks/use-project-name.ts b/apps/web/hooks/use-project-name.ts
index 2ee1313f..ef094ff6 100644
--- a/apps/web/hooks/use-project-name.ts
+++ b/apps/web/hooks/use-project-name.ts
@@ -1,8 +1,8 @@
-"use client";
+"use client"
-import { useQueryClient } from "@tanstack/react-query";
-import { useMemo } from "react";
-import { useProject } from "@/stores";
+import { useQueryClient } from "@tanstack/react-query"
+import { useMemo } from "react"
+import { useProject } from "@/stores"
/**
* Returns the display name of the currently selected project.
@@ -10,17 +10,17 @@ import { useProject } from "@/stores";
* hasnโ€™t been fetched yet.
*/
export function useProjectName() {
- const { selectedProject } = useProject();
- const queryClient = useQueryClient();
+ const { selectedProject } = useProject()
+ const queryClient = useQueryClient()
// This query is populated by ProjectsView โ€“ we just read from the cache.
const projects = queryClient.getQueryData(["projects"]) as
| Array<{ name: string; containerTag: string }>
- | undefined;
+ | undefined
return useMemo(() => {
- if (selectedProject === "sm_project_default") return "Default Project";
- const found = projects?.find((p) => p.containerTag === selectedProject);
- return found?.name ?? selectedProject;
- }, [projects, selectedProject]);
+ if (selectedProject === "sm_project_default") return "Default Project"
+ const found = projects?.find((p) => p.containerTag === selectedProject)
+ return found?.name ?? selectedProject
+ }, [projects, selectedProject])
}
diff --git a/apps/web/hooks/use-resize-observer.ts b/apps/web/hooks/use-resize-observer.ts
index b309347d..ae467b44 100644
--- a/apps/web/hooks/use-resize-observer.ts
+++ b/apps/web/hooks/use-resize-observer.ts
@@ -1,23 +1,23 @@
-import { useEffect, useState } from "react";
+import { useEffect, useState } from "react"
export default function useResizeObserver<T extends HTMLElement>(
ref: React.RefObject<T | null>,
) {
- const [size, setSize] = useState({ width: 0, height: 0 });
+ const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
- if (!ref.current) return;
+ if (!ref.current) return
const observer = new ResizeObserver(([entry]) => {
setSize({
width: entry?.contentRect.width ?? 0,
height: entry?.contentRect.height ?? 0,
- });
- });
+ })
+ })
- observer.observe(ref.current);
- return () => observer.disconnect();
- }, [ref]);
+ observer.observe(ref.current)
+ return () => observer.disconnect()
+ }, [ref])
- return size;
+ return size
}
diff --git a/apps/web/instrumentation-client.ts b/apps/web/instrumentation-client.ts
index 2c9c9e2d..f6730e9c 100644
--- a/apps/web/instrumentation-client.ts
+++ b/apps/web/instrumentation-client.ts
@@ -2,29 +2,29 @@
// The added config here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
-import * as Sentry from '@sentry/nextjs';
+import * as Sentry from "@sentry/nextjs"
Sentry.init({
- dsn: 'https://2451ebfd1a7490f05fa7776482df81b6@o4508385422802944.ingest.us.sentry.io/4509872269819904',
+ dsn: "https://2451ebfd1a7490f05fa7776482df81b6@o4508385422802944.ingest.us.sentry.io/4509872269819904",
- // Add optional integrations for additional features
- integrations: [Sentry.replayIntegration()],
+ // Add optional integrations for additional features
+ integrations: [Sentry.replayIntegration()],
- // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
- tracesSampleRate: 1,
- // Enable logs to be sent to Sentry
- enableLogs: true,
+ // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
+ tracesSampleRate: 1,
+ // Enable logs to be sent to Sentry
+ enableLogs: true,
- // Define how likely Replay events are sampled.
- // This sets the sample rate to be 10%. You may want this to be 100% while
- // in development and sample at a lower rate in production
- replaysSessionSampleRate: 0.1,
+ // Define how likely Replay events are sampled.
+ // This sets the sample rate to be 10%. You may want this to be 100% while
+ // in development and sample at a lower rate in production
+ replaysSessionSampleRate: 0.1,
- // Define how likely Replay events are sampled when an error occurs.
- replaysOnErrorSampleRate: 1.0,
+ // Define how likely Replay events are sampled when an error occurs.
+ replaysOnErrorSampleRate: 1.0,
- // Setting this option to true will print useful information to the console while you're setting up Sentry.
- debug: false,
-});
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+})
-export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts
index f73c03b5..9bc3b7f5 100644
--- a/apps/web/lib/analytics.ts
+++ b/apps/web/lib/analytics.ts
@@ -1,10 +1,10 @@
import posthog from "posthog-js"
// Helper function to safely capture events
-const safeCapture = (eventName: string, properties?: Record<string, any>) => {
- if (posthog.__loaded) {
- posthog.capture(eventName, properties)
- }
+const safeCapture = (eventName: string, properties?: Record<string, unknown>) => {
+ if (posthog.__loaded) {
+ posthog.capture(eventName, properties)
+ }
}
export const analytics = {
@@ -47,4 +47,16 @@ export const analytics = {
mcpInstallCmdCopied: () => safeCapture("mcp_install_cmd_copied"),
extensionInstallClicked: () => safeCapture("extension_install_clicked"),
+
+ // nova analytics
+ documentAdded: (props: {
+ type: "note" | "link" | "file" | "connect"
+ project_id?: string
+ }) => safeCapture("document_added", props),
+
+ newChatCreated: () => safeCapture("new_chat_created"),
+
+ mcpModalOpened: () => safeCapture("mcp_modal_opened"),
+
+ addDocumentModalOpened: () => safeCapture("add_document_modal_opened"),
}
diff --git a/apps/web/lib/mobile-panel-context.tsx b/apps/web/lib/mobile-panel-context.tsx
index 5dc4a01c..3b0b1838 100644
--- a/apps/web/lib/mobile-panel-context.tsx
+++ b/apps/web/lib/mobile-panel-context.tsx
@@ -1,32 +1,32 @@
-"use client";
+"use client"
-import { createContext, type ReactNode, useContext, useState } from "react";
+import { createContext, type ReactNode, useContext, useState } from "react"
-type ActivePanel = "menu" | "chat" | null;
+type ActivePanel = "menu" | "chat" | null
interface MobilePanelContextType {
- activePanel: ActivePanel;
- setActivePanel: (panel: ActivePanel) => void;
+ activePanel: ActivePanel
+ setActivePanel: (panel: ActivePanel) => void
}
const MobilePanelContext = createContext<MobilePanelContextType | undefined>(
undefined,
-);
+)
export function MobilePanelProvider({ children }: { children: ReactNode }) {
- const [activePanel, setActivePanel] = useState<ActivePanel>(null);
+ const [activePanel, setActivePanel] = useState<ActivePanel>(null)
return (
<MobilePanelContext.Provider value={{ activePanel, setActivePanel }}>
{children}
</MobilePanelContext.Provider>
- );
+ )
}
export function useMobilePanel() {
- const context = useContext(MobilePanelContext);
+ const context = useContext(MobilePanelContext)
if (!context) {
- throw new Error("useMobilePanel must be used within a MobilePanelProvider");
+ throw new Error("useMobilePanel must be used within a MobilePanelProvider")
}
- return context;
+ return context
}
diff --git a/apps/web/lib/view-mode-context.tsx b/apps/web/lib/view-mode-context.tsx
index 61627042..87c11da1 100644
--- a/apps/web/lib/view-mode-context.tsx
+++ b/apps/web/lib/view-mode-context.tsx
@@ -1,4 +1,4 @@
-"use client";
+"use client"
import {
createContext,
@@ -6,73 +6,73 @@ import {
useContext,
useEffect,
useState,
-} from "react";
-import { analytics } from "@/lib/analytics";
+} from "react"
+import { analytics } from "@/lib/analytics"
-type ViewMode = "graph" | "list";
+type ViewMode = "graph" | "list"
interface ViewModeContextType {
- viewMode: ViewMode;
- setViewMode: (mode: ViewMode) => void;
- isInitialized: boolean;
+ viewMode: ViewMode
+ setViewMode: (mode: ViewMode) => void
+ isInitialized: boolean
}
const ViewModeContext = createContext<ViewModeContextType | undefined>(
undefined,
-);
+)
// Cookie utility functions
const setCookie = (name: string, value: string, days = 365) => {
- if (typeof document === "undefined") return;
- const expires = new Date();
- expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
- document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
-};
+ if (typeof document === "undefined") return
+ const expires = new Date()
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000)
+ document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`
+}
const getCookie = (name: string): string | null => {
- if (typeof document === "undefined") return null;
- const nameEQ = `${name}=`;
- const ca = document.cookie.split(";");
+ if (typeof document === "undefined") return null
+ const nameEQ = `${name}=`
+ const ca = document.cookie.split(";")
for (let i = 0; i < ca.length; i++) {
- let c = ca[i];
- if (!c) continue;
- while (c.charAt(0) === " ") c = c.substring(1, c.length);
- if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
+ let c = ca[i]
+ if (!c) continue
+ while (c.charAt(0) === " ") c = c.substring(1, c.length)
+ if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length)
}
- return null;
-};
+ return null
+}
const isMobileDevice = () => {
- if (typeof window === "undefined") return false;
- return window.innerWidth < 768;
-};
+ if (typeof window === "undefined") return false
+ return window.innerWidth < 768
+}
export function ViewModeProvider({ children }: { children: ReactNode }) {
// Start with a default that works for SSR
- const [viewMode, setViewModeState] = useState<ViewMode>("graph");
- const [isInitialized, setIsInitialized] = useState(false);
+ const [viewMode, setViewModeState] = useState<ViewMode>("graph")
+ const [isInitialized, setIsInitialized] = useState(false)
// Load preferences on the client side
useEffect(() => {
if (!isInitialized) {
// Check for saved preference first
- const savedMode = getCookie("memoryViewMode");
+ const savedMode = getCookie("memoryViewMode")
if (savedMode === "list" || savedMode === "graph") {
- setViewModeState(savedMode);
+ setViewModeState(savedMode)
} else {
// If no saved preference, default to list on mobile, graph on desktop
- setViewModeState(isMobileDevice() ? "list" : "graph");
+ setViewModeState(isMobileDevice() ? "list" : "graph")
}
- setIsInitialized(true);
+ setIsInitialized(true)
}
- }, [isInitialized]);
+ }, [isInitialized])
// Save to cookie whenever view mode changes
const handleSetViewMode = (mode: ViewMode) => {
- analytics.viewModeChanged(mode);
- setViewModeState(mode);
- setCookie("memoryViewMode", mode);
- };
+ analytics.viewModeChanged(mode)
+ setViewModeState(mode)
+ setCookie("memoryViewMode", mode)
+ }
return (
<ViewModeContext.Provider
@@ -84,13 +84,13 @@ export function ViewModeProvider({ children }: { children: ReactNode }) {
>
{children}
</ViewModeContext.Provider>
- );
+ )
}
export function useViewMode() {
- const context = useContext(ViewModeContext);
+ const context = useContext(ViewModeContext)
if (!context) {
- throw new Error("useViewMode must be used within a ViewModeProvider");
+ throw new Error("useViewMode must be used within a ViewModeProvider")
}
- return context;
+ return context
}
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index a184b9b3..cf53d37e 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -4,7 +4,7 @@ import { NextResponse } from "next/server"
export default async function proxy(request: Request) {
console.debug("[PROXY] === PROXY START ===")
const url = new URL(request.url)
-
+
console.debug("[PROXY] Path:", url.pathname)
console.debug("[PROXY] Method:", request.method)
diff --git a/apps/web/open-next.config.ts b/apps/web/open-next.config.ts
index 4f3ea77b..9a3f4ccc 100644
--- a/apps/web/open-next.config.ts
+++ b/apps/web/open-next.config.ts
@@ -1,6 +1,6 @@
-import { defineCloudflareConfig } from "@opennextjs/cloudflare";
-import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
+import { defineCloudflareConfig } from "@opennextjs/cloudflare"
+import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
-});
+})
diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs
index f50127cd..78452aad 100644
--- a/apps/web/postcss.config.mjs
+++ b/apps/web/postcss.config.mjs
@@ -1,5 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
-};
+}
-export default config;
+export default config
diff --git a/apps/web/stores/chat.ts b/apps/web/stores/chat.ts
index 24f4084b..7df867b0 100644
--- a/apps/web/stores/chat.ts
+++ b/apps/web/stores/chat.ts
@@ -7,7 +7,10 @@ import { indexedDBStorage } from "./indexeddb-storage"
/**
* Deep equality check for UIMessage arrays to prevent unnecessary state updates
*/
-export function areUIMessageArraysEqual(a: UIMessage[], b: UIMessage[]): boolean {
+export function areUIMessageArraysEqual(
+ a: UIMessage[],
+ b: UIMessage[],
+): boolean {
if (a === b) return true
if (a.length !== b.length) return false
diff --git a/apps/web/stores/highlights.ts b/apps/web/stores/highlights.ts
index d7937db1..de04e105 100644
--- a/apps/web/stores/highlights.ts
+++ b/apps/web/stores/highlights.ts
@@ -1,10 +1,10 @@
-import { create } from "zustand";
+import { create } from "zustand"
interface GraphHighlightsState {
- documentIds: string[];
- lastUpdated: number;
- setDocumentIds: (ids: string[]) => void;
- clear: () => void;
+ documentIds: string[]
+ lastUpdated: number
+ setDocumentIds: (ids: string[]) => void
+ clear: () => void
}
export const useGraphHighlightsStore = create<GraphHighlightsState>()(
@@ -12,24 +12,24 @@ export const useGraphHighlightsStore = create<GraphHighlightsState>()(
documentIds: [],
lastUpdated: 0,
setDocumentIds: (ids) => {
- const next = Array.from(new Set(ids));
- const prev = get().documentIds;
+ const next = Array.from(new Set(ids))
+ const prev = get().documentIds
if (
prev.length === next.length &&
prev.every((id) => next.includes(id))
) {
- return;
+ return
}
- set({ documentIds: next, lastUpdated: Date.now() });
+ set({ documentIds: next, lastUpdated: Date.now() })
},
clear: () => set({ documentIds: [], lastUpdated: Date.now() }),
}),
-);
+)
export function useGraphHighlights() {
- const documentIds = useGraphHighlightsStore((s) => s.documentIds);
- const lastUpdated = useGraphHighlightsStore((s) => s.lastUpdated);
- const setDocumentIds = useGraphHighlightsStore((s) => s.setDocumentIds);
- const clear = useGraphHighlightsStore((s) => s.clear);
- return { documentIds, lastUpdated, setDocumentIds, clear };
+ const documentIds = useGraphHighlightsStore((s) => s.documentIds)
+ const lastUpdated = useGraphHighlightsStore((s) => s.lastUpdated)
+ const setDocumentIds = useGraphHighlightsStore((s) => s.setDocumentIds)
+ const clear = useGraphHighlightsStore((s) => s.clear)
+ return { documentIds, lastUpdated, setDocumentIds, clear }
}
diff --git a/apps/web/stores/indexeddb-storage.ts b/apps/web/stores/indexeddb-storage.ts
index c2a1db91..d1e3ecbf 100644
--- a/apps/web/stores/indexeddb-storage.ts
+++ b/apps/web/stores/indexeddb-storage.ts
@@ -1,24 +1,24 @@
-import { get, set, del } from 'idb-keyval';
+import { get, set, del } from "idb-keyval"
export const indexedDBStorage = {
- getItem: async (name: string) => {
- let value = await get(name);
- if (value !== undefined) {
- return value;
- }
- // Migrate from localStorage if exists
- value = localStorage.getItem(name);
- if (value !== null) {
- await set(name, value);
- localStorage.removeItem(name);
- return value;
- }
- return null;
- },
- setItem: async (name: string, value: string) => {
- await set(name, value);
- },
- removeItem: async (name: string) => {
- await del(name);
- },
-};
+ getItem: async (name: string) => {
+ let value = await get(name)
+ if (value !== undefined) {
+ return value
+ }
+ // Migrate from localStorage if exists
+ value = localStorage.getItem(name)
+ if (value !== null) {
+ await set(name, value)
+ localStorage.removeItem(name)
+ return value
+ }
+ return null
+ },
+ setItem: async (name: string, value: string) => {
+ await set(name, value)
+ },
+ removeItem: async (name: string) => {
+ await del(name)
+ },
+}
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 67626bbc..bdd64662 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -1,20 +1,20 @@
{
- "compilerOptions": {
- "incremental": true,
- "jsx": "preserve",
- "paths": {
- "@/*": ["./*"],
- "@ui/*": ["../../packages/ui/*"],
- "@lib/*": ["../../packages/lib/*"],
- "@hooks/*": ["../../packages/hooks/*"]
- },
- "plugins": [
- {
- "name": "next"
- }
- ]
- },
- "exclude": ["node_modules"],
- "extends": "@total-typescript/tsconfig/bundler/dom/app",
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]
+ "compilerOptions": {
+ "incremental": true,
+ "jsx": "preserve",
+ "paths": {
+ "@/*": ["./*"],
+ "@ui/*": ["../../packages/ui/*"],
+ "@lib/*": ["../../packages/lib/*"],
+ "@hooks/*": ["../../packages/hooks/*"]
+ },
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "exclude": ["node_modules"],
+ "extends": "@total-typescript/tsconfig/bundler/dom/app",
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]
}
diff --git a/apps/web/wrangler.jsonc b/apps/web/wrangler.jsonc
index dadfdf63..674f6270 100644
--- a/apps/web/wrangler.jsonc
+++ b/apps/web/wrangler.jsonc
@@ -1,31 +1,31 @@
{
- "$schema": "node_modules/wrangler/config-schema.json",
- "main": ".open-next/worker.js",
- "name": "supermemory-app",
- "compatibility_date": "2024-12-30",
- "compatibility_flags": [
- // Enable Node.js API
- // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag
- "nodejs_compat",
- // Allow to fetch URLs in your app
- // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public
- "global_fetch_strictly_public",
- ],
- "assets": {
- "directory": ".open-next/assets",
- "binding": "ASSETS",
- },
- "services": [
- {
- "binding": "WORKER_SELF_REFERENCE",
- // The service should match the "name" of your worker
- "service": "supermemory-app",
- },
- ],
- "r2_buckets": [
- {
- "binding": "NEXT_INC_CACHE_R2_BUCKET",
- "bucket_name": "supermemory-console-cache",
- },
- ],
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "main": ".open-next/worker.js",
+ "name": "supermemory-app",
+ "compatibility_date": "2024-12-30",
+ "compatibility_flags": [
+ // Enable Node.js API
+ // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag
+ "nodejs_compat",
+ // Allow to fetch URLs in your app
+ // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public
+ "global_fetch_strictly_public"
+ ],
+ "assets": {
+ "directory": ".open-next/assets",
+ "binding": "ASSETS"
+ },
+ "services": [
+ {
+ "binding": "WORKER_SELF_REFERENCE",
+ // The service should match the "name" of your worker
+ "service": "supermemory-app"
+ }
+ ],
+ "r2_buckets": [
+ {
+ "binding": "NEXT_INC_CACHE_R2_BUCKET",
+ "bucket_name": "supermemory-console-cache"
+ }
+ ]
}
diff --git a/packages/docs-test/run.ts b/packages/docs-test/run.ts
index a98d4a1b..be9a588c 100644
--- a/packages/docs-test/run.ts
+++ b/packages/docs-test/run.ts
@@ -8,114 +8,123 @@ const filter = args[0] // e.g., "typescript", "python", "integrations", or speci
const TESTS_DIR = path.join(import.meta.dir, "tests")
interface TestFile {
- name: string
- path: string
- type: "ts" | "py"
+ name: string
+ path: string
+ type: "ts" | "py"
}
function getTests(): TestFile[] {
- const tests: TestFile[] = []
-
- // TypeScript tests
- const tsTests = ["quickstart", "sdk", "search", "user-profiles"]
- for (const t of tsTests) {
- tests.push({
- name: `typescript/${t}`,
- path: path.join(TESTS_DIR, "typescript", `${t}.ts`),
- type: "ts",
- })
- }
-
- // Python tests
- const pyTests = ["quickstart", "sdk", "search", "user_profiles"]
- for (const t of pyTests) {
- tests.push({
- name: `python/${t}`,
- path: path.join(TESTS_DIR, "python", `${t}.py`),
- type: "py",
- })
- }
-
- // Integration tests
- const intTests = [
- { name: "ai-sdk", type: "ts" as const },
- { name: "openai-sdk", type: "ts" as const },
- { name: "openai-sdk", type: "py" as const },
- { name: "claude-memory", type: "ts" as const },
- ]
- for (const t of intTests) {
- tests.push({
- name: `integrations/${t.name}`,
- path: path.join(TESTS_DIR, "integrations", `${t.name}.${t.type === "ts" ? "ts" : "py"}`),
- type: t.type,
- })
- }
-
- return tests
+ const tests: TestFile[] = []
+
+ // TypeScript tests
+ const tsTests = ["quickstart", "sdk", "search", "user-profiles"]
+ for (const t of tsTests) {
+ tests.push({
+ name: `typescript/${t}`,
+ path: path.join(TESTS_DIR, "typescript", `${t}.ts`),
+ type: "ts",
+ })
+ }
+
+ // Python tests
+ const pyTests = ["quickstart", "sdk", "search", "user_profiles"]
+ for (const t of pyTests) {
+ tests.push({
+ name: `python/${t}`,
+ path: path.join(TESTS_DIR, "python", `${t}.py`),
+ type: "py",
+ })
+ }
+
+ // Integration tests
+ const intTests = [
+ { name: "ai-sdk", type: "ts" as const },
+ { name: "openai-sdk", type: "ts" as const },
+ { name: "openai-sdk", type: "py" as const },
+ { name: "claude-memory", type: "ts" as const },
+ ]
+ for (const t of intTests) {
+ tests.push({
+ name: `integrations/${t.name}`,
+ path: path.join(
+ TESTS_DIR,
+ "integrations",
+ `${t.name}.${t.type === "ts" ? "ts" : "py"}`,
+ ),
+ type: t.type,
+ })
+ }
+
+ return tests
}
async function runTest(test: TestFile): Promise<boolean> {
- return new Promise((resolve) => {
- console.log(`\n${"=".repeat(60)}`)
- console.log(`Running: ${test.name}`)
- console.log("=".repeat(60))
-
- const cmd = test.type === "ts" ? "bun" : path.join(import.meta.dir, ".venv", "bin", "python3")
- const proc = spawn(cmd, [test.path], {
- stdio: "inherit",
- env: { ...process.env },
- })
-
- proc.on("close", (code) => {
- resolve(code === 0)
- })
- })
+ return new Promise((resolve) => {
+ console.log(`\n${"=".repeat(60)}`)
+ console.log(`Running: ${test.name}`)
+ console.log("=".repeat(60))
+
+ const cmd =
+ test.type === "ts"
+ ? "bun"
+ : path.join(import.meta.dir, ".venv", "bin", "python3")
+ const proc = spawn(cmd, [test.path], {
+ stdio: "inherit",
+ env: { ...process.env },
+ })
+
+ proc.on("close", (code) => {
+ resolve(code === 0)
+ })
+ })
}
async function main() {
- console.log("Supermemory Docs Test Runner")
- console.log("============================\n")
+ console.log("Supermemory Docs Test Runner")
+ console.log("============================\n")
- let tests = getTests()
+ let tests = getTests()
- // Filter tests if specified
- if (filter) {
- tests = tests.filter((t) => t.name.includes(filter) || t.type === filter.replace(".", ""))
- }
+ // Filter tests if specified
+ if (filter) {
+ tests = tests.filter(
+ (t) => t.name.includes(filter) || t.type === filter.replace(".", ""),
+ )
+ }
- if (tests.length === 0) {
- console.log("No tests matched the filter:", filter)
- console.log("\nAvailable tests:")
- getTests().forEach((t) => console.log(` - ${t.name} (${t.type})`))
- process.exit(1)
- }
+ if (tests.length === 0) {
+ console.log("No tests matched the filter:", filter)
+ console.log("\nAvailable tests:")
+ getTests().forEach((t) => console.log(` - ${t.name} (${t.type})`))
+ process.exit(1)
+ }
- console.log(`Running ${tests.length} test(s)...\n`)
+ console.log(`Running ${tests.length} test(s)...\n`)
- const results: { test: string; passed: boolean }[] = []
+ const results: { test: string; passed: boolean }[] = []
- for (const test of tests) {
- const passed = await runTest(test)
- results.push({ test: test.name, passed })
- }
+ for (const test of tests) {
+ const passed = await runTest(test)
+ results.push({ test: test.name, passed })
+ }
- // Summary
- console.log(`\n${"=".repeat(60)}`)
- console.log("SUMMARY")
- console.log("=".repeat(60))
+ // Summary
+ console.log(`\n${"=".repeat(60)}`)
+ console.log("SUMMARY")
+ console.log("=".repeat(60))
- const passed = results.filter((r) => r.passed).length
- const failed = results.filter((r) => !r.passed).length
+ const passed = results.filter((r) => r.passed).length
+ const failed = results.filter((r) => !r.passed).length
- for (const r of results) {
- console.log(`${r.passed ? "โœ…" : "โŒ"} ${r.test}`)
- }
+ for (const r of results) {
+ console.log(`${r.passed ? "โœ…" : "โŒ"} ${r.test}`)
+ }
- console.log(`\nTotal: ${passed} passed, ${failed} failed`)
+ console.log(`\nTotal: ${passed} passed, ${failed} failed`)
- if (failed > 0) {
- process.exit(1)
- }
+ if (failed > 0) {
+ process.exit(1)
+ }
}
main().catch(console.error)
diff --git a/packages/docs-test/tests/integrations/ai-sdk.ts b/packages/docs-test/tests/integrations/ai-sdk.ts
index 7caba72f..f6d4f1d2 100644
--- a/packages/docs-test/tests/integrations/ai-sdk.ts
+++ b/packages/docs-test/tests/integrations/ai-sdk.ts
@@ -2,99 +2,109 @@ import "dotenv/config"
import { openai } from "@ai-sdk/openai"
import { createAnthropic } from "@ai-sdk/anthropic"
import {
- withSupermemory,
- supermemoryTools,
- searchMemoriesTool,
- addMemoryTool,
- type MemoryPromptData,
+ withSupermemory,
+ supermemoryTools,
+ searchMemoriesTool,
+ addMemoryTool,
+ type MemoryPromptData,
} from "@supermemory/tools/ai-sdk"
async function testMiddleware() {
- console.log("=== Middleware ===")
-
- // Basic wrapper
- const model = withSupermemory(openai("gpt-4"), "user-123")
- console.log("โœ“ withSupermemory basic")
-
- // With addMemory option
- const modelWithAdd = withSupermemory(openai("gpt-4"), "user-123", {
- addMemory: "always",
- })
- console.log("โœ“ withSupermemory with addMemory")
-
- // With verbose logging
- const modelVerbose = withSupermemory(openai("gpt-4"), "user-123", {
- verbose: true,
- })
- console.log("โœ“ withSupermemory with verbose")
+ console.log("=== Middleware ===")
+
+ // Basic wrapper
+ const model = withSupermemory(openai("gpt-4"), "user-123")
+ console.log("โœ“ withSupermemory basic")
+
+ // With addMemory option
+ const modelWithAdd = withSupermemory(openai("gpt-4"), "user-123", {
+ addMemory: "always",
+ })
+ console.log("โœ“ withSupermemory with addMemory")
+
+ // With verbose logging
+ const modelVerbose = withSupermemory(openai("gpt-4"), "user-123", {
+ verbose: true,
+ })
+ console.log("โœ“ withSupermemory with verbose")
}
async function testSearchModes() {
- console.log("\n=== Search Modes ===")
-
- const profileModel = withSupermemory(openai("gpt-4"), "user-123", { mode: "profile" })
- console.log("โœ“ mode: profile")
-
- const queryModel = withSupermemory(openai("gpt-4"), "user-123", { mode: "query" })
- console.log("โœ“ mode: query")
-
- const fullModel = withSupermemory(openai("gpt-4"), "user-123", { mode: "full" })
- console.log("โœ“ mode: full")
+ console.log("\n=== Search Modes ===")
+
+ const profileModel = withSupermemory(openai("gpt-4"), "user-123", {
+ mode: "profile",
+ })
+ console.log("โœ“ mode: profile")
+
+ const queryModel = withSupermemory(openai("gpt-4"), "user-123", {
+ mode: "query",
+ })
+ console.log("โœ“ mode: query")
+
+ const fullModel = withSupermemory(openai("gpt-4"), "user-123", {
+ mode: "full",
+ })
+ console.log("โœ“ mode: full")
}
async function testCustomPrompt() {
- console.log("\n=== Custom Prompt Template ===")
+ console.log("\n=== Custom Prompt Template ===")
- const anthropic = createAnthropic({ apiKey: "test-key" })
+ const anthropic = createAnthropic({ apiKey: "test-key" })
- const claudePrompt = (data: MemoryPromptData) =>
- `
+ const claudePrompt = (data: MemoryPromptData) =>
+ `
<context>
<user_profile>${data.userMemories}</user_profile>
<relevant_memories>${data.generalSearchMemories}</relevant_memories>
</context>
`.trim()
- const model = withSupermemory(anthropic("claude-3-sonnet-20240229"), "user-123", {
- mode: "full",
- promptTemplate: claudePrompt,
- })
- console.log("โœ“ Custom prompt template")
+ const model = withSupermemory(
+ anthropic("claude-3-sonnet-20240229"),
+ "user-123",
+ {
+ mode: "full",
+ promptTemplate: claudePrompt,
+ },
+ )
+ console.log("โœ“ Custom prompt template")
}
async function testTools() {
- console.log("\n=== Memory Tools ===")
+ console.log("\n=== Memory Tools ===")
- // All tools
- const tools = supermemoryTools("YOUR_API_KEY")
- console.log("โœ“ supermemoryTools")
+ // All tools
+ const tools = supermemoryTools("YOUR_API_KEY")
+ console.log("โœ“ supermemoryTools")
- // Individual tools
- const searchTool = searchMemoriesTool("API_KEY", { projectId: "personal" })
- console.log("โœ“ searchMemoriesTool")
+ // Individual tools
+ const searchTool = searchMemoriesTool("API_KEY", { projectId: "personal" })
+ console.log("โœ“ searchMemoriesTool")
- const addTool = addMemoryTool("API_KEY")
- console.log("โœ“ addMemoryTool")
+ const addTool = addMemoryTool("API_KEY")
+ console.log("โœ“ addMemoryTool")
- // Combined
- const toolsObj = {
- searchMemories: searchTool,
- addMemory: addTool,
- }
- console.log("โœ“ Combined tools object")
+ // Combined
+ const toolsObj = {
+ searchMemories: searchTool,
+ addMemory: addTool,
+ }
+ console.log("โœ“ Combined tools object")
}
async function main() {
- console.log("AI SDK Integration Tests")
- console.log("========================\n")
+ console.log("AI SDK Integration Tests")
+ console.log("========================\n")
- await testMiddleware()
- await testSearchModes()
- await testCustomPrompt()
- await testTools()
+ await testMiddleware()
+ await testSearchModes()
+ await testCustomPrompt()
+ await testTools()
- console.log("\n========================")
- console.log("โœ… All AI SDK tests passed!")
+ console.log("\n========================")
+ console.log("โœ… All AI SDK tests passed!")
}
main().catch(console.error)
diff --git a/packages/docs-test/tests/integrations/claude-memory.ts b/packages/docs-test/tests/integrations/claude-memory.ts
index c4078789..9256e872 100644
--- a/packages/docs-test/tests/integrations/claude-memory.ts
+++ b/packages/docs-test/tests/integrations/claude-memory.ts
@@ -2,100 +2,104 @@ import "dotenv/config"
import { createClaudeMemoryTool } from "@supermemory/tools/claude-memory"
async function testConfiguration() {
- console.log("=== Configuration ===")
-
- // Basic
- const tool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
- projectId: "my-app",
- })
- console.log("โœ“ Basic config")
-
- // Full options
- const toolFull = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
- projectId: "my-app",
- containerTags: ["user-123", "project-alpha"],
- memoryContainerTag: "my_memory_prefix",
- })
- console.log("โœ“ Full config")
-
- return tool
+ console.log("=== Configuration ===")
+
+ // Basic
+ const tool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
+ projectId: "my-app",
+ })
+ console.log("โœ“ Basic config")
+
+ // Full options
+ const toolFull = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
+ projectId: "my-app",
+ containerTags: ["user-123", "project-alpha"],
+ memoryContainerTag: "my_memory_prefix",
+ })
+ console.log("โœ“ Full config")
+
+ return tool
}
async function testMethods(tool: ReturnType<typeof createClaudeMemoryTool>) {
- console.log("\n=== Methods ===")
-
- console.log(`โœ“ handleCommand exists: ${typeof tool.handleCommand === "function"}`)
- console.log(
- `โœ“ handleCommandForToolResult exists: ${typeof tool.handleCommandForToolResult === "function"}`
- )
+ console.log("\n=== Methods ===")
+
+ console.log(
+ `โœ“ handleCommand exists: ${typeof tool.handleCommand === "function"}`,
+ )
+ console.log(
+ `โœ“ handleCommandForToolResult exists: ${typeof tool.handleCommandForToolResult === "function"}`,
+ )
}
async function testCommands(tool: ReturnType<typeof createClaudeMemoryTool>) {
- console.log("\n=== Commands ===")
-
- // View (list)
- const listResult = await tool.handleCommand({
- command: "view",
- path: "/memories/",
- })
- console.log(`โœ“ view (list): ${listResult.success}`)
-
- // Create
- const createResult = await tool.handleCommand({
- command: "create",
- path: "/memories/test-file.txt",
- file_text: "User prefers dark mode\nFavorite language: TypeScript",
- })
- console.log(`โœ“ create: ${createResult.success}`)
-
- // Wait for indexing
- await new Promise((r) => setTimeout(r, 3000))
-
- // str_replace
- const replaceResult = await tool.handleCommand({
- command: "str_replace",
- path: "/memories/test-file.txt",
- old_str: "dark mode",
- new_str: "light mode",
- })
- console.log(`โœ“ str_replace: ${replaceResult.success}`)
-
- // Delete
- const deleteResult = await tool.handleCommand({
- command: "delete",
- path: "/memories/test-file.txt",
- })
- console.log(`โœ“ delete: ${deleteResult.success}`)
+ console.log("\n=== Commands ===")
+
+ // View (list)
+ const listResult = await tool.handleCommand({
+ command: "view",
+ path: "/memories/",
+ })
+ console.log(`โœ“ view (list): ${listResult.success}`)
+
+ // Create
+ const createResult = await tool.handleCommand({
+ command: "create",
+ path: "/memories/test-file.txt",
+ file_text: "User prefers dark mode\nFavorite language: TypeScript",
+ })
+ console.log(`โœ“ create: ${createResult.success}`)
+
+ // Wait for indexing
+ await new Promise((r) => setTimeout(r, 3000))
+
+ // str_replace
+ const replaceResult = await tool.handleCommand({
+ command: "str_replace",
+ path: "/memories/test-file.txt",
+ old_str: "dark mode",
+ new_str: "light mode",
+ })
+ console.log(`โœ“ str_replace: ${replaceResult.success}`)
+
+ // Delete
+ const deleteResult = await tool.handleCommand({
+ command: "delete",
+ path: "/memories/test-file.txt",
+ })
+ console.log(`โœ“ delete: ${deleteResult.success}`)
}
-async function testToolResultFormat(tool: ReturnType<typeof createClaudeMemoryTool>) {
- console.log("\n=== Tool Result Format ===")
+async function testToolResultFormat(
+ tool: ReturnType<typeof createClaudeMemoryTool>,
+) {
+ console.log("\n=== Tool Result Format ===")
- const toolResult = await tool.handleCommandForToolResult(
- { command: "view", path: "/memories/" },
- "test-tool-id-123"
- )
+ const toolResult = await tool.handleCommandForToolResult(
+ { command: "view", path: "/memories/" },
+ "test-tool-id-123",
+ )
- const hasCorrectStructure =
- toolResult.type === "tool_result" &&
- toolResult.tool_use_id === "test-tool-id-123" &&
- typeof toolResult.content === "string" &&
- typeof toolResult.is_error === "boolean"
+ const hasCorrectStructure =
+ toolResult.type === "tool_result" &&
+ toolResult.tool_use_id === "test-tool-id-123" &&
+ typeof toolResult.content === "string" &&
+ typeof toolResult.is_error === "boolean"
- console.log(`โœ“ Tool result structure: ${hasCorrectStructure}`)
+ console.log(`โœ“ Tool result structure: ${hasCorrectStructure}`)
}
async function main() {
- console.log("Claude Memory Tool Tests")
- console.log("========================\n")
+ console.log("Claude Memory Tool Tests")
+ console.log("========================\n")
- const tool = await testConfiguration()
- await testMethods(tool)
- await testCommands(tool)
- await testToolResultFormat(tool)
+ const tool = await testConfiguration()
+ await testMethods(tool)
+ await testCommands(tool)
+ await testToolResultFormat(tool)
- console.log("\n========================")
- console.log("โœ… All Claude Memory tests passed!")
+ console.log("\n========================")
+ console.log("โœ… All Claude Memory tests passed!")
}
main().catch(console.error)
diff --git a/packages/docs-test/tests/integrations/openai-sdk.ts b/packages/docs-test/tests/integrations/openai-sdk.ts
index c651e7dc..c60c1fd0 100644
--- a/packages/docs-test/tests/integrations/openai-sdk.ts
+++ b/packages/docs-test/tests/integrations/openai-sdk.ts
@@ -5,76 +5,82 @@ import { withSupermemory } from "@supermemory/tools/openai"
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
async function testOpenAIWrapper() {
- console.log("=== OpenAI SDK Wrapper ===")
+ console.log("=== OpenAI SDK Wrapper ===")
- if (!OPENAI_API_KEY) {
- console.log("โš  OPENAI_API_KEY not set, skipping live tests")
- return false
- }
+ if (!OPENAI_API_KEY) {
+ console.log("โš  OPENAI_API_KEY not set, skipping live tests")
+ return false
+ }
- const openai = new OpenAI()
+ const openai = new OpenAI()
- // Basic wrapper
- const client = withSupermemory(openai, "docs-test-openai")
- console.log("โœ“ withSupermemory basic")
+ // Basic wrapper
+ const client = withSupermemory(openai, "docs-test-openai")
+ console.log("โœ“ withSupermemory basic")
- // With options
- const clientWithOptions = withSupermemory(openai, "docs-test-openai", {
- mode: "full",
- addMemory: "always",
- verbose: true,
- })
- console.log("โœ“ withSupermemory with options")
+ // With options
+ const clientWithOptions = withSupermemory(openai, "docs-test-openai", {
+ mode: "full",
+ addMemory: "always",
+ verbose: true,
+ })
+ console.log("โœ“ withSupermemory with options")
}
async function testChatCompletion() {
- console.log("\n=== Chat Completion with Memory ===")
+ console.log("\n=== Chat Completion with Memory ===")
- if (!OPENAI_API_KEY) {
- console.log("โš  OPENAI_API_KEY not set, skipping")
- return
- }
+ if (!OPENAI_API_KEY) {
+ console.log("โš  OPENAI_API_KEY not set, skipping")
+ return
+ }
- const openai = new OpenAI()
- const client = withSupermemory(openai, "docs-test-openai", {
- mode: "full",
- addMemory: "always",
- })
+ const openai = new OpenAI()
+ const client = withSupermemory(openai, "docs-test-openai", {
+ mode: "full",
+ addMemory: "always",
+ })
- // Add context
- const addResponse = await client.chat.completions.create({
- model: "gpt-4o-mini",
- messages: [
- { role: "system", content: "You are a helpful assistant." },
- { role: "user", content: "Remember that my favorite color is blue." },
- ],
- max_tokens: 100,
- })
- console.log("โœ“ Add context:", addResponse.choices[0]?.message.content?.substring(0, 50))
+ // Add context
+ const addResponse = await client.chat.completions.create({
+ model: "gpt-4o-mini",
+ messages: [
+ { role: "system", content: "You are a helpful assistant." },
+ { role: "user", content: "Remember that my favorite color is blue." },
+ ],
+ max_tokens: 100,
+ })
+ console.log(
+ "โœ“ Add context:",
+ addResponse.choices[0]?.message.content?.substring(0, 50),
+ )
- // Retrieve context
- const retrieveResponse = await client.chat.completions.create({
- model: "gpt-4o-mini",
- messages: [
- { role: "system", content: "You are a helpful assistant." },
- { role: "user", content: "What do you know about my preferences?" },
- ],
- max_tokens: 100,
- })
- console.log("โœ“ Retrieve context:", retrieveResponse.choices[0]?.message.content?.substring(0, 50))
+ // Retrieve context
+ const retrieveResponse = await client.chat.completions.create({
+ model: "gpt-4o-mini",
+ messages: [
+ { role: "system", content: "You are a helpful assistant." },
+ { role: "user", content: "What do you know about my preferences?" },
+ ],
+ max_tokens: 100,
+ })
+ console.log(
+ "โœ“ Retrieve context:",
+ retrieveResponse.choices[0]?.message.content?.substring(0, 50),
+ )
}
async function main() {
- console.log("OpenAI SDK Integration Tests")
- console.log("============================\n")
+ console.log("OpenAI SDK Integration Tests")
+ console.log("============================\n")
- const hasKey = await testOpenAIWrapper()
- if (hasKey !== false) {
- await testChatCompletion()
- }
+ const hasKey = await testOpenAIWrapper()
+ if (hasKey !== false) {
+ await testChatCompletion()
+ }
- console.log("\n============================")
- console.log("โœ… All OpenAI SDK tests passed!")
+ console.log("\n============================")
+ console.log("โœ… All OpenAI SDK tests passed!")
}
main().catch(console.error)
diff --git a/packages/docs-test/tests/typescript/sdk.ts b/packages/docs-test/tests/typescript/sdk.ts
index 7dc477e0..065d3b34 100644
--- a/packages/docs-test/tests/typescript/sdk.ts
+++ b/packages/docs-test/tests/typescript/sdk.ts
@@ -5,142 +5,147 @@ import Supermemory, { toFile } from "supermemory"
const client = new Supermemory()
async function testDocumentsCRUD() {
- console.log("\n=== Document CRUD Operations ===")
-
- // Create
- const doc = await client.documents.add({
- content: "Test content for TypeScript SDK - " + Date.now(),
- })
- console.log("โœ“ documents.add:", doc.id)
-
- // Read
- const fetched = await client.documents.get(doc.id)
- console.log("โœ“ documents.get:", fetched.id)
-
- // Update
- const updated = await client.documents.update(doc.id, {
- content: "Updated content - " + Date.now(),
- })
- console.log("โœ“ documents.update:", updated.id)
-
- // Wait for processing before delete
- await new Promise((r) => setTimeout(r, 10000))
-
- // Delete
- await client.documents.delete(doc.id)
- console.log("โœ“ documents.delete")
+ console.log("\n=== Document CRUD Operations ===")
+
+ // Create
+ const doc = await client.documents.add({
+ content: "Test content for TypeScript SDK - " + Date.now(),
+ })
+ console.log("โœ“ documents.add:", doc.id)
+
+ // Read
+ const fetched = await client.documents.get(doc.id)
+ console.log("โœ“ documents.get:", fetched.id)
+
+ // Update
+ const updated = await client.documents.update(doc.id, {
+ content: "Updated content - " + Date.now(),
+ })
+ console.log("โœ“ documents.update:", updated.id)
+
+ // Wait for processing before delete
+ await new Promise((r) => setTimeout(r, 10000))
+
+ // Delete
+ await client.documents.delete(doc.id)
+ console.log("โœ“ documents.delete")
}
async function testBatchOperations() {
- console.log("\n=== Batch Operations ===")
-
- const batch = await client.documents.batchAdd({
- documents: [
- { content: "Batch doc 1 - " + Date.now() },
- { content: "Batch doc 2 - " + Date.now() },
- ],
- })
- console.log("โœ“ documents.batchAdd:", batch)
+ console.log("\n=== Batch Operations ===")
+
+ const batch = await client.documents.batchAdd({
+ documents: [
+ { content: "Batch doc 1 - " + Date.now() },
+ { content: "Batch doc 2 - " + Date.now() },
+ ],
+ })
+ console.log("โœ“ documents.batchAdd:", batch)
}
async function testSearch() {
- console.log("\n=== Search ===")
+ console.log("\n=== Search ===")
- const results = await client.search.execute({ q: "test content" })
- console.log("โœ“ search.execute:", results.results?.length ?? 0, "results")
+ const results = await client.search.execute({ q: "test content" })
+ console.log("โœ“ search.execute:", results.results?.length ?? 0, "results")
}
async function testFileUploads() {
- console.log("\n=== File Uploads ===")
+ console.log("\n=== File Uploads ===")
- // Using fs.createReadStream
- const testFile = "/tmp/test-doc-upload.txt"
- fs.writeFileSync(testFile, "Test file content")
- await client.documents.uploadFile({ file: fs.createReadStream(testFile) })
- console.log("โœ“ uploadFile with fs.createReadStream")
+ // Using fs.createReadStream
+ const testFile = "/tmp/test-doc-upload.txt"
+ fs.writeFileSync(testFile, "Test file content")
+ await client.documents.uploadFile({ file: fs.createReadStream(testFile) })
+ console.log("โœ“ uploadFile with fs.createReadStream")
- // Using File API
- await client.documents.uploadFile({ file: new File(["my bytes"], "file") })
- console.log("โœ“ uploadFile with File API")
+ // Using File API
+ await client.documents.uploadFile({ file: new File(["my bytes"], "file") })
+ console.log("โœ“ uploadFile with File API")
- // Using toFile
- await client.documents.uploadFile({ file: await toFile(Buffer.from("my bytes"), "file") })
- console.log("โœ“ uploadFile with toFile")
+ // Using toFile
+ await client.documents.uploadFile({
+ file: await toFile(Buffer.from("my bytes"), "file"),
+ })
+ console.log("โœ“ uploadFile with toFile")
- fs.unlinkSync(testFile)
+ fs.unlinkSync(testFile)
}
async function testClientConfig() {
- console.log("\n=== Client Configuration ===")
-
- // Retries
- const clientWithRetries = new Supermemory({ maxRetries: 0 })
- console.log("โœ“ maxRetries config")
-
- // Timeout
- const clientWithTimeout = new Supermemory({ timeout: 20 * 1000 })
- console.log("โœ“ timeout config")
-
- // Per-request options
- await client.add({ content: "Test - " + Date.now() }, { maxRetries: 5, timeout: 5000 })
- console.log("โœ“ per-request options")
+ console.log("\n=== Client Configuration ===")
+
+ // Retries
+ const clientWithRetries = new Supermemory({ maxRetries: 0 })
+ console.log("โœ“ maxRetries config")
+
+ // Timeout
+ const clientWithTimeout = new Supermemory({ timeout: 20 * 1000 })
+ console.log("โœ“ timeout config")
+
+ // Per-request options
+ await client.add(
+ { content: "Test - " + Date.now() },
+ { maxRetries: 5, timeout: 5000 },
+ )
+ console.log("โœ“ per-request options")
}
async function testRawResponse() {
- console.log("\n=== Raw Response ===")
+ console.log("\n=== Raw Response ===")
- const response = await client.documents
- .add({ content: "Test - " + Date.now() })
- .asResponse()
- console.log("โœ“ .asResponse() status:", response.status)
+ const response = await client.documents
+ .add({ content: "Test - " + Date.now() })
+ .asResponse()
+ console.log("โœ“ .asResponse() status:", response.status)
- const { data, response: raw } = await client.documents
- .add({ content: "Test - " + Date.now() })
- .withResponse()
- console.log("โœ“ .withResponse() data:", data.id, "status:", raw.status)
+ const { data, response: raw } = await client.documents
+ .add({ content: "Test - " + Date.now() })
+ .withResponse()
+ console.log("โœ“ .withResponse() data:", data.id, "status:", raw.status)
}
async function testTypes() {
- console.log("\n=== Types ===")
+ console.log("\n=== Types ===")
- const params: Supermemory.AddParams = {
- content: "Typed content",
- }
- const response: Supermemory.AddResponse = await client.add(params)
- console.log("โœ“ TypeScript types work:", response.id)
+ const params: Supermemory.AddParams = {
+ content: "Typed content",
+ }
+ const response: Supermemory.AddResponse = await client.add(params)
+ console.log("โœ“ TypeScript types work:", response.id)
}
async function testErrorHandling() {
- console.log("\n=== Error Handling ===")
-
- const response = await client.documents
- .add({ content: "Test - " + Date.now() })
- .catch(async (err) => {
- if (err instanceof Supermemory.APIError) {
- console.log("Caught APIError:", err.status, err.name)
- } else {
- throw err
- }
- return null
- })
- console.log("โœ“ Error handling pattern works:", response?.id)
+ console.log("\n=== Error Handling ===")
+
+ const response = await client.documents
+ .add({ content: "Test - " + Date.now() })
+ .catch(async (err) => {
+ if (err instanceof Supermemory.APIError) {
+ console.log("Caught APIError:", err.status, err.name)
+ } else {
+ throw err
+ }
+ return null
+ })
+ console.log("โœ“ Error handling pattern works:", response?.id)
}
async function main() {
- console.log("TypeScript SDK Tests")
- console.log("====================")
-
- await testDocumentsCRUD()
- await testBatchOperations()
- await testSearch()
- await testFileUploads()
- await testClientConfig()
- await testRawResponse()
- await testTypes()
- await testErrorHandling()
-
- console.log("\n====================")
- console.log("โœ… All TypeScript SDK tests passed!")
+ console.log("TypeScript SDK Tests")
+ console.log("====================")
+
+ await testDocumentsCRUD()
+ await testBatchOperations()
+ await testSearch()
+ await testFileUploads()
+ await testClientConfig()
+ await testRawResponse()
+ await testTypes()
+ await testErrorHandling()
+
+ console.log("\n====================")
+ console.log("โœ… All TypeScript SDK tests passed!")
}
main().catch(console.error)
diff --git a/packages/docs-test/tests/typescript/search.ts b/packages/docs-test/tests/typescript/search.ts
index 45f903eb..d879fa1c 100644
--- a/packages/docs-test/tests/typescript/search.ts
+++ b/packages/docs-test/tests/typescript/search.ts
@@ -4,107 +4,115 @@ import Supermemory from "supermemory"
const client = new Supermemory()
async function testSearchModes() {
- console.log("=== Search Modes ===")
-
- // Hybrid: memories + document chunks (recommended)
- const hybridResults = await client.search.memories({
- q: "quarterly goals",
- containerTag: "user_123",
- searchMode: "hybrid",
- })
- console.log("โœ“ hybrid search:", hybridResults.results.length, "results")
-
- // Memories only
- const memoriesResults = await client.search.memories({
- q: "user preferences",
- containerTag: "user_123",
- searchMode: "memories",
- })
- console.log("โœ“ memories search:", memoriesResults.results.length, "results")
+ console.log("=== Search Modes ===")
+
+ // Hybrid: memories + document chunks (recommended)
+ const hybridResults = await client.search.memories({
+ q: "quarterly goals",
+ containerTag: "user_123",
+ searchMode: "hybrid",
+ })
+ console.log("โœ“ hybrid search:", hybridResults.results.length, "results")
+
+ // Memories only
+ const memoriesResults = await client.search.memories({
+ q: "user preferences",
+ containerTag: "user_123",
+ searchMode: "memories",
+ })
+ console.log("โœ“ memories search:", memoriesResults.results.length, "results")
}
async function testFiltering() {
- console.log("\n=== Filtering ===")
-
- // Basic containerTag filter
- const results = await client.search.memories({
- q: "project updates",
- containerTag: "user_123",
- searchMode: "hybrid",
- })
- console.log("โœ“ containerTag filter:", results.results.length, "results")
-
- // Metadata-based filtering
- const filteredResults = await client.search.memories({
- q: "meeting notes",
- containerTag: "user_123",
- filters: {
- AND: [
- { key: "type", value: "meeting" },
- { key: "year", value: "2024" },
- ],
- },
- })
- console.log("โœ“ metadata filter:", filteredResults.results.length, "results")
+ console.log("\n=== Filtering ===")
+
+ // Basic containerTag filter
+ const results = await client.search.memories({
+ q: "project updates",
+ containerTag: "user_123",
+ searchMode: "hybrid",
+ })
+ console.log("โœ“ containerTag filter:", results.results.length, "results")
+
+ // Metadata-based filtering
+ const filteredResults = await client.search.memories({
+ q: "meeting notes",
+ containerTag: "user_123",
+ filters: {
+ AND: [
+ { key: "type", value: "meeting" },
+ { key: "year", value: "2024" },
+ ],
+ },
+ })
+ console.log("โœ“ metadata filter:", filteredResults.results.length, "results")
}
async function testReranking() {
- console.log("\n=== Reranking ===")
-
- const results = await client.search.memories({
- q: "complex technical question",
- containerTag: "user_123",
- rerank: true,
- })
- console.log("โœ“ reranking:", results.results.length, "results")
+ console.log("\n=== Reranking ===")
+
+ const results = await client.search.memories({
+ q: "complex technical question",
+ containerTag: "user_123",
+ rerank: true,
+ })
+ console.log("โœ“ reranking:", results.results.length, "results")
}
async function testThreshold() {
- console.log("\n=== Threshold ===")
-
- const broadResults = await client.search.memories({
- q: "test query",
- threshold: 0.3,
- })
- console.log("โœ“ broad threshold (0.3):", broadResults.results.length, "results")
-
- const preciseResults = await client.search.memories({
- q: "test query",
- threshold: 0.8,
- })
- console.log("โœ“ precise threshold (0.8):", preciseResults.results.length, "results")
+ console.log("\n=== Threshold ===")
+
+ const broadResults = await client.search.memories({
+ q: "test query",
+ threshold: 0.3,
+ })
+ console.log(
+ "โœ“ broad threshold (0.3):",
+ broadResults.results.length,
+ "results",
+ )
+
+ const preciseResults = await client.search.memories({
+ q: "test query",
+ threshold: 0.8,
+ })
+ console.log(
+ "โœ“ precise threshold (0.8):",
+ preciseResults.results.length,
+ "results",
+ )
}
async function testChatbotContext() {
- console.log("\n=== Chatbot Context Pattern ===")
-
- async function getContext(userId: string, message: string) {
- const results = await client.search.memories({
- q: message,
- containerTag: userId,
- searchMode: "hybrid",
- threshold: 0.6,
- limit: 5,
- })
- return results.results.map((r) => r.memory || r.chunk).join("\n\n")
- }
-
- const context = await getContext("user_123", "What are the project updates?")
- console.log("โœ“ chatbot context:", context.length, "chars")
+ console.log("\n=== Chatbot Context Pattern ===")
+
+ async function getContext(userId: string, message: string) {
+ const results = await client.search.memories({
+ q: message,
+ containerTag: userId,
+ searchMode: "hybrid",
+ threshold: 0.6,
+ limit: 5,
+ })
+ return results.results.map((r) => r.memory || r.chunk).join("\n\n")
+ }
+
+ const context = await getContext("user_123", "What are the project updates?")
+ console.log("โœ“ chatbot context:", context.length, "chars")
}
async function main() {
- console.log("Search Tests")
- console.log("============\n")
+ console.log("Search Tests")
+ console.log("============\n")
- await testSearchModes()
- await testFiltering()
- await testReranking()
- await testThreshold()
- await testChatbotContext()
+ await testSearchModes()
+ await testFiltering()
+ await testReranking()
+ await testThreshold()
+ await testChatbotContext()
- console.log("\n============")
- console.log("โœ… All search tests passed!")
+ console.log("\n============")
+ console.log("โœ… All search tests passed!")
}
main().catch(console.error)
diff --git a/packages/docs-test/tests/typescript/user-profiles.ts b/packages/docs-test/tests/typescript/user-profiles.ts
index 4643af4f..1d66a384 100644
--- a/packages/docs-test/tests/typescript/user-profiles.ts
+++ b/packages/docs-test/tests/typescript/user-profiles.ts
@@ -5,58 +5,62 @@ const client = new Supermemory()
const USER_ID = "docs-test-profiles"
async function testBasicProfile() {
- console.log("=== Basic Profile ===")
-
- const profile = await client.profile({
- containerTag: USER_ID,
- q: "What are my preferences?",
- })
-
- console.log("โœ“ Static profile:", profile.profile.static.length, "items")
- console.log("โœ“ Dynamic profile:", profile.profile.dynamic.length, "items")
- console.log("โœ“ Search results:", profile.searchResults.results.length, "items")
+ console.log("=== Basic Profile ===")
+
+ const profile = await client.profile({
+ containerTag: USER_ID,
+ q: "What are my preferences?",
+ })
+
+ console.log("โœ“ Static profile:", profile.profile.static.length, "items")
+ console.log("โœ“ Dynamic profile:", profile.profile.dynamic.length, "items")
+ console.log(
+ "โœ“ Search results:",
+ profile.searchResults.results.length,
+ "items",
+ )
}
async function testProfileWithMemories() {
- console.log("\n=== Profile with Memory Context ===")
-
- // First add some memories
- await client.add({
- content: "User prefers dark mode for all applications",
- containerTag: USER_ID,
- })
- await client.add({
- content: "User is learning TypeScript and Rust",
- containerTag: USER_ID,
- })
-
- // Wait a bit for indexing
- await new Promise((r) => setTimeout(r, 2000))
-
- // Now get profile with search
- const profile = await client.profile({
- containerTag: USER_ID,
- q: "What programming languages does the user know?",
- })
-
- console.log("โœ“ Profile retrieved with memories")
- console.log(" Static:", profile.profile.static.slice(0, 2))
- console.log(" Dynamic:", profile.profile.dynamic.slice(0, 2))
+ console.log("\n=== Profile with Memory Context ===")
+
+ // First add some memories
+ await client.add({
+ content: "User prefers dark mode for all applications",
+ containerTag: USER_ID,
+ })
+ await client.add({
+ content: "User is learning TypeScript and Rust",
+ containerTag: USER_ID,
+ })
+
+ // Wait a bit for indexing
+ await new Promise((r) => setTimeout(r, 2000))
+
+ // Now get profile with search
+ const profile = await client.profile({
+ containerTag: USER_ID,
+ q: "What programming languages does the user know?",
+ })
+
+ console.log("โœ“ Profile retrieved with memories")
+ console.log(" Static:", profile.profile.static.slice(0, 2))
+ console.log(" Dynamic:", profile.profile.dynamic.slice(0, 2))
}
async function testBuildingContext() {
- console.log("\n=== Building LLM Context ===")
+ console.log("\n=== Building LLM Context ===")
- const conversation = [
- { role: "user", content: "What theme should I use for my IDE?" },
- ]
+ const conversation = [
+ { role: "user", content: "What theme should I use for my IDE?" },
+ ]
- const profile = await client.profile({
- containerTag: USER_ID,
- q: conversation.at(-1)!.content,
- })
+ const profile = await client.profile({
+ containerTag: USER_ID,
+ q: conversation.at(-1)!.content,
+ })
- const context = `User Profile:
+ const context = `User Profile:
${profile.profile.static.join("\n")}
Recent Context:
@@ -65,25 +69,25 @@ ${profile.profile.dynamic.join("\n")}
Relevant Memories:
${profile.searchResults.results.map((r) => r.content).join("\n")}`
- console.log("โœ“ Built context:", context.length, "chars")
+ console.log("โœ“ Built context:", context.length, "chars")
- const messages = [
- { role: "system", content: `Use this context about the user:\n${context}` },
- ...conversation,
- ]
- console.log("โœ“ Messages ready for LLM:", messages.length, "messages")
+ const messages = [
+ { role: "system", content: `Use this context about the user:\n${context}` },
+ ...conversation,
+ ]
+ console.log("โœ“ Messages ready for LLM:", messages.length, "messages")
}
async function main() {
- console.log("User Profiles Tests")
- console.log("===================\n")
+ console.log("User Profiles Tests")
+ console.log("===================\n")
- await testBasicProfile()
- await testProfileWithMemories()
- await testBuildingContext()
+ await testBasicProfile()
+ await testProfileWithMemories()
+ await testBuildingContext()
- console.log("\n===================")
- console.log("โœ… All user profile tests passed!")
+ console.log("\n===================")
+ console.log("โœ… All user profile tests passed!")
}
main().catch(console.error)
diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx
index 324b213e..4bfdc2d7 100644
--- a/packages/lib/auth-context.tsx
+++ b/packages/lib/auth-context.tsx
@@ -43,10 +43,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
.then((org) => {
if (org.metadata?.isConsumer === true) {
console.log("Consumer organization:", org)
- setOrg(org)
+ setOrg(org)
} else {
console.log("ALl orgs:", orgs)
- const consumerOrg = orgs?.find((o) => o.metadata?.isConsumer === true)
+ const consumerOrg = orgs?.find(
+ (o) => o.metadata?.isConsumer === true,
+ )
if (consumerOrg) {
setActiveOrg(consumerOrg.slug)
}
diff --git a/packages/memory-graph/src/components/graph-canvas.tsx b/packages/memory-graph/src/components/graph-canvas.tsx
index 28dd53c7..f4cede9f 100644
--- a/packages/memory-graph/src/components/graph-canvas.tsx
+++ b/packages/memory-graph/src/components/graph-canvas.tsx
@@ -84,11 +84,11 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const progress = Math.min(elapsed / duration, 1)
// Ease-out cubic easing for smooth deceleration
- const eased = 1 - Math.pow(1 - progress, 3)
+ const eased = 1 - (1 - progress) ** 3
dimProgress.current = startDim + (targetDim - startDim) * eased
// Force re-render to update canvas during animation
- forceRender(prev => prev + 1)
+ forceRender((prev) => prev + 1)
if (progress < 1) {
dimAnimationRef.current = requestAnimationFrame(animate)
@@ -145,8 +145,10 @@ export const GraphCanvas = memo<GraphCanvasProps>(
// Only check nodes in the clicked cell (and neighboring cells for edge cases)
const cellsToCheck = [
cellKey,
- `${cellX-1},${cellY}`, `${cellX+1},${cellY}`,
- `${cellX},${cellY-1}`, `${cellX},${cellY+1}`,
+ `${cellX - 1},${cellY}`,
+ `${cellX + 1},${cellY}`,
+ `${cellX},${cellY - 1}`,
+ `${cellX},${cellY + 1}`,
]
// Check from top-most to bottom-most: memory nodes are drawn after documents
@@ -360,7 +362,12 @@ export const GraphCanvas = memo<GraphCanvasProps>(
})
// Helper function to draw a single edge path
- const drawEdgePath = (edge: typeof edges[0], sourceNode: GraphNode, targetNode: GraphNode, edgeShouldDim: boolean) => {
+ const drawEdgePath = (
+ edge: (typeof edges)[0],
+ sourceNode: GraphNode,
+ targetNode: GraphNode,
+ edgeShouldDim: boolean,
+ ) => {
const sourceX = sourceNode.x * zoom + panX
const sourceY = sourceNode.y * zoom + panY
const targetX = targetNode.x * zoom + panX
@@ -381,9 +388,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const dy = targetY - sourceY
const distance = Math.sqrt(dx * dx + dy * dy)
const controlOffset =
- edge.edgeType === "doc-memory"
- ? 15
- : Math.min(30, distance * 0.2)
+ edge.edgeType === "doc-memory" ? 15 : Math.min(30, distance * 0.2)
ctx.beginPath()
ctx.moveTo(sourceX, sourceY)
@@ -398,7 +403,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
}
// Smooth edge opacity: interpolate between full and 0.05 (dimmed)
- const edgeDimOpacity = 1 - (dimProgress.current * 0.95)
+ const edgeDimOpacity = 1 - dimProgress.current * 0.95
// BATCH 1: Draw all doc-memory edges together
if (docMemoryEdges.length > 0) {
@@ -417,7 +422,8 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
- const edgeShouldDim = selectedNodeId !== null &&
+ const edgeShouldDim =
+ selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim ? edgeDimOpacity : 0.9
@@ -444,10 +450,13 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
- const edgeShouldDim = selectedNodeId !== null &&
+ const edgeShouldDim =
+ selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
- const opacity = edgeShouldDim ? edgeDimOpacity : Math.max(0, edge.similarity * 0.5)
+ const opacity = edgeShouldDim
+ ? edgeDimOpacity
+ : Math.max(0, edge.similarity * 0.5)
const lineWidth = Math.max(1, edge.similarity * 2)
// Set color based on similarity strength
@@ -480,7 +489,8 @@ export const GraphCanvas = memo<GraphCanvasProps>(
: edge.target
if (sourceNode && targetNode) {
- const edgeShouldDim = selectedNodeId !== null &&
+ const edgeShouldDim =
+ selectedNodeId !== null &&
sourceNode.id !== selectedNodeId &&
targetNode.id !== selectedNodeId
const opacity = edgeShouldDim ? edgeDimOpacity : 0.8
@@ -568,7 +578,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const isSelected = selectedNodeId === node.id
const shouldDim = selectedNodeId !== null && !isSelected
// Smooth opacity: interpolate between 1 (full) and 0.1 (dimmed) based on animation progress
- const nodeOpacity = shouldDim ? 1 - (dimProgress.current * 0.9) : 1
+ const nodeOpacity = shouldDim ? 1 - dimProgress.current * 0.9 : 1
const isHighlightedDocument = (() => {
if (node.type !== "document" || highlightSet.size === 0) return false
const doc = node.data as DocumentWithMemories
@@ -710,7 +720,7 @@ export const GraphCanvas = memo<GraphCanvasProps>(
const radius = nodeSize / 2
ctx.fillStyle = fillColor
- ctx.globalAlpha = shouldDim ? nodeOpacity : (isLatest ? 1 : 0.4)
+ ctx.globalAlpha = shouldDim ? nodeOpacity : isLatest ? 1 : 0.4
ctx.strokeStyle = borderColor
ctx.lineWidth = isDragging ? 3 : isHovered ? 2 : 1.5
@@ -833,7 +843,17 @@ export const GraphCanvas = memo<GraphCanvasProps>(
})
ctx.globalAlpha = 1
- }, [nodes, edges, panX, panY, zoom, width, height, highlightDocumentIds, nodeMap])
+ }, [
+ nodes,
+ edges,
+ panX,
+ panY,
+ zoom,
+ width,
+ height,
+ highlightDocumentIds,
+ nodeMap,
+ ])
// Hybrid rendering: continuous when simulation active, change-based when idle
const lastRenderParams = useRef<number>(0)
@@ -857,9 +877,16 @@ export const GraphCanvas = memo<GraphCanvasProps>(
}, 0)
// Combine all factors into a single number
- return positionHash ^ edges.length ^
- Math.round(panX) ^ Math.round(panY) ^
- Math.round(zoom * 100) ^ width ^ height ^ highlightHash
+ return (
+ positionHash ^
+ edges.length ^
+ Math.round(panX) ^
+ Math.round(panY) ^
+ Math.round(zoom * 100) ^
+ width ^
+ height ^
+ highlightHash
+ )
}, [
nodes,
edges.length,
@@ -962,13 +989,10 @@ export const GraphCanvas = memo<GraphCanvasProps>(
// Calculate effective DPR that keeps us within safe limits
// Prevent division by zero by checking for valid dimensions
- const maxDpr = width > 0 && height > 0
- ? Math.min(
- MAX_CANVAS_SIZE / width,
- MAX_CANVAS_SIZE / height,
- dpr
- )
- : dpr
+ const maxDpr =
+ width > 0 && height > 0
+ ? Math.min(MAX_CANVAS_SIZE / width, MAX_CANVAS_SIZE / height, dpr)
+ : dpr
// upscale backing store with clamped dimensions
canvas.style.width = `${width}px`
@@ -1026,4 +1050,4 @@ export const GraphCanvas = memo<GraphCanvasProps>(
},
)
-GraphCanvas.displayName = "GraphCanvas" \ No newline at end of file
+GraphCanvas.displayName = "GraphCanvas"
diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx
index b8dd493d..3368bc4d 100644
--- a/packages/memory-graph/src/components/memory-graph.tsx
+++ b/packages/memory-graph/src/components/memory-graph.tsx
@@ -2,7 +2,14 @@
import { GlassMenuEffect } from "@/ui/glass-effect"
import { AnimatePresence } from "motion/react"
-import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useReducer,
+ useRef,
+ useState,
+} from "react"
import { GraphCanvas } from "./graph-canvas"
import { useGraphData } from "@/hooks/use-graph-data"
import { useGraphInteractions } from "@/hooks/use-graph-interactions"
@@ -426,8 +433,10 @@ export const MemoryGraph = ({
// Calculate node dimensions to position popover with proper gap
const nodeSize = selectedNodeData.size * zoom
- const nodeWidth = selectedNodeData.type === "document" ? nodeSize * 1.4 : nodeSize
- const nodeHeight = selectedNodeData.type === "document" ? nodeSize * 0.9 : nodeSize
+ const nodeWidth =
+ selectedNodeData.type === "document" ? nodeSize * 1.4 : nodeSize
+ const nodeHeight =
+ selectedNodeData.type === "document" ? nodeSize * 0.9 : nodeSize
const gap = 20 // Gap between node and popover
// Smart positioning: flip to other side if would go off-screen
@@ -457,7 +466,14 @@ export const MemoryGraph = ({
}
return { x: popoverX, y: popoverY }
- }, [selectedNodeData, zoom, panX, panY, containerSize.width, containerSize.height])
+ }, [
+ selectedNodeData,
+ zoom,
+ panX,
+ panY,
+ containerSize.width,
+ containerSize.height,
+ ])
// Viewport-based loading: load more when most documents are visible (optional)
const checkAndLoadMore = useCallback(() => {
@@ -564,7 +580,14 @@ export const MemoryGraph = ({
containerSizeRef.current = containerSize
onSlideshowNodeChangeRef.current = onSlideshowNodeChange
forceSimulationRef.current = forceSimulation
- }, [nodes, handleNodeClick, centerViewportOn, containerSize, onSlideshowNodeChange, forceSimulation])
+ }, [
+ nodes,
+ handleNodeClick,
+ centerViewportOn,
+ containerSize,
+ onSlideshowNodeChange,
+ forceSimulation,
+ ])
useEffect(() => {
// Clear any existing interval and timeout when isSlideshowActive changes
@@ -731,7 +754,7 @@ export const MemoryGraph = ({
height={containerSize.height}
nodes={nodes}
highlightDocumentIds={highlightsVisible ? highlightDocumentIds : []}
- isSimulationActive={forceSimulation.isActive()}
+ isSimulationActive={forceSimulation.isActive()}
onDoubleClick={handleDoubleClick}
onNodeClick={handleNodeClickWithPhysics}
onNodeDragEnd={handleNodeDragEndWithPhysics}
diff --git a/packages/memory-graph/src/components/navigation-controls.css.ts b/packages/memory-graph/src/components/navigation-controls.css.ts
index c17f09b4..318cf067 100644
--- a/packages/memory-graph/src/components/navigation-controls.css.ts
+++ b/packages/memory-graph/src/components/navigation-controls.css.ts
@@ -17,7 +17,7 @@ const navButtonBase = style({
backgroundColor: "rgba(0, 0, 0, 0.2)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
- border: `1px solid rgba(255, 255, 255, 0.1)`,
+ border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: themeContract.radii.lg,
padding: themeContract.space[2],
color: "rgba(255, 255, 255, 0.7)",
diff --git a/packages/memory-graph/src/components/node-detail-panel.tsx b/packages/memory-graph/src/components/node-detail-panel.tsx
index b022364d..578224b2 100644
--- a/packages/memory-graph/src/components/node-detail-panel.tsx
+++ b/packages/memory-graph/src/components/node-detail-panel.tsx
@@ -68,7 +68,7 @@ const getDocumentIcon = (type: string) => {
return <PDF {...iconProps} />
default:
{
- /*@ts-ignore */
+ /*@ts-expect-error */
}
return <FileText {...iconProps} />
}
@@ -109,7 +109,7 @@ export const NodeDetailPanel = memo(function NodeDetailPanel({
{isDocument ? (
getDocumentIcon((data as DocumentWithMemories).type ?? "")
) : (
- // @ts-ignore
+ // @ts-expect-error
<Brain className={styles.headerIconMemory} />
)}
<HeadingH3Bold>{isDocument ? "Document" : "Memory"}</HeadingH3Bold>
diff --git a/packages/memory-graph/src/components/node-popover.tsx b/packages/memory-graph/src/components/node-popover.tsx
index 8c798110..5570038d 100644
--- a/packages/memory-graph/src/components/node-popover.tsx
+++ b/packages/memory-graph/src/components/node-popover.tsx
@@ -55,7 +55,11 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
return (
<>
{/* Invisible backdrop to catch clicks outside */}
- <div onClick={handleBackdropClick} className={backdropClassName} style={backdropStyle} />
+ <div
+ onClick={handleBackdropClick}
+ className={backdropClassName}
+ style={backdropStyle}
+ />
{/* Popover content */}
<div
@@ -72,16 +76,24 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Header */}
<div className={styles.header}>
<div className={styles.headerTitle}>
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.headerIcon}>
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
- <polyline points="14 2 14 8 20 8"></polyline>
- <line x1="16" y1="13" x2="8" y2="13"></line>
- <line x1="16" y1="17" x2="8" y2="17"></line>
- <polyline points="10 9 9 9 8 9"></polyline>
+ <svg
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className={styles.headerIcon}
+ >
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
+ <polyline points="14 2 14 8 20 8" />
+ <line x1="16" y1="13" x2="8" y2="13" />
+ <line x1="16" y1="17" x2="8" y2="17" />
+ <polyline points="10 9 9 9 8 9" />
</svg>
- <h3 className={styles.title}>
- Document
- </h3>
+ <h3 className={styles.title}>Document</h3>
</div>
<button
type="button"
@@ -96,9 +108,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
<div className={styles.sectionsContainer}>
{/* Title */}
<div>
- <div className={styles.fieldLabel}>
- Title
- </div>
+ <div className={styles.fieldLabel}>Title</div>
<p className={styles.fieldValue}>
{(node.data as any).title || "Untitled Document"}
</p>
@@ -107,9 +117,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Summary - truncated to 2 lines */}
{(node.data as any).summary && (
<div>
- <div className={styles.fieldLabel}>
- Summary
- </div>
+ <div className={styles.fieldLabel}>Summary</div>
<p className={styles.summaryValue}>
{(node.data as any).summary}
</p>
@@ -118,9 +126,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Type */}
<div>
- <div className={styles.fieldLabel}>
- Type
- </div>
+ <div className={styles.fieldLabel}>Type</div>
<p className={styles.fieldValue}>
{(node.data as any).type || "Document"}
</p>
@@ -128,9 +134,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Memory Count */}
<div>
- <div className={styles.fieldLabel}>
- Memory Count
- </div>
+ <div className={styles.fieldLabel}>Memory Count</div>
<p className={styles.fieldValue}>
{(node.data as any).memoryEntries?.length || 0} memories
</p>
@@ -139,9 +143,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* URL */}
{((node.data as any).url || (node.data as any).customId) && (
<div>
- <div className={styles.fieldLabel}>
- URL
- </div>
+ <div className={styles.fieldLabel}>URL</div>
<a
href={(() => {
const doc = node.data as any
@@ -160,10 +162,19 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
rel="noopener noreferrer"
className={styles.link}
>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
- <polyline points="15 3 21 3 21 9"></polyline>
- <line x1="10" y1="14" x2="21" y2="3"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
+ <polyline points="15 3 21 3 21 9" />
+ <line x1="10" y1="14" x2="21" y2="3" />
</svg>
View Document
</a>
@@ -173,20 +184,42 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Footer with metadata */}
<div className={styles.footer}>
<div className={styles.footerItem}>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
- <line x1="16" y1="2" x2="16" y2="6"></line>
- <line x1="8" y1="2" x2="8" y2="6"></line>
- <line x1="3" y1="10" x2="21" y2="10"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+ <line x1="16" y1="2" x2="16" y2="6" />
+ <line x1="8" y1="2" x2="8" y2="6" />
+ <line x1="3" y1="10" x2="21" y2="10" />
</svg>
- <span>{new Date((node.data as any).createdAt).toLocaleDateString()}</span>
+ <span>
+ {new Date(
+ (node.data as any).createdAt,
+ ).toLocaleDateString()}
+ </span>
</div>
<div className={styles.footerItemId}>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <line x1="4" y1="9" x2="20" y2="9"></line>
- <line x1="4" y1="15" x2="20" y2="15"></line>
- <line x1="10" y1="3" x2="8" y2="21"></line>
- <line x1="16" y1="3" x2="14" y2="21"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <line x1="4" y1="9" x2="20" y2="9" />
+ <line x1="4" y1="15" x2="20" y2="15" />
+ <line x1="10" y1="3" x2="8" y2="21" />
+ <line x1="16" y1="3" x2="14" y2="21" />
</svg>
<span className={styles.idText}>{node.id}</span>
</div>
@@ -199,13 +232,21 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Header */}
<div className={styles.header}>
<div className={styles.headerTitle}>
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.headerIconMemory}>
- <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"></path>
- <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"></path>
+ <svg
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ className={styles.headerIconMemory}
+ >
+ <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z" />
+ <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z" />
</svg>
- <h3 className={styles.title}>
- Memory
- </h3>
+ <h3 className={styles.title}>Memory</h3>
</div>
<button
type="button"
@@ -220,31 +261,31 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
<div className={styles.sectionsContainer}>
{/* Memory content */}
<div>
- <div className={styles.fieldLabel}>
- Memory
- </div>
+ <div className={styles.fieldLabel}>Memory</div>
<p className={styles.fieldValue}>
- {(node.data as any).memory || (node.data as any).content || "No content"}
+ {(node.data as any).memory ||
+ (node.data as any).content ||
+ "No content"}
</p>
{(node.data as any).isForgotten && (
- <div className={styles.forgottenBadge}>
- Forgotten
- </div>
+ <div className={styles.forgottenBadge}>Forgotten</div>
)}
{/* Expires (inline with memory if exists) */}
{(node.data as any).forgetAfter && (
<p className={styles.expiresText}>
- Expires: {new Date((node.data as any).forgetAfter).toLocaleDateString()}
- {(node.data as any).forgetReason && ` - ${(node.data as any).forgetReason}`}
+ Expires:{" "}
+ {new Date(
+ (node.data as any).forgetAfter,
+ ).toLocaleDateString()}
+ {(node.data as any).forgetReason &&
+ ` - ${(node.data as any).forgetReason}`}
</p>
)}
</div>
{/* Space */}
<div>
- <div className={styles.fieldLabel}>
- Space
- </div>
+ <div className={styles.fieldLabel}>Space</div>
<p className={styles.fieldValue}>
{(node.data as any).spaceId || "Default"}
</p>
@@ -253,20 +294,42 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
{/* Footer with metadata */}
<div className={styles.footer}>
<div className={styles.footerItem}>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
- <line x1="16" y1="2" x2="16" y2="6"></line>
- <line x1="8" y1="2" x2="8" y2="6"></line>
- <line x1="3" y1="10" x2="21" y2="10"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
+ <line x1="16" y1="2" x2="16" y2="6" />
+ <line x1="8" y1="2" x2="8" y2="6" />
+ <line x1="3" y1="10" x2="21" y2="10" />
</svg>
- <span>{new Date((node.data as any).createdAt).toLocaleDateString()}</span>
+ <span>
+ {new Date(
+ (node.data as any).createdAt,
+ ).toLocaleDateString()}
+ </span>
</div>
<div className={styles.footerItemId}>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <line x1="4" y1="9" x2="20" y2="9"></line>
- <line x1="4" y1="15" x2="20" y2="15"></line>
- <line x1="10" y1="3" x2="8" y2="21"></line>
- <line x1="16" y1="3" x2="14" y2="21"></line>
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <line x1="4" y1="9" x2="20" y2="9" />
+ <line x1="4" y1="15" x2="20" y2="15" />
+ <line x1="10" y1="3" x2="8" y2="21" />
+ <line x1="16" y1="3" x2="14" y2="21" />
</svg>
<span className={styles.idText}>{node.id}</span>
</div>
diff --git a/packages/memory-graph/src/hooks/use-force-simulation.ts b/packages/memory-graph/src/hooks/use-force-simulation.ts
index d409a4b1..32c38962 100644
--- a/packages/memory-graph/src/hooks/use-force-simulation.ts
+++ b/packages/memory-graph/src/hooks/use-force-simulation.ts
@@ -127,9 +127,8 @@ export function useForceSimulation(
// Update edges
if (edges.length > 0) {
- const linkForce = simulationRef.current.force<
- d3.ForceLink<GraphNode, GraphEdge>
- >("link")
+ const linkForce =
+ simulationRef.current.force<d3.ForceLink<GraphNode, GraphEdge>>("link")
if (linkForce) {
linkForce.links(edges)
}
diff --git a/packages/memory-graph/src/hooks/use-graph-data.ts b/packages/memory-graph/src/hooks/use-graph-data.ts
index 9bcd0d55..a94bebdc 100644
--- a/packages/memory-graph/src/hooks/use-graph-data.ts
+++ b/packages/memory-graph/src/hooks/use-graph-data.ts
@@ -19,7 +19,16 @@ import type {
export function useGraphData(
data: DocumentsResponse | null,
selectedSpace: string,
- nodePositions: Map<string, { x: number; y: number; parentDocId?: string; offsetX?: number; offsetY?: number }>,
+ nodePositions: Map<
+ string,
+ {
+ x: number
+ y: number
+ parentDocId?: string
+ offsetX?: number
+ offsetY?: number
+ }
+ >,
draggingNodeId: string | null,
memoryLimit?: number,
maxNodes?: number,
@@ -60,35 +69,34 @@ export function useGraphData(
})
// Filter by space and prepare documents
- let processedDocs = sortedDocs
- .map((doc) => {
- let memories =
- selectedSpace === "all"
- ? doc.memoryEntries
- : doc.memoryEntries.filter(
- (memory) =>
- (memory.spaceContainerTag ?? memory.spaceId ?? "default") ===
- selectedSpace,
- )
-
- // Sort memories by relevance score (if available) or recency
- memories = memories.sort((a, b) => {
- // Prioritize sourceRelevanceScore if available
- if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
- return b.sourceRelevanceScore - a.sourceRelevanceScore // Higher score first
- }
- // Fall back to most recent
- const dateA = new Date(a.updatedAt || a.createdAt).getTime()
- const dateB = new Date(b.updatedAt || b.createdAt).getTime()
- return dateB - dateA // Most recent first
- })
-
- return {
- ...doc,
- memoryEntries: memories,
+ let processedDocs = sortedDocs.map((doc) => {
+ let memories =
+ selectedSpace === "all"
+ ? doc.memoryEntries
+ : doc.memoryEntries.filter(
+ (memory) =>
+ (memory.spaceContainerTag ?? memory.spaceId ?? "default") ===
+ selectedSpace,
+ )
+
+ // Sort memories by relevance score (if available) or recency
+ memories = memories.sort((a, b) => {
+ // Prioritize sourceRelevanceScore if available
+ if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
+ return b.sourceRelevanceScore - a.sourceRelevanceScore // Higher score first
}
+ // Fall back to most recent
+ const dateA = new Date(a.updatedAt || a.createdAt).getTime()
+ const dateB = new Date(b.updatedAt || b.createdAt).getTime()
+ return dateB - dateA // Most recent first
})
+ return {
+ ...doc,
+ memoryEntries: memories,
+ }
+ })
+
// Apply maxNodes limit using Option B (dynamic cap per document)
if (maxNodes && maxNodes > 0) {
const totalDocs = processedDocs.length
@@ -112,37 +120,57 @@ export function useGraphData(
// If we still have budget left, distribute remaining nodes to first docs
let remainingBudget = maxNodes - totalNodes
if (remainingBudget > 0) {
- for (let i = 0; i < processedDocs.length && remainingBudget > 0; i++) {
+ for (
+ let i = 0;
+ i < processedDocs.length && remainingBudget > 0;
+ i++
+ ) {
const doc = processedDocs[i]
if (!doc) continue
- const originalDoc = sortedDocs.find(d => d.id === doc.id)
+ const originalDoc = sortedDocs.find((d) => d.id === doc.id)
if (!originalDoc) continue
const currentMemCount = doc.memoryEntries.length
const originalMemCount = originalDoc.memoryEntries.filter(
- m => selectedSpace === "all" ||
- (m.spaceContainerTag ?? m.spaceId ?? "default") === selectedSpace
+ (m) =>
+ selectedSpace === "all" ||
+ (m.spaceContainerTag ?? m.spaceId ?? "default") ===
+ selectedSpace,
).length
// Can we add more memories to this doc?
const canAdd = originalMemCount - currentMemCount
if (canAdd > 0) {
const toAdd = Math.min(canAdd, remainingBudget)
- const additionalMems = doc.memoryEntries.slice(0, currentMemCount + toAdd)
+ const additionalMems = doc.memoryEntries.slice(
+ 0,
+ currentMemCount + toAdd,
+ )
processedDocs[i] = {
...doc,
memoryEntries: originalDoc.memoryEntries
- .filter(m => selectedSpace === "all" ||
- (m.spaceContainerTag ?? m.spaceId ?? "default") === selectedSpace)
+ .filter(
+ (m) =>
+ selectedSpace === "all" ||
+ (m.spaceContainerTag ?? m.spaceId ?? "default") ===
+ selectedSpace,
+ )
.sort((a, b) => {
- if (a.sourceRelevanceScore != null && b.sourceRelevanceScore != null) {
+ if (
+ a.sourceRelevanceScore != null &&
+ b.sourceRelevanceScore != null
+ ) {
return b.sourceRelevanceScore - a.sourceRelevanceScore
}
- const dateA = new Date(a.updatedAt || a.createdAt).getTime()
- const dateB = new Date(b.updatedAt || b.createdAt).getTime()
+ const dateA = new Date(
+ a.updatedAt || a.createdAt,
+ ).getTime()
+ const dateB = new Date(
+ b.updatedAt || b.createdAt,
+ ).getTime()
return dateB - dateA
})
- .slice(0, currentMemCount + toAdd)
+ .slice(0, currentMemCount + toAdd),
}
remainingBudget -= toAdd
}
@@ -238,8 +266,7 @@ export function useGraphData(
})
// Enhanced Layout with Space Separation
- const { centerX, centerY, clusterRadius } =
- LAYOUT_CONSTANTS
+ const { centerX, centerY, clusterRadius } = LAYOUT_CONSTANTS
/* 1. Build DOCUMENT nodes with space-aware clustering */
const documentNodes: GraphNode[] = []
@@ -255,8 +282,10 @@ export function useGraphData(
// Loose grid spacing - physics will organize it better
const spacing = 200
- const defaultX = centerX + (col - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
- const defaultY = centerY + (row - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
+ const defaultX =
+ centerX + (col - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
+ const defaultY =
+ centerY + (row - gridSize / 2) * spacing + (Math.random() - 0.5) * 50
const customPos = nodePositions.get(doc.id)
@@ -294,7 +323,7 @@ export function useGraphData(
// D3-force will handle collision avoidance and spacing dynamically
allNodes.push(...documentNodes)
-
+
/* 3. Add memories around documents WITH doc-memory connections */
documentNodes.forEach((docNode) => {
const memoryNodeMap = new Map<string, GraphNode>()
@@ -318,9 +347,11 @@ export function useGraphData(
if (customMemPos) {
// If memory was manually positioned and has stored offset relative to parent
- if (customMemPos.parentDocId === docNode.id &&
+ if (
+ customMemPos.parentDocId === docNode.id &&
customMemPos.offsetX !== undefined &&
- customMemPos.offsetY !== undefined) {
+ customMemPos.offsetY !== undefined
+ ) {
// Apply the stored offset to the current document position
finalMemX = docNode.x + customMemPos.offsetX
finalMemY = docNode.y + customMemPos.offsetY
@@ -386,7 +417,8 @@ export function useGraphData(
data.documents.forEach((doc) => {
doc.memoryEntries.forEach((mem: MemoryEntry) => {
// Support both new object structure and legacy array/single parent fields
- let parentRelations: Record<string, MemoryRelation> = (mem.memoryRelations ?? {}) as Record<string, MemoryRelation>
+ let parentRelations: Record<string, MemoryRelation> =
+ (mem.memoryRelations ?? {}) as Record<string, MemoryRelation>
if (
mem.memoryRelations &&
@@ -436,4 +468,4 @@ export function useGraphData(
return { nodes: allNodes, edges: allEdges }
}, [data, filteredDocuments, nodePositions, draggingNodeId, similarityEdges])
-} \ No newline at end of file
+}
diff --git a/packages/memory-graph/src/hooks/use-graph-interactions.ts b/packages/memory-graph/src/hooks/use-graph-interactions.ts
index bcf0f5dd..d9044ad3 100644
--- a/packages/memory-graph/src/hooks/use-graph-interactions.ts
+++ b/packages/memory-graph/src/hooks/use-graph-interactions.ts
@@ -24,7 +24,16 @@ export function useGraphInteractions(
nodeY: 0,
})
const [nodePositions, setNodePositions] = useState<
- Map<string, { x: number; y: number; parentDocId?: string; offsetX?: number; offsetY?: number }>
+ Map<
+ string,
+ {
+ x: number
+ y: number
+ parentDocId?: string
+ offsetX?: number
+ offsetY?: number
+ }
+ >
>(new Map())
// Touch gesture state
@@ -125,8 +134,11 @@ export function useGraphInteractions(
// For memory nodes, find the parent document and store relative offset
const memoryData = draggedNode.data as any // MemoryEntry type
const parentDoc = nodes?.find(
- (n) => n.type === "document" &&
- (n.data as any).memoryEntries?.some((m: any) => m.id === memoryData.id)
+ (n) =>
+ n.type === "document" &&
+ (n.data as any).memoryEntries?.some(
+ (m: any) => m.id === memoryData.id,
+ ),
)
if (parentDoc) {
@@ -140,7 +152,7 @@ export function useGraphInteractions(
y: newY,
parentDocId: parentDoc.id,
offsetX,
- offsetY
+ offsetY,
}),
)
return
diff --git a/packages/tools/src/claude-memory-simple-example.ts b/packages/tools/src/claude-memory-simple-example.ts
index 8ce21f83..f0752372 100644
--- a/packages/tools/src/claude-memory-simple-example.ts
+++ b/packages/tools/src/claude-memory-simple-example.ts
@@ -4,68 +4,68 @@
* Shows the cleanest way to integrate Claude's memory tool with supermemory
*/
-import Anthropic from '@anthropic-ai/sdk'
-import { createClaudeMemoryTool } from './claude-memory'
+import Anthropic from "@anthropic-ai/sdk"
+import { createClaudeMemoryTool } from "./claude-memory"
const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY!,
+ apiKey: process.env.ANTHROPIC_API_KEY!,
})
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
- projectId: 'my-app',
+ projectId: "my-app",
})
async function chatWithMemory(userMessage: string) {
- // Send message to Claude with memory tool
- const response = await anthropic.beta.messages.create({
- model: 'claude-sonnet-4-5',
- max_tokens: 2048,
- messages: [{ role: 'user', content: userMessage }],
- tools: [{ type: 'memory_20250818', name: 'memory' }],
- betas: ['context-management-2025-06-27'],
- })
+ // Send message to Claude with memory tool
+ const response = await anthropic.beta.messages.create({
+ model: "claude-sonnet-4-5",
+ max_tokens: 2048,
+ messages: [{ role: "user", content: userMessage }],
+ tools: [{ type: "memory_20250818", name: "memory" }],
+ betas: ["context-management-2025-06-27"],
+ })
- // Handle any memory tool calls
- const toolResults = []
- for (const block of response.content) {
- if (block.type === 'tool_use' && block.name === 'memory') {
- const toolResult = await memoryTool.handleCommandForToolResult(
- block.input as any,
- block.id
- )
- toolResults.push(toolResult)
- }
- }
+ // Handle any memory tool calls
+ const toolResults = []
+ for (const block of response.content) {
+ if (block.type === "tool_use" && block.name === "memory") {
+ const toolResult = await memoryTool.handleCommandForToolResult(
+ block.input as any,
+ block.id,
+ )
+ toolResults.push(toolResult)
+ }
+ }
- // Send tool results back to Claude if needed
- if (toolResults.length > 0) {
- const finalResponse = await anthropic.beta.messages.create({
- model: 'claude-sonnet-4-5',
- max_tokens: 2048,
- messages: [
- { role: 'user', content: userMessage },
- { role: 'assistant', content: response.content },
- { role: 'user', content: toolResults },
- ],
- tools: [{ type: 'memory_20250818', name: 'memory' }],
- betas: ['context-management-2025-06-27'],
- })
+ // Send tool results back to Claude if needed
+ if (toolResults.length > 0) {
+ const finalResponse = await anthropic.beta.messages.create({
+ model: "claude-sonnet-4-5",
+ max_tokens: 2048,
+ messages: [
+ { role: "user", content: userMessage },
+ { role: "assistant", content: response.content },
+ { role: "user", content: toolResults },
+ ],
+ tools: [{ type: "memory_20250818", name: "memory" }],
+ betas: ["context-management-2025-06-27"],
+ })
- return finalResponse
- }
+ return finalResponse
+ }
- return response
+ return response
}
// Example usage
async function example() {
- const response = await chatWithMemory(
- "Remember that I prefer React with TypeScript for my projects"
- )
+ const response = await chatWithMemory(
+ "Remember that I prefer React with TypeScript for my projects",
+ )
- console.log(response.content[0])
+ console.log(response.content[0])
}
if (import.meta.main) {
- example()
-} \ No newline at end of file
+ example()
+}
diff --git a/packages/tools/test-supermemory.ts b/packages/tools/test-supermemory.ts
index 5a96acc0..b3863c7e 100644
--- a/packages/tools/test-supermemory.ts
+++ b/packages/tools/test-supermemory.ts
@@ -32,12 +32,13 @@ async function testResponses() {
console.log("\n=== Testing Responses API ===")
const response = await openaiWithSupermemory.chat.completions.create({
model: "gpt-4o",
- messages: [
- { role: "user", content: "what's my favoritge color?" },
- ],
+ messages: [{ role: "user", content: "what's my favoritge color?" }],
})
- console.log("Response:", JSON.stringify(response.choices[0]?.message.content, null, 2))
+ console.log(
+ "Response:",
+ JSON.stringify(response.choices[0]?.message.content, null, 2),
+ )
}
// Run tests
diff --git a/packages/tools/test/ai-sdk-test.ts b/packages/tools/test/ai-sdk-test.ts
index 99a94618..3d9f13ea 100644
--- a/packages/tools/test/ai-sdk-test.ts
+++ b/packages/tools/test/ai-sdk-test.ts
@@ -14,4 +14,4 @@ const result = await generateText({
messages: [{ role: "user", content: "Where do i live?" }],
})
-console.log(result.text) \ No newline at end of file
+console.log(result.text)
diff --git a/packages/tools/test/anthropic-example.ts b/packages/tools/test/anthropic-example.ts
index 039f87ac..cfb82ddb 100644
--- a/packages/tools/test/anthropic-example.ts
+++ b/packages/tools/test/anthropic-example.ts
@@ -4,257 +4,281 @@
* Shows how to use the memory tool with the official Anthropic SDK
*/
-import Anthropic from '@anthropic-ai/sdk'
-import { createClaudeMemoryTool } from './claude-memory'
-import 'dotenv/config'
+import Anthropic from "@anthropic-ai/sdk"
+import { createClaudeMemoryTool } from "./claude-memory"
+import "dotenv/config"
/**
* Handle Claude's memory tool calls using the Anthropic SDK
*/
async function chatWithMemoryTool() {
- console.log('๐Ÿค– Anthropic SDK Example - Claude with Memory Tool')
- console.log('=' .repeat(50))
-
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY
- const SUPERMEMORY_API_KEY = process.env.SUPERMEMORY_API_KEY
-
- if (!ANTHROPIC_API_KEY || !SUPERMEMORY_API_KEY) {
- console.error('โŒ Missing required API keys:')
- console.error('- ANTHROPIC_API_KEY')
- console.error('- SUPERMEMORY_API_KEY')
- return
- }
-
- // Initialize Anthropic client
- const anthropic = new Anthropic({
- apiKey: ANTHROPIC_API_KEY,
- })
-
- // Initialize memory tool
- const memoryTool = createClaudeMemoryTool(SUPERMEMORY_API_KEY, {
- projectId: 'anthropic-sdk-demo',
- memoryContainerTag: 'claude_memory_anthropic',
- })
-
- // Conversation messages
- const messages: Anthropic.Messages.MessageParam[] = [
- {
- role: 'user',
- content: "Hi Claude! I'm working on a new React project using TypeScript and I want you to remember my preferences. Can you help me debug some code later?",
- },
- ]
-
- console.log('๐Ÿ’ฌ User:', messages[0].content)
- console.log('\n๐Ÿ”„ Sending to Claude with memory tool...')
-
- try {
- // Make initial request to Claude with memory tool
- const response = await anthropic.beta.messages.create({
- model: 'claude-sonnet-4-5',
- max_tokens: 2048,
- messages: messages,
- tools: [
- {
- type: 'memory_20250818',
- name: 'memory',
- },
- ],
- betas: ['context-management-2025-06-27'],
- })
-
- console.log('๐Ÿ“ฅ Claude responded:')
-
- // Process the response
- let toolResults: Anthropic.Messages.ToolResultBlockParam[] = []
-
- for (const block of response.content) {
- if (block.type === 'text') {
- console.log('๐Ÿ’ญ', block.text)
- } else if (block.type === 'tool_use' && block.name === 'memory') {
- console.log('๐Ÿ”ง Claude is using memory tool:')
- console.log(' Command:', block.input.command)
- console.log(' Path:', block.input.path)
-
- // Handle the memory tool call
- const memoryResult = await memoryTool.handleCommand(block.input as any)
-
- const toolResult: Anthropic.Messages.ToolResultBlockParam = {
- type: 'tool_result',
- tool_use_id: block.id,
- content: memoryResult.success
- ? memoryResult.content || 'Operation completed successfully'
- : `Error: ${memoryResult.error}`,
- is_error: !memoryResult.success,
- }
-
- toolResults.push(toolResult)
-
- console.log('๐Ÿ“Š Memory operation result:', memoryResult.success ? 'โœ… Success' : 'โŒ Failed')
- if (memoryResult.content) {
- console.log('๐Ÿ“„ Content preview:', memoryResult.content.substring(0, 100) + '...')
- }
- }
- }
-
- // If Claude used memory tools, send the results back
- if (toolResults.length > 0) {
- console.log('\n๐Ÿ”„ Sending tool results back to Claude...')
-
- // Add Claude's response to conversation
- messages.push({
- role: 'assistant',
- content: response.content,
- })
-
- // Add tool results
- messages.push({
- role: 'user',
- content: toolResults,
- })
-
- const finalResponse = await anthropic.beta.messages.create({
- model: 'claude-sonnet-4-5',
- max_tokens: 2048,
- messages: messages,
- tools: [
- {
- type: 'memory_20250818',
- name: 'memory',
- },
- ],
- betas: ['context-management-2025-06-27'],
- })
-
- console.log('๐Ÿ“ฅ Claude\'s final response:')
-
- for (const block of finalResponse.content) {
- if (block.type === 'text') {
- console.log('๐Ÿ’ญ', block.text)
- } else if (block.type === 'tool_use' && block.name === 'memory') {
- console.log('๐Ÿ”ง Claude is using memory tool again:')
- console.log(' Command:', block.input.command)
- console.log(' Path:', block.input.path)
-
- // Handle additional memory tool calls
- const memoryResult = await memoryTool.handleCommand(block.input as any)
- console.log('๐Ÿ“Š Memory operation result:', memoryResult.success ? 'โœ… Success' : 'โŒ Failed')
- }
- }
- }
-
- console.log('\nโœจ Conversation completed!')
- console.log('\n๐Ÿ“‹ Usage statistics:')
- console.log('- Input tokens:', response.usage.input_tokens)
- console.log('- Output tokens:', response.usage.output_tokens)
- console.log('- Memory operations:', toolResults.length)
-
- } catch (error) {
- console.error('โŒ Error:', error)
- }
+ console.log("๐Ÿค– Anthropic SDK Example - Claude with Memory Tool")
+ console.log("=".repeat(50))
+
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY
+ const SUPERMEMORY_API_KEY = process.env.SUPERMEMORY_API_KEY
+
+ if (!ANTHROPIC_API_KEY || !SUPERMEMORY_API_KEY) {
+ console.error("โŒ Missing required API keys:")
+ console.error("- ANTHROPIC_API_KEY")
+ console.error("- SUPERMEMORY_API_KEY")
+ return
+ }
+
+ // Initialize Anthropic client
+ const anthropic = new Anthropic({
+ apiKey: ANTHROPIC_API_KEY,
+ })
+
+ // Initialize memory tool
+ const memoryTool = createClaudeMemoryTool(SUPERMEMORY_API_KEY, {
+ projectId: "anthropic-sdk-demo",
+ memoryContainerTag: "claude_memory_anthropic",
+ })
+
+ // Conversation messages
+ const messages: Anthropic.Messages.MessageParam[] = [
+ {
+ role: "user",
+ content:
+ "Hi Claude! I'm working on a new React project using TypeScript and I want you to remember my preferences. Can you help me debug some code later?",
+ },
+ ]
+
+ console.log("๐Ÿ’ฌ User:", messages[0].content)
+ console.log("\n๐Ÿ”„ Sending to Claude with memory tool...")
+
+ try {
+ // Make initial request to Claude with memory tool
+ const response = await anthropic.beta.messages.create({
+ model: "claude-sonnet-4-5",
+ max_tokens: 2048,
+ messages: messages,
+ tools: [
+ {
+ type: "memory_20250818",
+ name: "memory",
+ },
+ ],
+ betas: ["context-management-2025-06-27"],
+ })
+
+ console.log("๐Ÿ“ฅ Claude responded:")
+
+ // Process the response
+ const toolResults: Anthropic.Messages.ToolResultBlockParam[] = []
+
+ for (const block of response.content) {
+ if (block.type === "text") {
+ console.log("๐Ÿ’ญ", block.text)
+ } else if (block.type === "tool_use" && block.name === "memory") {
+ console.log("๐Ÿ”ง Claude is using memory tool:")
+ console.log(" Command:", block.input.command)
+ console.log(" Path:", block.input.path)
+
+ // Handle the memory tool call
+ const memoryResult = await memoryTool.handleCommand(block.input as any)
+
+ const toolResult: Anthropic.Messages.ToolResultBlockParam = {
+ type: "tool_result",
+ tool_use_id: block.id,
+ content: memoryResult.success
+ ? memoryResult.content || "Operation completed successfully"
+ : `Error: ${memoryResult.error}`,
+ is_error: !memoryResult.success,
+ }
+
+ toolResults.push(toolResult)
+
+ console.log(
+ "๐Ÿ“Š Memory operation result:",
+ memoryResult.success ? "โœ… Success" : "โŒ Failed",
+ )
+ if (memoryResult.content) {
+ console.log(
+ "๐Ÿ“„ Content preview:",
+ memoryResult.content.substring(0, 100) + "...",
+ )
+ }
+ }
+ }
+
+ // If Claude used memory tools, send the results back
+ if (toolResults.length > 0) {
+ console.log("\n๐Ÿ”„ Sending tool results back to Claude...")
+
+ // Add Claude's response to conversation
+ messages.push({
+ role: "assistant",
+ content: response.content,
+ })
+
+ // Add tool results
+ messages.push({
+ role: "user",
+ content: toolResults,
+ })
+
+ const finalResponse = await anthropic.beta.messages.create({
+ model: "claude-sonnet-4-5",
+ max_tokens: 2048,
+ messages: messages,
+ tools: [
+ {
+ type: "memory_20250818",
+ name: "memory",
+ },
+ ],
+ betas: ["context-management-2025-06-27"],
+ })
+
+ console.log("๐Ÿ“ฅ Claude's final response:")
+
+ for (const block of finalResponse.content) {
+ if (block.type === "text") {
+ console.log("๐Ÿ’ญ", block.text)
+ } else if (block.type === "tool_use" && block.name === "memory") {
+ console.log("๐Ÿ”ง Claude is using memory tool again:")
+ console.log(" Command:", block.input.command)
+ console.log(" Path:", block.input.path)
+
+ // Handle additional memory tool calls
+ const memoryResult = await memoryTool.handleCommand(
+ block.input as any,
+ )
+ console.log(
+ "๐Ÿ“Š Memory operation result:",
+ memoryResult.success ? "โœ… Success" : "โŒ Failed",
+ )
+ }
+ }
+ }
+
+ console.log("\nโœจ Conversation completed!")
+ console.log("\n๐Ÿ“‹ Usage statistics:")
+ console.log("- Input tokens:", response.usage.input_tokens)
+ console.log("- Output tokens:", response.usage.output_tokens)
+ console.log("- Memory operations:", toolResults.length)
+ } catch (error) {
+ console.error("โŒ Error:", error)
+ }
}
/**
* Test memory tool operations directly
*/
async function testMemoryOperations() {
- console.log('\n๐Ÿงช Testing Memory Operations Directly')
- console.log('=' .repeat(50))
-
- if (!process.env.SUPERMEMORY_API_KEY) {
- console.error('โŒ SUPERMEMORY_API_KEY is required')
- return
- }
-
- const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
- projectId: 'direct-test',
- memoryContainerTag: 'claude_memory_direct',
- })
-
- const testCases = [
- {
- name: 'View empty memory directory',
- command: { command: 'view' as const, path: '/memories' },
- },
- {
- name: 'Create user preferences file',
- command: {
- command: 'create' as const,
- path: '/memories/user-preferences.md',
- file_text: '# User Preferences\n\n- Prefers React with TypeScript\n- Likes clean, readable code\n- Uses functional programming style\n- Prefers ESLint and Prettier for code formatting',
- },
- },
- {
- name: 'Create project notes',
- command: {
- command: 'create' as const,
- path: '/memories/project-notes.txt',
- file_text: 'Current Project: React TypeScript App\n\nFeatures to implement:\n1. User authentication\n2. Dashboard with widgets\n3. Data visualization\n4. Export functionality\n\nTech stack:\n- React 18\n- TypeScript\n- Vite\n- Tailwind CSS',
- },
- },
- {
- name: 'List directory contents',
- command: { command: 'view' as const, path: '/memories/' },
- },
- {
- name: 'Read user preferences',
- command: { command: 'view' as const, path: '/memories/user-preferences.md' },
- },
- {
- name: 'Update preferences (add VS Code)',
- command: {
- command: 'str_replace' as const,
- path: '/memories/user-preferences.md',
- old_str: '- Prefers ESLint and Prettier for code formatting',
- new_str: '- Prefers ESLint and Prettier for code formatting\n- Uses VS Code as primary editor\n- Likes GitHub Copilot for code completion',
- },
- },
- {
- name: 'Insert new task in project notes',
- command: {
- command: 'insert' as const,
- path: '/memories/project-notes.txt',
- insert_line: 6,
- insert_text: '5. Unit testing with Jest and React Testing Library',
- },
- },
- {
- name: 'Read updated project notes',
- command: { command: 'view' as const, path: '/memories/project-notes.txt', view_range: [4, 8] },
- },
- ]
-
- for (const testCase of testCases) {
- console.log(`\n๐Ÿ” ${testCase.name}`)
- try {
- const result = await memoryTool.handleCommand(testCase.command)
-
- if (result.success) {
- console.log('โœ… Success')
- if (result.content && result.content.length <= 200) {
- console.log('๐Ÿ“„ Result:', result.content)
- } else if (result.content) {
- console.log('๐Ÿ“„ Result:', result.content.substring(0, 150) + '... (truncated)')
- }
- } else {
- console.log('โŒ Failed')
- console.log('๐Ÿ“„ Error:', result.error)
- }
- } catch (error) {
- console.log('๐Ÿ’ฅ Exception:', error)
- }
-
- // Small delay to avoid rate limiting
- await new Promise(resolve => setTimeout(resolve, 300))
- }
+ console.log("\n๐Ÿงช Testing Memory Operations Directly")
+ console.log("=".repeat(50))
+
+ if (!process.env.SUPERMEMORY_API_KEY) {
+ console.error("โŒ SUPERMEMORY_API_KEY is required")
+ return
+ }
+
+ const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY, {
+ projectId: "direct-test",
+ memoryContainerTag: "claude_memory_direct",
+ })
+
+ const testCases = [
+ {
+ name: "View empty memory directory",
+ command: { command: "view" as const, path: "/memories" },
+ },
+ {
+ name: "Create user preferences file",
+ command: {
+ command: "create" as const,
+ path: "/memories/user-preferences.md",
+ file_text:
+ "# User Preferences\n\n- Prefers React with TypeScript\n- Likes clean, readable code\n- Uses functional programming style\n- Prefers ESLint and Prettier for code formatting",
+ },
+ },
+ {
+ name: "Create project notes",
+ command: {
+ command: "create" as const,
+ path: "/memories/project-notes.txt",
+ file_text:
+ "Current Project: React TypeScript App\n\nFeatures to implement:\n1. User authentication\n2. Dashboard with widgets\n3. Data visualization\n4. Export functionality\n\nTech stack:\n- React 18\n- TypeScript\n- Vite\n- Tailwind CSS",
+ },
+ },
+ {
+ name: "List directory contents",
+ command: { command: "view" as const, path: "/memories/" },
+ },
+ {
+ name: "Read user preferences",
+ command: {
+ command: "view" as const,
+ path: "/memories/user-preferences.md",
+ },
+ },
+ {
+ name: "Update preferences (add VS Code)",
+ command: {
+ command: "str_replace" as const,
+ path: "/memories/user-preferences.md",
+ old_str: "- Prefers ESLint and Prettier for code formatting",
+ new_str:
+ "- Prefers ESLint and Prettier for code formatting\n- Uses VS Code as primary editor\n- Likes GitHub Copilot for code completion",
+ },
+ },
+ {
+ name: "Insert new task in project notes",
+ command: {
+ command: "insert" as const,
+ path: "/memories/project-notes.txt",
+ insert_line: 6,
+ insert_text: "5. Unit testing with Jest and React Testing Library",
+ },
+ },
+ {
+ name: "Read updated project notes",
+ command: {
+ command: "view" as const,
+ path: "/memories/project-notes.txt",
+ view_range: [4, 8],
+ },
+ },
+ ]
+
+ for (const testCase of testCases) {
+ console.log(`\n๐Ÿ” ${testCase.name}`)
+ try {
+ const result = await memoryTool.handleCommand(testCase.command)
+
+ if (result.success) {
+ console.log("โœ… Success")
+ if (result.content && result.content.length <= 200) {
+ console.log("๐Ÿ“„ Result:", result.content)
+ } else if (result.content) {
+ console.log(
+ "๐Ÿ“„ Result:",
+ result.content.substring(0, 150) + "... (truncated)",
+ )
+ }
+ } else {
+ console.log("โŒ Failed")
+ console.log("๐Ÿ“„ Error:", result.error)
+ }
+ } catch (error) {
+ console.log("๐Ÿ’ฅ Exception:", error)
+ }
+
+ // Small delay to avoid rate limiting
+ await new Promise((resolve) => setTimeout(resolve, 300))
+ }
}
// Run the examples
async function main() {
- await testMemoryOperations()
- console.log('\n' + '=' .repeat(70) + '\n')
- await chatWithMemoryTool()
+ await testMemoryOperations()
+ console.log("\n" + "=".repeat(70) + "\n")
+ await chatWithMemoryTool()
}
if (import.meta.main) {
- main().catch(console.error)
-} \ No newline at end of file
+ main().catch(console.error)
+}
diff --git a/packages/tools/test/chatapp/app/layout.tsx b/packages/tools/test/chatapp/app/layout.tsx
index f7fa87eb..bb3b3e84 100644
--- a/packages/tools/test/chatapp/app/layout.tsx
+++ b/packages/tools/test/chatapp/app/layout.tsx
@@ -1,34 +1,34 @@
-import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import "./globals.css";
+import type { Metadata } from "next"
+import { Geist, Geist_Mono } from "next/font/google"
+import "./globals.css"
const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+})
const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+})
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
-};
+ title: "Create Next App",
+ description: "Generated by create next app",
+}
export default function RootLayout({
- children,
+ children,
}: Readonly<{
- children: React.ReactNode;
+ children: React.ReactNode
}>) {
- return (
- <html lang="en">
- <body
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
- >
- {children}
- </body>
- </html>
- );
+ return (
+ <html lang="en">
+ <body
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
+ >
+ {children}
+ </body>
+ </html>
+ )
}
diff --git a/packages/tools/test/chatapp/app/openai-chat/new/page.tsx b/packages/tools/test/chatapp/app/openai-chat/new/page.tsx
index fb168085..bf04cd6e 100644
--- a/packages/tools/test/chatapp/app/openai-chat/new/page.tsx
+++ b/packages/tools/test/chatapp/app/openai-chat/new/page.tsx
@@ -7,4 +7,4 @@ function generateId() {
export default function NewChatPage() {
const chatId = generateId()
redirect(`/openai-chat/${chatId}`)
-} \ No newline at end of file
+}
diff --git a/packages/tools/test/chatapp/eslint.config.mjs b/packages/tools/test/chatapp/eslint.config.mjs
index 05e726d1..afd7eab0 100644
--- a/packages/tools/test/chatapp/eslint.config.mjs
+++ b/packages/tools/test/chatapp/eslint.config.mjs
@@ -1,18 +1,18 @@
-import { defineConfig, globalIgnores } from "eslint/config";
-import nextVitals from "eslint-config-next/core-web-vitals";
-import nextTs from "eslint-config-next/typescript";
+import { defineConfig, globalIgnores } from "eslint/config"
+import nextVitals from "eslint-config-next/core-web-vitals"
+import nextTs from "eslint-config-next/typescript"
const eslintConfig = defineConfig([
- ...nextVitals,
- ...nextTs,
- // Override default ignores of eslint-config-next.
- globalIgnores([
- // Default ignores of eslint-config-next:
- ".next/**",
- "out/**",
- "build/**",
- "next-env.d.ts",
- ]),
-]);
+ ...nextVitals,
+ ...nextTs,
+ // Override default ignores of eslint-config-next.
+ globalIgnores([
+ // Default ignores of eslint-config-next:
+ ".next/**",
+ "out/**",
+ "build/**",
+ "next-env.d.ts",
+ ]),
+])
-export default eslintConfig;
+export default eslintConfig
diff --git a/packages/tools/test/chatapp/next.config.ts b/packages/tools/test/chatapp/next.config.ts
index e9ffa308..9da1646d 100644
--- a/packages/tools/test/chatapp/next.config.ts
+++ b/packages/tools/test/chatapp/next.config.ts
@@ -1,7 +1,7 @@
-import type { NextConfig } from "next";
+import type { NextConfig } from "next"
const nextConfig: NextConfig = {
- /* config options here */
-};
+ /* config options here */
+}
-export default nextConfig;
+export default nextConfig
diff --git a/packages/tools/test/chatapp/postcss.config.mjs b/packages/tools/test/chatapp/postcss.config.mjs
index 61e36849..d8af3e5d 100644
--- a/packages/tools/test/chatapp/postcss.config.mjs
+++ b/packages/tools/test/chatapp/postcss.config.mjs
@@ -1,7 +1,7 @@
const config = {
- plugins: {
- "@tailwindcss/postcss": {},
- },
-};
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+}
-export default config;
+export default config
diff --git a/packages/tools/test/chatapp/tsconfig.json b/packages/tools/test/chatapp/tsconfig.json
index 3a13f90a..6b4686ac 100644
--- a/packages/tools/test/chatapp/tsconfig.json
+++ b/packages/tools/test/chatapp/tsconfig.json
@@ -1,34 +1,34 @@
{
- "compilerOptions": {
- "target": "ES2017",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "react-jsx",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": ["./*"]
- }
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts",
- ".next/dev/types/**/*.ts",
- "**/*.mts"
- ],
- "exclude": ["node_modules"]
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
}
diff --git a/packages/tools/test/claude-memory-examples.ts b/packages/tools/test/claude-memory-examples.ts
index b0602938..6921d151 100644
--- a/packages/tools/test/claude-memory-examples.ts
+++ b/packages/tools/test/claude-memory-examples.ts
@@ -17,7 +17,7 @@ import { createClaudeMemoryTool, type MemoryCommand } from "./claude-memory"
*/
export async function directFetchExample() {
console.log("๐Ÿš€ Direct Fetch Example - Claude Memory Tool")
- console.log("=" .repeat(50))
+ console.log("=".repeat(50))
// Initialize the memory tool
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
@@ -30,7 +30,8 @@ export async function directFetchExample() {
{
command: "create",
path: "/memories/project-notes.md",
- file_text: "# Project Notes\n\n## Meeting with Client\n- Discussed requirements\n- Set deadline for next week\n- Need to follow up on budget\n\n## Technical Notes\n- Use React for frontend\n- Node.js backend\n- PostgreSQL database",
+ file_text:
+ "# Project Notes\n\n## Meeting with Client\n- Discussed requirements\n- Set deadline for next week\n- Need to follow up on budget\n\n## Technical Notes\n- Use React for frontend\n- Node.js backend\n- PostgreSQL database",
},
{
command: "view",
@@ -56,7 +57,8 @@ export async function directFetchExample() {
{
command: "create",
path: "/memories/todo.txt",
- file_text: "TODO List:\n1. Set up development environment\n2. Create project structure\n3. Implement authentication\n4. Build user dashboard",
+ file_text:
+ "TODO List:\n1. Set up development environment\n2. Create project structure\n3. Implement authentication\n4. Build user dashboard",
},
{
command: "view",
@@ -67,7 +69,9 @@ export async function directFetchExample() {
// Execute each command
for (let i = 0; i < commands.length; i++) {
const command = commands[i]
- console.log(`\n๐Ÿ“ Step ${i + 1}: ${command.command.toUpperCase()} ${command.path}`)
+ console.log(
+ `\n๐Ÿ“ Step ${i + 1}: ${command.command.toUpperCase()} ${command.path}`,
+ )
try {
const result = await memoryTool.handleCommand(command)
@@ -98,7 +102,7 @@ export async function directFetchExample() {
*/
export async function anthropicSdkExample() {
console.log("๐Ÿค– Anthropic SDK Example - Claude Memory Tool")
- console.log("=" .repeat(50))
+ console.log("=".repeat(50))
// Initialize memory tool
const memoryTool = createClaudeMemoryTool(process.env.SUPERMEMORY_API_KEY!, {
@@ -110,20 +114,28 @@ export async function anthropicSdkExample() {
console.log("๐Ÿ—ฃ๏ธ Simulating Claude conversation with memory tool access...")
// Scenario: User asks Claude to remember something
- console.log("\nUser: 'Remember that I prefer React over Vue for frontend development'")
+ console.log(
+ "\nUser: 'Remember that I prefer React over Vue for frontend development'",
+ )
const rememberResult = await memoryTool.handleCommand({
command: "create",
path: "/memories/user-preferences.md",
- file_text: "# User Preferences\n\n## Frontend Development\n- Prefers React over Vue\n- Likes TypeScript for type safety",
+ file_text:
+ "# User Preferences\n\n## Frontend Development\n- Prefers React over Vue\n- Likes TypeScript for type safety",
})
console.log("๐Ÿค– Claude: 'I'll remember that preference for you.'")
- console.log("Memory operation result:", rememberResult.success ? "โœ… Stored" : "โŒ Failed")
+ console.log(
+ "Memory operation result:",
+ rememberResult.success ? "โœ… Stored" : "โŒ Failed",
+ )
// Scenario: User asks about their preferences later
console.log("\nUser: 'What frontend framework do I prefer?'")
- console.log("๐Ÿค– Claude: 'Let me check what I remember about your preferences...'")
+ console.log(
+ "๐Ÿค– Claude: 'Let me check what I remember about your preferences...'",
+ )
const recallResult = await memoryTool.handleCommand({
command: "view",
@@ -133,20 +145,27 @@ export async function anthropicSdkExample() {
if (recallResult.success) {
console.log("๐Ÿ“š Claude retrieved from memory:")
console.log(recallResult.content)
- console.log("\n๐Ÿค– Claude: 'Based on what I remember, you prefer React over Vue for frontend development!'")
+ console.log(
+ "\n๐Ÿค– Claude: 'Based on what I remember, you prefer React over Vue for frontend development!'",
+ )
}
// Scenario: User provides additional context
- console.log("\nUser: 'Actually, also add that I like using Tailwind CSS for styling'")
+ console.log(
+ "\nUser: 'Actually, also add that I like using Tailwind CSS for styling'",
+ )
await memoryTool.handleCommand({
command: "str_replace",
path: "/memories/user-preferences.md",
old_str: "- Likes TypeScript for type safety",
- new_str: "- Likes TypeScript for type safety\n- Prefers Tailwind CSS for styling",
+ new_str:
+ "- Likes TypeScript for type safety\n- Prefers Tailwind CSS for styling",
})
- console.log("๐Ÿค– Claude: 'I've updated my memory with your Tailwind CSS preference!'")
+ console.log(
+ "๐Ÿค– Claude: 'I've updated my memory with your Tailwind CSS preference!'",
+ )
// Scenario: Show current memory directory
console.log("\n๐Ÿค– Claude: 'Here's what I currently remember about you:'")
@@ -216,7 +235,7 @@ async function chatWithMemory(userMessage: string) {
// Example usage:
// await chatWithMemory("Remember that I'm working on a React project with TypeScript");
// await chatWithMemory("What programming languages am I using in my current project?");
-`;
+`
// =====================================================
// Example 4: cURL Commands for Testing
@@ -280,7 +299,7 @@ curl -X PATCH "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\
# 5. Delete a memory file
curl -X DELETE "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\
-H "Authorization: Bearer YOUR_API_KEY"
-`;
+`
// =====================================================
// Main runner function
@@ -288,30 +307,29 @@ curl -X DELETE "https://api.supermemory.ai/v3/documents/DOCUMENT_ID" \\
export async function runAllExamples() {
if (!process.env.SUPERMEMORY_API_KEY) {
- console.error("โŒ SUPERMEMORY_API_KEY environment variable is required");
- console.log("Set your API key in .env file or environment variable");
- return;
+ console.error("โŒ SUPERMEMORY_API_KEY environment variable is required")
+ console.log("Set your API key in .env file or environment variable")
+ return
}
try {
- await directFetchExample();
- console.log("\\n" + "=".repeat(70) + "\\n");
- await anthropicSdkExample();
+ await directFetchExample()
+ console.log("\\n" + "=".repeat(70) + "\\n")
+ await anthropicSdkExample()
- console.log("\\n" + "=".repeat(70));
- console.log("๐Ÿ“‹ Real Anthropic SDK Integration Template:");
- console.log(anthropicIntegrationTemplate);
-
- console.log("\\n" + "=".repeat(70));
- console.log("๐Ÿ”ง cURL Examples for Direct API Testing:");
- console.log(curlExamples);
+ console.log("\\n" + "=".repeat(70))
+ console.log("๐Ÿ“‹ Real Anthropic SDK Integration Template:")
+ console.log(anthropicIntegrationTemplate)
+ console.log("\\n" + "=".repeat(70))
+ console.log("๐Ÿ”ง cURL Examples for Direct API Testing:")
+ console.log(curlExamples)
} catch (error) {
- console.error("๐Ÿ’ฅ Error running examples:", error);
+ console.error("๐Ÿ’ฅ Error running examples:", error)
}
}
// Run examples if this file is executed directly
if (import.meta.main) {
- runAllExamples();
-} \ No newline at end of file
+ runAllExamples()
+}
diff --git a/packages/tools/test/claude-memory-real-example.ts b/packages/tools/test/claude-memory-real-example.ts
index 64b51ee3..06ebb546 100644
--- a/packages/tools/test/claude-memory-real-example.ts
+++ b/packages/tools/test/claude-memory-real-example.ts
@@ -25,9 +25,11 @@ export async function handleClaudeMemoryToolCall(
projectId?: string
memoryContainerTag?: string
baseUrl?: string
- }
+ },
) {
- console.log(`๐Ÿ”ง Handling Claude memory tool call: ${toolUseBlock.input.command}`)
+ console.log(
+ `๐Ÿ”ง Handling Claude memory tool call: ${toolUseBlock.input.command}`,
+ )
console.log(`๐Ÿ“ Path: ${toolUseBlock.input.path}`)
// Initialize memory tool
@@ -50,7 +52,10 @@ export async function handleClaudeMemoryToolCall(
is_error: !result.success,
}
- console.log(`${result.success ? 'โœ…' : 'โŒ'} Result:`, result.content || result.error)
+ console.log(
+ `${result.success ? "โœ…" : "โŒ"} Result:`,
+ result.content || result.error,
+ )
return toolResult
}
@@ -60,7 +65,7 @@ export async function handleClaudeMemoryToolCall(
*/
export async function realClaudeMemoryExample() {
console.log("๐Ÿค– Real Claude Memory Tool Integration")
- console.log("=" .repeat(50))
+ console.log("=".repeat(50))
// Your API keys
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY
@@ -79,14 +84,19 @@ export async function realClaudeMemoryExample() {
const initialRequest = {
model: "claude-sonnet-4-5",
max_tokens: 2048,
- messages: [{
- role: "user" as const,
- content: "I'm working on a Python web scraper that keeps crashing with a timeout error. Here's the problematic function:\\n\\n```python\\ndef fetch_page(url, retries=3):\\n for i in range(retries):\\n try:\\n response = requests.get(url, timeout=5)\\n return response.text\\n except requests.exceptions.Timeout:\\n if i == retries - 1:\\n raise\\n time.sleep(1)\\n```\\n\\nPlease help me debug this."
- }],
- tools: [{
- type: "memory_20250818" as const,
- name: "memory"
- }]
+ messages: [
+ {
+ role: "user" as const,
+ content:
+ "I'm working on a Python web scraper that keeps crashing with a timeout error. Here's the problematic function:\\n\\n```python\\ndef fetch_page(url, retries=3):\\n for i in range(retries):\\n try:\\n response = requests.get(url, timeout=5)\\n return response.text\\n except requests.exceptions.Timeout:\\n if i == retries - 1:\\n raise\\n time.sleep(1)\\n```\\n\\nPlease help me debug this.",
+ },
+ ],
+ tools: [
+ {
+ type: "memory_20250818" as const,
+ name: "memory",
+ },
+ ],
}
// Make the API call
@@ -96,9 +106,9 @@ export async function realClaudeMemoryExample() {
"x-api-key": ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
- "anthropic-beta": "context-management-2025-06-27"
+ "anthropic-beta": "context-management-2025-06-27",
},
- body: JSON.stringify(initialRequest)
+ body: JSON.stringify(initialRequest),
})
const responseData = await claudeResponse.json()
@@ -111,7 +121,7 @@ export async function realClaudeMemoryExample() {
if (responseData.content) {
for (const block of responseData.content) {
if (block.type === "tool_use" && block.name === "memory") {
- console.log(`\\n๐Ÿ”ง Processing memory tool call:`)
+ console.log("\\n๐Ÿ”ง Processing memory tool call:")
console.log(`Command: ${block.input.command}`)
console.log(`Path: ${block.input.path}`)
@@ -121,8 +131,8 @@ export async function realClaudeMemoryExample() {
SUPERMEMORY_API_KEY,
{
projectId: "python-scraper-help",
- memoryContainerTag: "claude_memory_debug"
- }
+ memoryContainerTag: "claude_memory_debug",
+ },
)
toolResults.push(toolResult)
@@ -141,26 +151,29 @@ export async function realClaudeMemoryExample() {
...initialRequest.messages,
{
role: "assistant" as const,
- content: responseData.content
+ content: responseData.content,
},
{
role: "user" as const,
- content: toolResults
- }
+ content: toolResults,
+ },
],
- tools: initialRequest.tools
+ tools: initialRequest.tools,
}
- const followUpResponse = await fetch("https://api.anthropic.com/v1/messages", {
- method: "POST",
- headers: {
- "x-api-key": ANTHROPIC_API_KEY,
- "anthropic-version": "2023-06-01",
- "content-type": "application/json",
- "anthropic-beta": "context-management-2025-06-27"
+ const followUpResponse = await fetch(
+ "https://api.anthropic.com/v1/messages",
+ {
+ method: "POST",
+ headers: {
+ "x-api-key": ANTHROPIC_API_KEY,
+ "anthropic-version": "2023-06-01",
+ "content-type": "application/json",
+ "anthropic-beta": "context-management-2025-06-27",
+ },
+ body: JSON.stringify(followUpRequest),
},
- body: JSON.stringify(followUpRequest)
- })
+ )
const followUpData = await followUpResponse.json()
console.log("๐Ÿ“ฅ Claude's final response:")
@@ -178,7 +191,7 @@ export async function processClaudeResponse(
projectId?: string
memoryContainerTag?: string
baseUrl?: string
- }
+ },
): Promise<any[]> {
const toolResults = []
@@ -188,7 +201,7 @@ export async function processClaudeResponse(
const toolResult = await handleClaudeMemoryToolCall(
block,
supermemoryApiKey,
- config
+ config,
)
toolResults.push(toolResult)
}
@@ -259,7 +272,7 @@ app.post('/chat-with-memory', async (req, res) => {
res.status(500).json({ error: 'Failed to process chat with memory' });
}
});
-`;
+`
// =====================================================
// Test with actual tool call from your example
@@ -267,7 +280,7 @@ app.post('/chat-with-memory', async (req, res) => {
export async function testWithRealToolCall() {
console.log("๐Ÿงช Testing with Real Tool Call from Your Example")
- console.log("=" .repeat(50))
+ console.log("=".repeat(50))
// This is the actual tool call Claude made in your example
const realToolCall = {
@@ -276,8 +289,8 @@ export async function testWithRealToolCall() {
name: "memory" as const,
input: {
command: "view" as const,
- path: "/memories"
- }
+ path: "/memories",
+ },
}
console.log("๐Ÿ” Tool call from Claude:")
@@ -294,8 +307,8 @@ export async function testWithRealToolCall() {
process.env.SUPERMEMORY_API_KEY,
{
projectId: "python-scraper-debug",
- memoryContainerTag: "claude_memory_test"
- }
+ memoryContainerTag: "claude_memory_test",
+ },
)
console.log("\\n๐Ÿ“‹ Tool Result to send back to Claude:")
@@ -308,7 +321,7 @@ export async function testWithRealToolCall() {
export async function runRealExamples() {
console.log("๐Ÿš€ Running Real Claude Memory Tool Examples")
- console.log("=" .repeat(70))
+ console.log("=".repeat(70))
// Test with the actual tool call first
await testWithRealToolCall()
@@ -324,11 +337,13 @@ export async function runRealExamples() {
console.log("\\n" + "=".repeat(70) + "\\n")
await realClaudeMemoryExample()
} else {
- console.log("\\nโš ๏ธ Set ANTHROPIC_API_KEY and SUPERMEMORY_API_KEY to run full API example")
+ console.log(
+ "\\nโš ๏ธ Set ANTHROPIC_API_KEY and SUPERMEMORY_API_KEY to run full API example",
+ )
}
}
// Run if executed directly
if (import.meta.main) {
runRealExamples()
-} \ No newline at end of file
+}
diff --git a/packages/tools/test/claude-memory.test.ts b/packages/tools/test/claude-memory.test.ts
index 1a5fa164..97bccd90 100644
--- a/packages/tools/test/claude-memory.test.ts
+++ b/packages/tools/test/claude-memory.test.ts
@@ -357,7 +357,8 @@ async function runManualTests() {
command: {
command: "create" as const,
path: "/memories/manual-test.md",
- file_text: "# Manual Test File\n\nThis is a test file for manual testing.\n\n- Item 1\n- Item 2\n- Item 3",
+ file_text:
+ "# Manual Test File\n\nThis is a test file for manual testing.\n\n- Item 1\n- Item 2\n- Item 3",
},
},
{
@@ -440,10 +441,12 @@ async function runManualTests() {
}
console.log("\nโœจ Manual tests completed!")
- console.log("Check your supermemory instance to verify the memory files were created correctly.")
+ console.log(
+ "Check your supermemory instance to verify the memory files were created correctly.",
+ )
}
// If this file is run directly, execute manual tests
if (import.meta.main) {
runManualTests()
-} \ No newline at end of file
+}
diff --git a/packages/tools/test/test-memory-tool.ts b/packages/tools/test/test-memory-tool.ts
index 21a7ccbe..c0395a79 100644
--- a/packages/tools/test/test-memory-tool.ts
+++ b/packages/tools/test/test-memory-tool.ts
@@ -9,7 +9,7 @@ import "dotenv/config"
async function testMemoryTool() {
console.log("๐Ÿงช Testing Claude Memory Tool Operations")
- console.log("=" .repeat(50))
+ console.log("=".repeat(50))
if (!process.env.SUPERMEMORY_API_KEY) {
console.error("โŒ SUPERMEMORY_API_KEY environment variable is required")
@@ -37,7 +37,8 @@ async function testMemoryTool() {
command: {
command: "create",
path: "/memories/project-notes.md",
- file_text: "# Project Notes\\n\\n## Meeting Notes\\n- Discussed requirements\\n- Set timeline\\n- Assigned tasks\\n\\n## Technical Stack\\n- Frontend: React\\n- Backend: Node.js\\n- Database: PostgreSQL",
+ file_text:
+ "# Project Notes\\n\\n## Meeting Notes\\n- Discussed requirements\\n- Set timeline\\n- Assigned tasks\\n\\n## Technical Stack\\n- Frontend: React\\n- Backend: Node.js\\n- Database: PostgreSQL",
},
expectSuccess: true,
},
@@ -46,7 +47,8 @@ async function testMemoryTool() {
command: {
command: "create",
path: "/memories/todo.txt",
- file_text: "TODO List:\\n1. Set up development environment\\n2. Create database schema\\n3. Build authentication system\\n4. Implement user dashboard\\n5. Write documentation",
+ file_text:
+ "TODO List:\\n1. Set up development environment\\n2. Create database schema\\n3. Build authentication system\\n4. Implement user dashboard\\n5. Write documentation",
},
expectSuccess: true,
},
@@ -99,7 +101,8 @@ async function testMemoryTool() {
command: {
command: "create",
path: "/memories/personal/preferences.md",
- file_text: "# My Preferences\\n\\n- Prefers React over Vue\\n- Uses TypeScript for type safety\\n- Likes clean, readable code\\n- Prefers functional programming style",
+ file_text:
+ "# My Preferences\\n\\n- Prefers React over Vue\\n- Uses TypeScript for type safety\\n- Likes clean, readable code\\n- Prefers functional programming style",
},
expectSuccess: true,
},
@@ -150,12 +153,16 @@ async function testMemoryTool() {
console.log("๐Ÿ“„ Result:")
console.log(result.content)
} else if (result.content) {
- console.log(`๐Ÿ“„ Result: ${result.content.substring(0, 100)}... (truncated)`)
+ console.log(
+ `๐Ÿ“„ Result: ${result.content.substring(0, 100)}... (truncated)`,
+ )
}
passed++
} else {
console.log("โŒ FAIL")
- console.log(`Expected success: ${testCase.expectSuccess}, got: ${result.success}`)
+ console.log(
+ `Expected success: ${testCase.expectSuccess}, got: ${result.success}`,
+ )
if (result.error) {
console.log(`Error: ${result.error}`)
}
@@ -168,20 +175,24 @@ async function testMemoryTool() {
}
// Add a small delay to avoid rate limiting
- await new Promise(resolve => setTimeout(resolve, 500))
+ await new Promise((resolve) => setTimeout(resolve, 500))
}
- console.log(`\\n๐Ÿ“Š Test Results:`)
+ console.log("\\n๐Ÿ“Š Test Results:")
console.log(`โœ… Passed: ${passed}`)
console.log(`โŒ Failed: ${failed}`)
- console.log(`๐Ÿ“ˆ Success Rate: ${((passed / (passed + failed)) * 100).toFixed(1)}%`)
+ console.log(
+ `๐Ÿ“ˆ Success Rate: ${((passed / (passed + failed)) * 100).toFixed(1)}%`,
+ )
if (failed === 0) {
- console.log("\\n๐ŸŽ‰ All tests passed! Claude Memory Tool is working perfectly!")
+ console.log(
+ "\\n๐ŸŽ‰ All tests passed! Claude Memory Tool is working perfectly!",
+ )
} else {
console.log(`\\nโš ๏ธ ${failed} test(s) failed. Check the errors above.`)
}
}
// Run the tests
-testMemoryTool().catch(console.error) \ No newline at end of file
+testMemoryTool().catch(console.error)
diff --git a/packages/tools/test/with-supermemory/integration.test.ts b/packages/tools/test/with-supermemory/integration.test.ts
index 98b7e853..4c112cb0 100644
--- a/packages/tools/test/with-supermemory/integration.test.ts
+++ b/packages/tools/test/with-supermemory/integration.test.ts
@@ -96,10 +96,14 @@ describe.skipIf(!shouldRunIntegration)(
const { model, getCapturedGenerateParams } =
createIntegrationMockModel()
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ },
+ )
await wrapped.doGenerate({
prompt: [
@@ -123,19 +127,26 @@ describe.skipIf(!shouldRunIntegration)(
const conversationId = `test-generate-${Date.now()}`
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- addMemory: "always",
- conversationId,
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ addMemory: "always",
+ conversationId,
+ },
+ )
await wrapped.doGenerate({
prompt: [
{
role: "user",
content: [
- { type: "text", text: "Remember that I love integration tests" },
+ {
+ type: "text",
+ text: "Remember that I love integration tests",
+ },
],
},
],
@@ -161,17 +172,23 @@ describe.skipIf(!shouldRunIntegration)(
const conversationId = `test-conversation-${Date.now()}`
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- conversationId,
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ conversationId,
+ },
+ )
await wrapped.doGenerate({
prompt: [
{
role: "user",
- content: [{ type: "text", text: "First message in conversation" }],
+ content: [
+ { type: "text", text: "First message in conversation" },
+ ],
},
],
})
@@ -186,10 +203,14 @@ describe.skipIf(!shouldRunIntegration)(
it("should fetch memories and stream response", async () => {
const { model, getCapturedStreamParams } = createIntegrationMockModel()
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ },
+ )
const { stream } = await wrapped.doStream({
prompt: [
@@ -221,12 +242,16 @@ describe.skipIf(!shouldRunIntegration)(
const conversationId = `test-stream-${Date.now()}`
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- addMemory: "always",
- conversationId,
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ addMemory: "always",
+ conversationId,
+ },
+ )
const { stream } = await wrapped.doStream({
prompt: [
@@ -261,10 +286,14 @@ describe.skipIf(!shouldRunIntegration)(
it("should handle text-delta chunks correctly", async () => {
const { model } = createIntegrationMockModel()
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ },
+ )
const { stream } = await wrapped.doStream({
prompt: [
@@ -298,10 +327,14 @@ describe.skipIf(!shouldRunIntegration)(
const { model } = createIntegrationMockModel()
const fetchSpy = vi.spyOn(globalThis, "fetch")
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ },
+ )
await wrapped.doGenerate({
prompt: [
@@ -322,7 +355,9 @@ describe.skipIf(!shouldRunIntegration)(
// Verify the request body does NOT contain 'q' for profile mode
const profileCall = profileCalls[0]
if (profileCall?.[1]) {
- const body = JSON.parse((profileCall[1] as RequestInit).body as string)
+ const body = JSON.parse(
+ (profileCall[1] as RequestInit).body as string,
+ )
expect(body.q).toBeUndefined()
}
@@ -333,10 +368,14 @@ describe.skipIf(!shouldRunIntegration)(
const { model } = createIntegrationMockModel()
const fetchSpy = vi.spyOn(globalThis, "fetch")
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "query",
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "query",
+ },
+ )
await wrapped.doGenerate({
prompt: [
@@ -357,7 +396,9 @@ describe.skipIf(!shouldRunIntegration)(
// Verify the request body contains 'q'
const profileCall = profileCalls[0]
if (profileCall?.[1]) {
- const body = JSON.parse((profileCall[1] as RequestInit).body as string)
+ const body = JSON.parse(
+ (profileCall[1] as RequestInit).body as string,
+ )
expect(body.q).toBe("What are my favorite foods?")
}
@@ -368,10 +409,14 @@ describe.skipIf(!shouldRunIntegration)(
const { model } = createIntegrationMockModel()
const fetchSpy = vi.spyOn(globalThis, "fetch")
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "full",
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "full",
+ },
+ )
await wrapped.doGenerate({
prompt: [
@@ -391,7 +436,9 @@ describe.skipIf(!shouldRunIntegration)(
const profileCall = profileCalls[0]
if (profileCall?.[1]) {
- const body = JSON.parse((profileCall[1] as RequestInit).body as string)
+ const body = JSON.parse(
+ (profileCall[1] as RequestInit).body as string,
+ )
expect(body.q).toBe("Full mode query test")
}
@@ -409,11 +456,15 @@ describe.skipIf(!shouldRunIntegration)(
generalSearchMemories: string
}) => `<custom-memories>${data.userMemories}</custom-memories>`
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- promptTemplate: customTemplate,
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ promptTemplate: customTemplate,
+ },
+ )
await wrapped.doGenerate({
prompt: [
@@ -434,11 +485,15 @@ describe.skipIf(!shouldRunIntegration)(
const { model, getCapturedGenerateParams } =
createIntegrationMockModel()
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- verbose: true,
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ verbose: true,
+ },
+ )
await wrapped.doGenerate({
prompt: [
@@ -459,11 +514,15 @@ describe.skipIf(!shouldRunIntegration)(
const fetchSpy = vi.spyOn(globalThis, "fetch")
// Use the configured base URL (or default)
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- baseUrl: INTEGRATION_CONFIG.baseUrl,
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ baseUrl: INTEGRATION_CONFIG.baseUrl,
+ },
+ )
await wrapped.doGenerate({
prompt: [
@@ -497,10 +556,14 @@ describe.skipIf(!shouldRunIntegration)(
new Error("Model error"),
)
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: INTEGRATION_CONFIG.apiKey,
- mode: "profile",
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: INTEGRATION_CONFIG.apiKey,
+ mode: "profile",
+ },
+ )
await expect(
wrapped.doGenerate({
@@ -517,10 +580,14 @@ describe.skipIf(!shouldRunIntegration)(
it("should handle invalid API key gracefully", async () => {
const { model } = createIntegrationMockModel()
- const wrapped = withSupermemory(model, INTEGRATION_CONFIG.containerTag, {
- apiKey: "invalid-api-key-12345",
- mode: "profile",
- })
+ const wrapped = withSupermemory(
+ model,
+ INTEGRATION_CONFIG.containerTag,
+ {
+ apiKey: "invalid-api-key-12345",
+ mode: "profile",
+ },
+ )
await expect(
wrapped.doGenerate({
diff --git a/packages/ui/button/external-auth.tsx b/packages/ui/button/external-auth.tsx
index 23164cb1..314cb4fb 100644
--- a/packages/ui/button/external-auth.tsx
+++ b/packages/ui/button/external-auth.tsx
@@ -20,8 +20,10 @@ export function ExternalAuthButton({
)}
style={{
borderRadius: "12px",
- background: "linear-gradient(180deg, #00264F 0%, #001933 100%), linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
- boxShadow: "0 1px 2px 0 rgba(0, 43, 87, 0.10), 1px 1px 1px 1px #002B57 inset",
+ background:
+ "linear-gradient(180deg, #00264F 0%, #001933 100%), linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
+ boxShadow:
+ "0 1px 2px 0 rgba(0, 43, 87, 0.10), 1px 1px 1px 1px #002B57 inset",
height: "44px",
}}
{...props}
diff --git a/packages/ui/components/grid-plus.tsx b/packages/ui/components/grid-plus.tsx
index b5b31bdb..5517e5b3 100644
--- a/packages/ui/components/grid-plus.tsx
+++ b/packages/ui/components/grid-plus.tsx
@@ -42,4 +42,4 @@ export const BackgroundPlus: React.FC<PlusPatternBackgroundProps> = ({
);
};
-export default BackgroundPlus; \ No newline at end of file
+export default BackgroundPlus;
diff --git a/packages/ui/components/hover-card.tsx b/packages/ui/components/hover-card.tsx
index bdf3ce94..d75f45f6 100644
--- a/packages/ui/components/hover-card.tsx
+++ b/packages/ui/components/hover-card.tsx
@@ -1,44 +1,43 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
-
-import { cn } from "@lib/utils"
+import { cn } from "@lib/utils";
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
+import type * as React from "react";
function HoverCard({
- ...props
+ ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
- return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
+ return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
- ...props
+ ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
- return (
- <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
- )
+ return (
+ <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
+ );
}
function HoverCardContent({
- className,
- align = "center",
- sideOffset = 4,
- ...props
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
- return (
- <HoverCardPrimitive.Portal data-slot="hover-card-portal">
- <HoverCardPrimitive.Content
- data-slot="hover-card-content"
- align={align}
- sideOffset={sideOffset}
- className={cn(
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
- className
- )}
- {...props}
- />
- </HoverCardPrimitive.Portal>
- )
+ return (
+ <HoverCardPrimitive.Portal data-slot="hover-card-portal">
+ <HoverCardPrimitive.Content
+ data-slot="hover-card-content"
+ align={align}
+ sideOffset={sideOffset}
+ className={cn(
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
+ className,
+ )}
+ {...props}
+ />
+ </HoverCardPrimitive.Portal>
+ );
}
-export { HoverCard, HoverCardTrigger, HoverCardContent }
+export { HoverCard, HoverCardTrigger, HoverCardContent };
diff --git a/packages/ui/components/sonner.tsx b/packages/ui/components/sonner.tsx
index f9bc649e..6a984710 100644
--- a/packages/ui/components/sonner.tsx
+++ b/packages/ui/components/sonner.tsx
@@ -9,14 +9,22 @@ const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
className="toaster group"
- style={
- {
- "--normal-bg": "var(--popover)",
- "--normal-text": "var(--popover-foreground)",
- "--normal-border": "var(--border)",
- } as React.CSSProperties
- }
theme={theme as ToasterProps["theme"]}
+ closeButton
+ toastOptions={{
+ classNames: {
+ toast:
+ "!bg-[#0b1017] !border !border-[#1b1f24] !rounded-[10px] !p-3 !shadow-lg",
+ title:
+ "!text-[#fafafa] !text-[12px] !leading-[1.35] !tracking-[-0.12px] !font-['DM_Sans',sans-serif]",
+ description:
+ "!text-[#fafafa] !text-[12px] !leading-[1.35] !tracking-[-0.12px] !font-['DM_Sans',sans-serif]",
+ closeButton:
+ "!bg-transparent !border-none !text-[#fafafa] hover:!bg-white/10 !size-6 !static !ml-2 !shrink-0",
+ actionButton: "!bg-white/10 !text-[#fafafa]",
+ cancelButton: "!bg-transparent !text-[#fafafa]",
+ },
+ }}
{...props}
/>
);
diff --git a/packages/ui/input/labeled-input.tsx b/packages/ui/input/labeled-input.tsx
index 5bad5410..c21e06c6 100644
--- a/packages/ui/input/labeled-input.tsx
+++ b/packages/ui/input/labeled-input.tsx
@@ -21,7 +21,9 @@ export function LabeledInput({
}: LabeledInputProps) {
return (
<div className={cn("flex flex-col gap-2", className)} {...props}>
- {label && <Label1Regular className="text-foreground">{label}</Label1Regular>}
+ {label && (
+ <Label1Regular className="text-foreground">{label}</Label1Regular>
+ )}
<Input
className={cn(
"w-full leading-[1.375rem] tracking-[-0.4px] rounded-xl p-4 placeholder:text-muted-foreground/50 text-foreground disabled:cursor-not-allowed disabled:opacity-50",
@@ -32,7 +34,8 @@ export function LabeledInput({
borderRadius: "12px",
border: "1px solid rgba(82, 89, 102, 0.20)",
background: "#070E1B",
- boxShadow: "0 1px 2px 0 rgba(0, 43, 87, 0.10), 0 0 0 1px rgba(43, 49, 67, 0.08) inset, 0 1px 1px 0 rgba(0, 0, 0, 0.08) inset, 0 2px 4px 0 rgba(0, 0, 0, 0.02) inset",
+ boxShadow:
+ "0 1px 2px 0 rgba(0, 43, 87, 0.10), 0 0 0 1px rgba(43, 49, 67, 0.08) inset, 0 1px 1px 0 rgba(0, 0, 0, 0.08) inset, 0 2px 4px 0 rgba(0, 0, 0, 0.02) inset",
}}
placeholder={inputPlaceholder}
type={inputType}
diff --git a/packages/ui/memory-graph/memory-graph.tsx b/packages/ui/memory-graph/memory-graph.tsx
index 8c1ad3c2..07895a20 100644
--- a/packages/ui/memory-graph/memory-graph.tsx
+++ b/packages/ui/memory-graph/memory-graph.tsx
@@ -34,7 +34,7 @@ export const MemoryGraph = ({
}: MemoryGraphProps) => {
// Derive showSpacesSelector from variant if not explicitly provided
// console variant shows spaces selector, consumer variant hides it
- const finalShowSpacesSelector = showSpacesSelector ?? (variant === "console");
+ const finalShowSpacesSelector = showSpacesSelector ?? variant === "console";
const [selectedSpace, setSelectedSpace] = useState<string>("all");
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
@@ -138,15 +138,10 @@ export const MemoryGraph = ({
autoFitToViewport(nodes, containerSize.width, containerSize.height);
hasAutoFittedRef.current = true;
}, 100);
-
+
return () => clearTimeout(timer);
}
- }, [
- nodes,
- containerSize.width,
- containerSize.height,
- autoFitToViewport,
- ]);
+ }, [nodes, containerSize.width, containerSize.height, autoFitToViewport]);
// Reset auto-fit flag when nodes array becomes empty (switching views)
useEffect(() => {
@@ -182,7 +177,7 @@ export const MemoryGraph = ({
if (containerRef.current) {
const newWidth = containerRef.current.clientWidth;
const newHeight = containerRef.current.clientHeight;
-
+
// Only update if size actually changed and is valid
setContainerSize((prev) => {
if (prev.width !== newWidth || prev.height !== newHeight) {
@@ -196,15 +191,15 @@ export const MemoryGraph = ({
// Use a slight delay to ensure DOM is fully rendered
const timer = setTimeout(updateSize, 0);
updateSize(); // Also call immediately
-
+
window.addEventListener("resize", updateSize);
-
+
// Use ResizeObserver for more accurate container size detection
const resizeObserver = new ResizeObserver(updateSize);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
-
+
return () => {
clearTimeout(timer);
window.removeEventListener("resize", updateSize);
@@ -224,32 +219,47 @@ export const MemoryGraph = ({
const handleCenter = useCallback(() => {
if (nodes.length > 0) {
// Calculate center of all nodes
- let sumX = 0
- let sumY = 0
- let count = 0
-
+ let sumX = 0;
+ let sumY = 0;
+ let count = 0;
+
nodes.forEach((node) => {
- sumX += node.x
- sumY += node.y
- count++
- })
-
+ sumX += node.x;
+ sumY += node.y;
+ count++;
+ });
+
if (count > 0) {
- const centerX = sumX / count
- const centerY = sumY / count
- centerViewportOn(centerX, centerY, containerSize.width, containerSize.height)
+ const centerX = sumX / count;
+ const centerY = sumY / count;
+ centerViewportOn(
+ centerX,
+ centerY,
+ containerSize.width,
+ containerSize.height,
+ );
}
}
- }, [nodes, centerViewportOn, containerSize.width, containerSize.height])
+ }, [nodes, centerViewportOn, containerSize.width, containerSize.height]);
const handleAutoFit = useCallback(() => {
- if (nodes.length > 0 && containerSize.width > 0 && containerSize.height > 0) {
+ if (
+ nodes.length > 0 &&
+ containerSize.width > 0 &&
+ containerSize.height > 0
+ ) {
autoFitToViewport(nodes, containerSize.width, containerSize.height, {
occludedRightPx,
animate: true,
- })
+ });
}
- }, [nodes, containerSize.width, containerSize.height, occludedRightPx, autoFitToViewport])
+ }, [
+ nodes,
+ containerSize.width,
+ containerSize.height,
+ occludedRightPx,
+ autoFitToViewport,
+ ]);
// Get selected node data
const selectedNodeData = useMemo(() => {
@@ -414,7 +424,7 @@ export const MemoryGraph = ({
WebkitUserSelect: "none",
}}
>
- {(containerSize.width > 0 && containerSize.height > 0) && (
+ {containerSize.width > 0 && containerSize.height > 0 && (
<GraphCanvas
draggingNodeId={draggingNodeId}
edges={edges}
@@ -445,8 +455,12 @@ export const MemoryGraph = ({
{containerSize.width > 0 && (
<NavigationControls
onCenter={handleCenter}
- onZoomIn={() => zoomIn(containerSize.width / 2, containerSize.height / 2)}
- onZoomOut={() => zoomOut(containerSize.width / 2, containerSize.height / 2)}
+ onZoomIn={() =>
+ zoomIn(containerSize.width / 2, containerSize.height / 2)
+ }
+ onZoomOut={() =>
+ zoomOut(containerSize.width / 2, containerSize.height / 2)
+ }
onAutoFit={handleAutoFit}
nodes={nodes}
className="absolute bottom-4 left-4"
diff --git a/packages/ui/memory-graph/navigation-controls.tsx b/packages/ui/memory-graph/navigation-controls.tsx
index b2abd67f..afd98a0f 100644
--- a/packages/ui/memory-graph/navigation-controls.tsx
+++ b/packages/ui/memory-graph/navigation-controls.tsx
@@ -1,67 +1,62 @@
-"use client"
+"use client";
-import { memo } from "react"
-import type { GraphNode } from "./types"
+import { memo } from "react";
+import type { GraphNode } from "./types";
interface NavigationControlsProps {
- onCenter: () => void
- onZoomIn: () => void
- onZoomOut: () => void
- onAutoFit: () => void
- nodes: GraphNode[]
- className?: string
+ onCenter: () => void;
+ onZoomIn: () => void;
+ onZoomOut: () => void;
+ onAutoFit: () => void;
+ nodes: GraphNode[];
+ className?: string;
}
-export const NavigationControls = memo<NavigationControlsProps>(({
- onCenter,
- onZoomIn,
- onZoomOut,
- onAutoFit,
- nodes,
- className = "",
-}) => {
- if (nodes.length === 0) {
- return null
- }
+export const NavigationControls = memo<NavigationControlsProps>(
+ ({ onCenter, onZoomIn, onZoomOut, onAutoFit, nodes, className = "" }) => {
+ if (nodes.length === 0) {
+ return null;
+ }
- return (
- <div className={`flex flex-col gap-1 ${className}`}>
- <button
- type="button"
- onClick={onAutoFit}
- className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
- title="Auto-fit graph to viewport"
- >
- Fit
- </button>
- <button
- type="button"
- onClick={onCenter}
- className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
- title="Center view on graph"
- >
- Center
- </button>
- <div className="flex flex-col">
+ return (
+ <div className={`flex flex-col gap-1 ${className}`}>
<button
type="button"
- onClick={onZoomIn}
- className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-t-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16 border-b-0"
- title="Zoom in"
+ onClick={onAutoFit}
+ className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
+ title="Auto-fit graph to viewport"
>
- +
+ Fit
</button>
<button
type="button"
- onClick={onZoomOut}
- className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-b-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
- title="Zoom out"
+ onClick={onCenter}
+ className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
+ title="Center view on graph"
>
- โˆ’
+ Center
</button>
+ <div className="flex flex-col">
+ <button
+ type="button"
+ onClick={onZoomIn}
+ className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-t-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16 border-b-0"
+ title="Zoom in"
+ >
+ +
+ </button>
+ <button
+ type="button"
+ onClick={onZoomOut}
+ className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-b-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
+ title="Zoom out"
+ >
+ โˆ’
+ </button>
+ </div>
</div>
- </div>
- )
-})
+ );
+ },
+);
-NavigationControls.displayName = "NavigationControls" \ No newline at end of file
+NavigationControls.displayName = "NavigationControls";
diff --git a/packages/ui/pages/login.tsx b/packages/ui/pages/login.tsx
index 69b52089..e0c0fe04 100644
--- a/packages/ui/pages/login.tsx
+++ b/packages/ui/pages/login.tsx
@@ -5,8 +5,8 @@ import { usePostHog } from "@lib/posthog";
import { LogoFull } from "@repo/ui/assets/Logo";
import { TextSeparator } from "@repo/ui/components/text-separator";
import { ExternalAuthButton } from "@ui/button/external-auth";
-import { Button } from "@ui/components/button";
import { Badge } from "@ui/components/badge";
+import { Button } from "@ui/components/button";
import {
Carousel,
CarouselContent,
@@ -20,7 +20,7 @@ import { Title1Bold } from "@ui/text/title/title-1-bold";
import Autoplay from "embla-carousel-autoplay";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
-import { useState, useEffect } from "react";
+import { useEffect, useState } from "react";
export function LoginPage({
heroText = "The unified memory API for the AI era.",
@@ -480,4 +480,4 @@ export function LoginPage({
)}
</section>
);
-} \ No newline at end of file
+}