diff options
| author | MaheshtheDev <[email protected]> | 2026-01-21 03:11:53 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2026-01-21 03:11:53 +0000 |
| commit | 1423bd70041c8dc0d863c13f1377865c6c875181 (patch) | |
| tree | 29155642e31153d6873640aa5e4d8af5c45e213e /apps | |
| parent | feat: create space, delete spaces and emoji picker (#687) (diff) | |
| download | supermemory-1423bd70041c8dc0d863c13f1377865c6c875181.tar.xz supermemory-1423bd70041c8dc0d863c13f1377865c6c875181.zip | |
feat: mobile responsive, lint formats, toast, render issue fix (#688)01-20-feat_mobile_responsive_lint_formats_ui_improvements_render_issue_fix
- Mobile responsive
- new toast design
- web document render issue fix
- posthog analytics
- ui improvements
Diffstat (limited to 'apps')
80 files changed, 2370 insertions, 1794 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" + } + ] } |